Skip to content

Commit ebf7540

Browse files
committed
Fix UI/UX issues in workspace layout, scrolling, export, and license (closes #13)
- Replace overflow:hidden body lock with natural body scroll; footer now reachable - Sidebar: position:sticky with max-height and gradient fade mask - Workspace layout: standard CSS grid (280px 1fr) inside max-width container - Move Back/Copy All/Download to page-level top bar above session grid - Wrap session header + chat bubbles in a single card extending to bottom of chat - Add max-width:920px on session content for readable line length - Add GET /api/export/state endpoint; POST /api/export supports since=last filtering - Add Export new since last button; projects page refreshes after export - Exported filenames: timestamp prefix added, 60-char slug truncation removed - Add LICENSE (Boost Software License 1.0); fix footer license text and year Made-with: Cursor
1 parent 2ca2d53 commit ebf7540

5 files changed

Lines changed: 212 additions & 104 deletions

File tree

LICENSE

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Boost Software License - Version 1.0 - August 17th, 2003
2+
3+
Permission is hereby granted, free of charge, to any person or organization
4+
obtaining a copy of the software and accompanying documentation covered by
5+
this license (the "Software") to use, reproduce, display, distribute,
6+
execute, and transmit the Software, and to prepare derivative works of the
7+
Software, and to permit third-parties to whom the Software is furnished to
8+
do so, all subject to the following:
9+
10+
The copyright notices in the Software and this entire statement, including
11+
the above license grant, this restriction and the following disclaimer,
12+
must be included in all copies of the Software, in whole or in part, and
13+
all derivative works of the Software, unless such copies or derivative
14+
works are solely in the form of machine-executable object code generated by
15+
a source language processor.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
20+
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
21+
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
22+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
DEALINGS IN THE SOFTWARE.

api/export_api.py

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io
44
import json
5+
import os
56
import zipfile
67
from datetime import datetime
78

@@ -16,6 +17,28 @@
1617

1718
export_bp = Blueprint("export", __name__)
1819

20+
_STATE_FILE = os.path.join(os.path.expanduser("~"), ".claude-code-chat-browser", "export_state.json")
21+
22+
23+
def _read_state() -> dict:
24+
if os.path.exists(_STATE_FILE):
25+
try:
26+
with open(_STATE_FILE) as f:
27+
return json.load(f)
28+
except Exception:
29+
pass
30+
return {}
31+
32+
33+
def _write_state(sessions_map: dict, count: int):
34+
os.makedirs(os.path.dirname(_STATE_FILE), exist_ok=True)
35+
state = _read_state()
36+
state["lastExportTime"] = datetime.now().isoformat()
37+
state["exportedCount"] = count
38+
state.setdefault("sessions", {}).update(sessions_map)
39+
with open(_STATE_FILE, "w") as f:
40+
json.dump(state, f, indent=2)
41+
1942

2043
def _session_text_for_exclusion(session: dict) -> str:
2144
"""Extract a plain-text snippet from session messages for exclusion matching."""
@@ -27,20 +50,50 @@ def _session_text_for_exclusion(session: dict) -> str:
2750
return "\n\n".join(parts)
2851

2952

53+
@export_bp.route("/api/export/state")
54+
def get_export_state():
55+
state = _read_state()
56+
return jsonify({
57+
"last_export_time": state.get("lastExportTime"),
58+
"export_count": state.get("exportedCount", 0),
59+
})
60+
61+
62+
def _slugify(text: str) -> str:
63+
import re
64+
text = text.lower()
65+
text = re.sub(r"[^a-z0-9]+", "-", text)
66+
return text.strip("-")
67+
68+
3069
@export_bp.route("/api/export", methods=["POST"])
3170
def bulk_export():
71+
body = request.get_json(silent=True) or {}
72+
since = "last" if body.get("since") == "last" else "all"
73+
3274
base = current_app.config.get("CLAUDE_PROJECTS_DIR") or get_claude_projects_dir()
3375
projects = list_projects(base)
3476
rules = current_app.config.get("EXCLUSION_RULES") or []
3577

78+
state = _read_state()
79+
last_export_sessions: dict = state.get("sessions", {}) if since == "last" else {}
80+
3681
buf = io.BytesIO()
3782
count = 0
3883
manifest = []
84+
new_sessions_map: dict = {}
3985
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
4086
for project in projects:
4187
sessions = list_sessions(project["path"])
4288
for sess_info in sessions:
89+
sid = sess_info["id"]
4390
try:
91+
if since == "last":
92+
prev_mtime = last_export_sessions.get(sid, 0)
93+
curr_mtime = sess_info.get("modified", 0)
94+
if curr_mtime and curr_mtime <= prev_mtime:
95+
continue
96+
4497
session = parse_session(sess_info["path"])
4598
if session["title"] == "Untitled Session":
4699
continue
@@ -58,35 +111,42 @@ def bulk_export():
58111

59112
stats = compute_stats(session)
60113
md = session_to_markdown(session, stats)
61-
title_slug = _slugify(session["title"])[:60] or "session"
62-
short_id = sess_info["id"][:8]
114+
title_slug = _slugify(session["title"]) or "session"
115+
short_id = sid[:8]
63116
proj_slug = _slugify(project["name"])
64-
rel_path = f"{proj_slug}/{title_slug}__{short_id}.md"
117+
ts = session["metadata"].get("first_timestamp", "")
118+
ts_file = ts[:19].replace(":", "-") if ts else "0000-00-00T00-00-00"
119+
rel_path = f"{proj_slug}/{ts_file}__{title_slug}__{short_id}.md"
65120
zf.writestr(rel_path, md)
66121
manifest.append({
67-
"session_id": sess_info["id"],
122+
"session_id": sid,
68123
"title": session["title"],
69124
"project": project["name"],
70125
"tokens": session["metadata"]["total_input_tokens"]
71126
+ session["metadata"]["total_output_tokens"],
72127
"tool_calls": session["metadata"]["total_tool_calls"],
73128
"cost_estimate_usd": stats.get("cost_estimate_usd"),
74129
})
130+
new_sessions_map[sid] = sess_info.get("modified", 0)
75131
count += 1
76132
except Exception as e:
77-
current_app.logger.warning("Failed to export %s: %s", sess_info["id"][:10], e)
133+
current_app.logger.warning("Failed to export %s: %s", sid[:10], e)
78134
continue
79135
if manifest:
80136
manifest_str = "\n".join(json.dumps(e, default=str) for e in manifest)
81137
zf.writestr("manifest.jsonl", manifest_str)
82138

139+
if count > 0:
140+
_write_state(new_sessions_map, count)
141+
83142
buf.seek(0)
84143
date_tag = datetime.now().strftime("%Y-%m-%d")
144+
suffix = "-since-last" if since == "last" else ""
85145
return send_file(
86146
buf,
87147
mimetype="application/zip",
88148
as_attachment=True,
89-
download_name=f"claude-code-export-{date_tag}.zip",
149+
download_name=f"claude-code-export{suffix}-{date_tag}.zip",
90150
)
91151

92152

@@ -106,7 +166,7 @@ def export_session(project_name, session_id):
106166
fmt = request.args.get("format", "md")
107167
session = parse_session(filepath)
108168
stats = compute_stats(session)
109-
title_slug = _slugify(session["title"])[:60] or "session"
169+
title_slug = _slugify(session["title"]) or "session"
110170

111171
if fmt == "json":
112172
content = session_to_json(session, stats)
@@ -130,13 +190,3 @@ def export_session(project_name, session_id):
130190
)
131191

