feat: bidirectional session rename via cc-native custom-title (Claude Code)#8
feat: bidirectional session rename via cc-native custom-title (Claude Code)#8nengqi wants to merge 4 commits into
Conversation
745051d to
6b24d97
Compare
…m-title row
Adds setSessionAlias to ClaudeCodeAgent that upserts a single
{type:"custom-title",customTitle,sessionId} record — the same row that
`claude --name` writes. This makes renames bidirectional:
- codesesh-side renames immediately surface in cc's /resume picker,
prompt box, and terminal title.
- cc-side renames done with --name show up in codesesh on next scan.
cc appends a new custom-title row on every resume rather than
upserting, so parseSessionHead reads the *last* row (matching cc's own
behavior) and setSessionAlias collapses any existing rows down to one
on every save to keep the file from growing unboundedly.
Title precedence becomes:
custom-title (== claude --name) > sessions-index.json summary
> session-end-hook summary > first user message > directory basename.
Verified end-to-end: cc resume succeeds against jsonl files written by
codesesh, and cc's append-on-resume of the same custom-title is
idempotent on the next scan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validates that the agent advertises setSessionAlias (currently only
claudecode), accepts {title: string | null}, and returns the refreshed
SessionHead. Eagerly patches the in-memory scan snapshot so subsequent
GETs see the new title without waiting for chokidar's debounced rescan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@xingkaixin 已 rebase 到最新 main ( 为了让 diff 好 review,drop 了 web 那个 commit ( 现在 PR 剩 2 个 commit / ~560 LOC:
想问下:你倾向只合后端,还是希望我把 sidebar 适配的 UI 也补完再 review? |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6156f373f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| customTitle: desired, | ||
| sessionId, | ||
| }); | ||
| if (customTitleIndices.length === 0) { | ||
| dataLines.unshift(newLine); |
There was a problem hiding this comment.
Keep original timestamp when prepending custom-title
When a session has no existing custom-title, this prepends a new first JSONL row that has no timestamp. parseSessionHead() derives time_created from the first row and falls back to file mtime when that timestamp is missing, so after the first rename the session's creation time is rewritten to “now” and continues drifting with future file writes. This can reorder old sessions and skew date-window/recent-session logic that depends on time_created.
Useful? React with 👍 / 👎.
| if (idx >= 0) agentSessions[idx] = updated; | ||
| } | ||
| const flatIdx = scanResult.sessions.findIndex((session) => session.id === sessionId); | ||
| if (flatIdx >= 0) scanResult.sessions[flatIdx] = updated; |
There was a problem hiding this comment.
Merge rename result instead of replacing enriched snapshot entries
The handler replaces cached session objects with updated directly, but setSessionAlias() returns a bare SessionHead from parseSessionHead() that does not include scanner-enriched fields like project_identity and smart_tags. After a rename, those fields are dropped from in-memory snapshots until the next rescan, so project/tag-filtered endpoints can temporarily omit or misclassify the renamed session.
Useful? React with 👍 / 👎.
P1 (claudecode.ts) — When setSessionAlias prepended a custom-title row to a session that had no existing custom-title, the new first row had no `timestamp` field. parseSessionHead derives time_created from lines[0]'s timestamp and falls back to file mtime when absent, so the session's creation time was silently being re-anchored to "now" on every rename and continued drifting with future writes — corrupting time-based sort and date-window filters. Now both the prepend path and the replace-at-row-0 path copy a timestamp from another existing record into the custom-title row before writing. parseSessionHead therefore reads a stable timestamp regardless of how many rename round-trips have happened. P2 (handlers.ts) — handlePatchSession was replacing in-memory snapshot entries with the bare SessionHead returned by setSessionAlias()/parse- SessionHead(), which dropped scanner-enriched fields (project_identity, smart_tags, smart_tags_source_updated_at) until the next chokidar rescan. Project- and tag-filtered endpoints could temporarily omit or misclassify the renamed session. Merged per-field with `existing ?? updated` fallbacks so enriched fields survive a rename. Response body now returns the merged record instead of the bare one. Tests: - New regression: time_created remains stable across 3 renames (initial, re-rename, clear) even when file mtime is forced to a far-future value. - New regression: project_identity / smart_tags / smart_tags_source_updated_at survive handlePatchSession and appear in both the in-memory snapshot and the response body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
你可以提交完整的前后端的功能代码,这个功能是可以对所有 Coding Agent 都适用的。自定义的标题持久化存储到 sqlite 中。 |
…has one (codex review) Codex flagged a remaining edge case in setSessionAlias' replace-at-row-0 path: `findFallbackTimestamp(0)` unconditionally skips index 0 to avoid adopting the row's own (potentially missing) timestamp. But if a previous codesesh-written custom-title at row 0 was the only timestamp source in the file — e.g. after cc append-on-resume'd another timestamp-less custom-title at the tail — the helper returns undefined and we'd write a new row 0 without a timestamp, re-triggering the original P1 mtime drift. Added a `readRowTimestamp(idx)` helper and chained it via `??` so the replace-at-row-0 path now prefers any other record's timestamp first, but falls back to the existing row 0's own timestamp when nothing else has one. Regression test: file shape `[codesesh-custom-title with ts, cc-custom- title without ts]` plus mtime forced to 2099. Without the fallback, time_created drifts to 2099; with it, time_created stays at the original ts and the surviving collapsed custom-title carries the preserved ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@xingkaixin codex 反馈的两条都修了,push 上去了 (commit P1 ( 我自己 review 时多发现一个 codex 没明说的边界——replace-at-row-0 时如果其他 row 都没有 timestamp (比如 cc append-on-resume 留下了无 timestamp 的 custom-title),会回退到 row 0 自己已有的 timestamp,而不是写一个无 timestamp 的新 row 0 复发原 bug。 P2 ( 测试:
剩下还是想问下你倾向:就先合后端这版本,还是要等 sidebar 适配的 UI 一起? |
好的,等 UI 一起吧。 |
|
@xingkaixin 谢谢方向反馈。SQLite-backed alias + 所有 agent 适用是一个干净的统一抽象,但跟这个 PR 的设计核心冲突——这个 PR 故意写 cc 自己的 所以这个 PR 不再适合直接演化成你想要的形态,close 掉避免占着 PR 列表 noise。如果之后你实现 SQLite alias 表后还想要 cc 这个 agent 上的双向同步作为 nice-to-have,这里的 |
Summary
Lets users rename a Claude Code session from either the detail header or any sidebar list item. Renames round-trip through cc's own
{type:"custom-title", customTitle, sessionId}jsonl record — the exact fieldclaude --namewrites — so the alias is bidirectional: codesesh-side renames show up in cc's/resumepicker / prompt box / terminal title, andclaude --name Xfrom the terminal shows up in codesesh on next scan.ClaudeCodeAgent.setSessionAlias()upserts a singlecustom-titlerow (atomic temp-file + rename), collapsing any duplicates cc accumulates from append-on-resume;parseSessionHead()now reads the lastcustom-titlerow (matching cc's own behavior). New title precedence:custom-title→sessions-index.jsonsummary →session-end-hooksummary → first user message → directory basename.PATCH /api/sessions/:agent/:idwith body{title: string | null}. Empty/null clears the alias. Gated to agents that advertisesetSessionAlias— onlyclaudecodefor this PR. The in-memory scan snapshot is patched in place so subsequent GETs don't have to wait for chokidar's debounced rescan.Why cc-native
custom-titleinstead of a codesesh-only field?cc itself already writes
{type:"custom-title", customTitle, sessionId}records into the jsonl when launched with--name, and reads them back for the/resumepicker, prompt box, and terminal title. Using the same row makes the alias bidirectional with zero extra storage. An earlier draft used{type:"summary", source:"codesesh-alias"}; that worked but was a one-way private channel — cc had no way to see codesesh-set names and codesesh had no way to seeclaude --namenames.End-to-end verified that cc successfully
--resumes a jsonl that codesesh wrote, and that cc's append-on-resume of the same custom-title is idempotent on the next scan.Scope: claudecode only
The other agents (cursor / kimi / codex / opencode) all use different storage formats and would each need bespoke write paths (codex has a native
summaryfield, opencode is a SQLite UPDATE, etc.). Wiring those up after the UX is validated; for now the rename pencil only renders when the active agent is claudecode.Test plan
pnpm test— 132 tests pass (102 core + 30 cli)pnpm lintcleanpnpm --filter @codesesh/web build && pnpm --filter codesesh buildcleancustom-titlerows collapsed to a single row (no duplicates)title: null) removes allcustom-titlerows; codesesh falls back through the precedence chainexit 0) and cc's append-on-resume of the same name doesn't change codesesh's readingclaude --name X --resumewrites a custom-title row that codesesh reads on next scan🤖 Generated with Claude Code