132192

133-
def _slugify(text: str) -> str:
134-
slug = ""
135-
for c in text.lower():
136-
if c.isalnum():
137-
slug += c
138-
elif c in " -_/.":
139-
slug += "-"
140-
while "--" in slug:
141-
slug = slug.replace("--", "-")
142-
return slug.strip("-")

static/css/style.css

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* ============================================================
22
Claude Code Chat Browser — CSS
3-
Palette and components match cursor-chat-browser-python.
43
Claude-specific additions (stat badges, toast, confirm,
54
loading bar, scroll-top) are in their own sections below.
65
============================================================ */
@@ -162,8 +161,6 @@ a:hover { text-decoration: underline; }
162161
.footer a { color: var(--text-muted); }
163162
.footer a:hover { color: var(--text); }
164163

165-
/* Hide footer in workspace mode */
166-
body.workspace-mode .footer { display: none; }
167164

168165
/* ---------- Typography ---------- */
169166
h1 { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; }
@@ -285,43 +282,67 @@ h3 { font-size: 1.15rem; font-weight: 600; }
285282
/* ---------- Utility ---------- */
286283
.flex-between { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
287284

288-
/* ---------- Workspace Layout (full-height split) ---------- */
289-
body.workspace-mode { overflow: hidden; }
290-
body.workspace-mode #content { padding: 0; max-width: none; flex: 1; overflow: hidden; display: flex; flex-direction: column; }
285+
/* ---------- Workspace Layout ---------- */
286+
/* Body scrolls naturally; no overflow:hidden trap — footer is always reachable */
291287

292-
.workspace-layout {
288+
.workspace-top-bar {
293289
display: flex;
294-
flex: 1;
295-
height: calc(100vh - 3.5rem);
296-
overflow: hidden;
290+
align-items: center;
291+
justify-content: space-between;
292+
gap: 1rem;
293+
margin-bottom: 1rem;
294+
}
295+
296+
/* 2-column grid: fixed sidebar + fluid main panel */
297+
.workspace-wrap {
298+
display: grid;
299+
grid-template-columns: 280px 1fr;
300+
gap: 1.5rem;
301+
align-items: start;
302+
}
303+
304+
@media (max-width: 768px) {
305+
.workspace-wrap { grid-template-columns: 1fr; }
297306
}
298307

299308
/* ---------- Sidebar ---------- */
300309
.sidebar {
301-
width: 280px;
302-
min-width: 280px;
303-
border-right: 1px solid var(--border);
304-
background: var(--bg-card);
310+
position: sticky;
311+
top: 5rem; /* below 3.5rem navbar + 1rem gap */
312+
max-height: calc(100vh - 7rem);
305313
overflow-y: auto;
306-
height: 100%;
314+
border: 1px solid var(--border);
315+
border-radius: 8px;
316+
padding: 0.75rem;
317+
background: var(--bg-card);
318+
mask-image: linear-gradient(
319+
to bottom,
320+
transparent 0%,
321+
black 2%,
322+
black 98%,
323+
transparent 100%
324+
);
325+
-webkit-mask-image: linear-gradient(
326+
to bottom,
327+
transparent 0%,
328+
black 2%,
329+
black 98%,
330+
transparent 100%
331+
);
307332
scrollbar-width: thin;
308333
scrollbar-color: var(--border) transparent;
309-
mask-image: linear-gradient(to bottom, transparent 0%, black 2%, black 98%, transparent 100%);
310-
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 2%, black 98%, transparent 100%);
311334
}
312335
.sidebar::-webkit-scrollbar { width: 6px; }
313336
.sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
314337
.sidebar::-webkit-scrollbar-track { background: transparent; }
315338

316339
.sidebar-header {
317-
padding: 0.75rem;
318340
font-size: 0.875rem;
319341
font-weight: 600;
320-
border-bottom: 1px solid var(--border);
342+
margin-bottom: 0.5rem;
343+
padding: 0 0.25rem;
321344
}
322345

323-
.sidebar-body { padding: 0.75rem; }
324-
325346
.sidebar-item {
326347
display: block;
327348
width: 100%;
@@ -371,25 +392,24 @@ body.workspace-mode #content { padding: 0; max-width: none; flex: 1; overflow: h
371392
opacity: 0.6;
372393
}
373394

374-
/* ---------- Main panel ---------- */
395+
/* ---------- Main panel — normal document flow, body scrolls ---------- */
375396
.main-panel {
376-
flex: 1;
377-
overflow-y: auto;
378-
height: 100%;
379-
scrollbar-width: thin;
380-
scrollbar-color: var(--border) transparent;
397+
min-width: 0;
381398
}
382-
.main-panel::-webkit-scrollbar { width: 6px; }
383-
.main-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
384399

385400
/* ---------- Project info ---------- */
386401
.project-info {
387402
padding: 1.25rem 1.5rem;
388403
background: var(--bg-muted);
389-
border-bottom: 1px solid var(--border);
404+
margin-bottom: 1.5rem;
390405
}
391406
.project-info h2 { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.15rem; }
392407

408+
/* ---------- Session content ---------- */
409+
.session-content-inner {
410+
max-width: 920px;
411+
}
412+
393413
/* ---------- Panel header ---------- */
394414
.panel-header {
395415
padding: 1.25rem 1.5rem 0.75rem;
@@ -904,12 +924,9 @@ pre code { background: none; padding: 0; font-size: inherit; }
904924
}
905925
.sidebar.open { transform: translateX(0); }
906926

907-
.main-panel { width: 100%; }
908927
.panel-header { flex-direction: column; gap: 0.75rem; }
909928
.panel-title { font-size: 1rem; }
910929
.stat-badges { gap: 0.25rem; }
911930
.stat-badge { font-size: 0.65rem; padding: 0.1rem 0.4rem; }
912931
.chat-bubbles { padding: 0.75rem; }
913-
body.workspace-mode #content { overflow: auto; }
914-
.workspace-layout { height: auto; flex-direction: column; }
915932
}

static/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<!-- Footer (hidden in workspace mode via CSS) -->
4141
<footer class="footer">
4242
<div class="footer-inner">
43-
<span>&copy; 2025 Claude Code Chat Browser. MIT License.</span>
43+
<span>&copy; <span id="footer-year">2025</span> Claude Code Chat Browser. Boost Software License 1.0.</span>
4444
<a href="https://github.com/CppDigest/claude-code-chat-browser" target="_blank" rel="noopener noreferrer" title="GitHub">
4545
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
4646
</a>

0 commit comments

Comments
 (0)