Skip to content

feat: bidirectional session rename via cc-native custom-title (Claude Code)#8

Closed
nengqi wants to merge 4 commits into
xingkaixin:mainfrom
nengqi:feat/session-alias
Closed

feat: bidirectional session rename via cc-native custom-title (Claude Code)#8
nengqi wants to merge 4 commits into
xingkaixin:mainfrom
nengqi:feat/session-alias

Conversation

@nengqi

@nengqi nengqi commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

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 field claude --name writes — so the alias is bidirectional: codesesh-side renames show up in cc's /resume picker / prompt box / terminal title, and claude --name X from the terminal shows up in codesesh on next scan.

  • core: ClaudeCodeAgent.setSessionAlias() upserts a single custom-title row (atomic temp-file + rename), collapsing any duplicates cc accumulates from append-on-resume; parseSessionHead() now reads the last custom-title row (matching cc's own behavior). New title precedence: custom-titlesessions-index.json summary → session-end-hook summary → first user message → directory basename.
  • api: PATCH /api/sessions/:agent/:id with body {title: string | null}. Empty/null clears the alias. Gated to agents that advertise setSessionAlias — only claudecode for this PR. The in-memory scan snapshot is patched in place so subsequent GETs don't have to wait for chokidar's debounced rescan.
  • web: Pencil icon (a) next to the detail header title and (b) on every sidebar list item. Both swap the title for an inline input (Enter saves, Esc cancels, blur saves) and share a single handler that optimistically patches detail + sidebar state.

Why cc-native custom-title instead 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 /resume picker, 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 see claude --name names.

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 summary field, 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)
    • core: prepends a custom-title row, collapses cc's append-on-resume duplicates into one, reads last row when several exist, prefers custom-title over hook summary, clear removes all custom-title rows and falls back, returns null for unknown sessions
    • api: 404 unknown agent, 400 unsupported agent / missing title / non-string title, 404 missing session, in-memory snapshot patched on success, 200-char clamp, null clears
  • pnpm lint clean
  • pnpm --filter @codesesh/web build && pnpm --filter codesesh build clean
  • CDP end-to-end self-check against the running daemon:
    • Detail header: pencil → input → type → Enter → header + sidebar + jsonl all updated; Esc cancels without round-trip
    • Sidebar item: pencil → input → Enter saves; Esc cancels
    • Re-rename: existing custom-title rows collapsed to a single row (no duplicates)
    • Clear (title: null) removes all custom-title rows; codesesh falls back through the precedence chain
    • cc resume against a codesesh-renamed session succeeds (exit 0) and cc's append-on-resume of the same name doesn't change codesesh's reading
    • Reverse direction: claude --name X --resume writes a custom-title row that codesesh reads on next scan
    • Bad payloads return 400; unknown agents return 400/404 as expected

🤖 Generated with Claude Code

@nengqi nengqi force-pushed the feat/session-alias branch from 745051d to 6b24d97 Compare April 26, 2026 17:26
@nengqi nengqi changed the title feat: inline session rename for Claude Code (alias persisted in jsonl) feat: bidirectional session rename via cc-native custom-title (Claude Code) Apr 26, 2026
nengqi and others added 2 commits May 7, 2026 12:21
…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>
@nengqi nengqi force-pushed the feat/session-alias branch from 6b24d97 to 6156f37 Compare May 7, 2026 04:31
@nengqi

nengqi commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@xingkaixin 已 rebase 到最新 main (0a5a1e4)。

为了让 diff 好 review,drop 了 web 那个 commit (6b24d97 inline session rename in detail header and sidebar list)——它是基于旧版 App.tsx 的 sidebar 写的,新的 SessionTreeSidebar 重构后那段位置已经没了,需要重写 UI 才能适配。如果后端这部分你想合,UI 我后续再单独提 PR;不想合的话这两个 commit 一起丢也可以。

现在 PR 剩 2 个 commit / ~560 LOC:

  • core: ClaudeCodeAgent.setSessionAlias() 写一行 cc 自己的 custom-title row,跟 claude --name 双向通用
  • api: PATCH /api/sessions/:agent/:id,只对支持 setSessionAlias 的 agent 开放

pnpm test 我这边 core 全过 (133/133),cli 我新加的 8 个 test 也都过;cli 那 3 个 fail 在 main 上就有 (computeIdentity / listCachedProjectGroups / Test Session 那个),不是这个 PR 引入。pnpm lint 干净。

想问下:你倾向只合后端,还是希望我把 sidebar 适配的 UI 也补完再 review?

@xingkaixin

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +259 to +263
customTitle: desired,
sessionId,
});
if (customTitleIndices.length === 0) {
dataLines.unshift(newLine);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread packages/cli/src/api/handlers.ts Outdated
Comment on lines +379 to +382
if (idx >= 0) agentSessions[idx] = updated;
}
const flatIdx = scanResult.sessions.findIndex((session) => session.id === sessionId);
if (flatIdx >= 0) scanResult.sessions[flatIdx] = updated;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@xingkaixin

Copy link
Copy Markdown
Owner

@xingkaixin 已 rebase 到最新 main (0a5a1e4)。

为了让 diff 好 review,drop 了 web 那个 commit (6b24d97 inline session rename in detail header and sidebar list)——它是基于旧版 App.tsx 的 sidebar 写的,新的 SessionTreeSidebar 重构后那段位置已经没了,需要重写 UI 才能适配。如果后端这部分你想合,UI 我后续再单独提 PR;不想合的话这两个 commit 一起丢也可以。

现在 PR 剩 2 个 commit / ~560 LOC:

  • core: ClaudeCodeAgent.setSessionAlias() 写一行 cc 自己的 custom-title row,跟 claude --name 双向通用
  • api: PATCH /api/sessions/:agent/:id,只对支持 setSessionAlias 的 agent 开放

pnpm test 我这边 core 全过 (133/133),cli 我新加的 8 个 test 也都过;cli 那 3 个 fail 在 main 上就有 (computeIdentity / listCachedProjectGroups / Test Session 那个),不是这个 PR 引入。pnpm lint 干净。

想问下:你倾向只合后端,还是希望我把 sidebar 适配的 UI 也补完再 review?

你可以提交完整的前后端的功能代码,这个功能是可以对所有 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>
@nengqi

nengqi commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@xingkaixin codex 反馈的两条都修了,push 上去了 (commit 819d126 + 4e2487d)。

P1 (claudecode.ts):prepend 和 replace-at-row-0 两个路径写入 custom-title 时,从存量记录拷贝一个 timestamp 字段塞到新 row 上,这样 parseSessionHeadlines[0] 解析时不会 fallback 到 file mtime,session 创建时间就不会随每次 rename 漂移到「现在」。

我自己 review 时多发现一个 codex 没明说的边界——replace-at-row-0 时如果其他 row 都没有 timestamp (比如 cc append-on-resume 留下了无 timestamp 的 custom-title),会回退到 row 0 自己已有的 timestamp,而不是写一个无 timestamp 的新 row 0 复发原 bug。commit 4e2487d 修这个,带一个对应的 regression test (文件形态 [codesesh-custom-title with ts, cc-custom-title without ts] + mtime 强制设到 2099,验证 time_created 不漂移)。

P2 (handlers.ts):handlePatchSession 改成 explicit per-field merge with updated ?? existing,显式保留 project_identity / smart_tags / smart_tags_source_updated_at,响应体也返回 merged 后的而不是 bare SessionHead。看了 SessionHead 类型定义,目前 scanner-enriched 的 optional 字段就是这三个。

测试:

  • core 135/135 全过 (新增 2 个 P1 regression: time_created 跨 3 次 rename 不漂移 + row-0-only-timestamp-source 边界)
  • cli 我新加的 8 个 PATCH 相关 test 都过 (那 3 个 baseline fail 跟之前一样,在 main 上就有,不是这 PR 引入)
  • lint 干净

剩下还是想问下你倾向:就先合后端这版本,还是要等 sidebar 适配的 UI 一起?

@xingkaixin

Copy link
Copy Markdown
Owner

@xingkaixin codex 反馈的两条都修了,push 上去了 (commit 819d126 + 4e2487d)。

P1 (claudecode.ts):prepend 和 replace-at-row-0 两个路径写入 custom-title 时,从存量记录拷贝一个 timestamp 字段塞到新 row 上,这样 parseSessionHeadlines[0] 解析时不会 fallback 到 file mtime,session 创建时间就不会随每次 rename 漂移到「现在」。

我自己 review 时多发现一个 codex 没明说的边界——replace-at-row-0 时如果其他 row 都没有 timestamp (比如 cc append-on-resume 留下了无 timestamp 的 custom-title),会回退到 row 0 自己已有的 timestamp,而不是写一个无 timestamp 的新 row 0 复发原 bug。commit 4e2487d 修这个,带一个对应的 regression test (文件形态 [codesesh-custom-title with ts, cc-custom-title without ts] + mtime 强制设到 2099,验证 time_created 不漂移)。

P2 (handlers.ts):handlePatchSession 改成 explicit per-field merge with updated ?? existing,显式保留 project_identity / smart_tags / smart_tags_source_updated_at,响应体也返回 merged 后的而不是 bare SessionHead。看了 SessionHead 类型定义,目前 scanner-enriched 的 optional 字段就是这三个。

测试:

  • core 135/135 全过 (新增 2 个 P1 regression: time_created 跨 3 次 rename 不漂移 + row-0-only-timestamp-source 边界)
  • cli 我新加的 8 个 PATCH 相关 test 都过 (那 3 个 baseline fail 跟之前一样,在 main 上就有,不是这 PR 引入)
  • lint 干净

剩下还是想问下你倾向:就先合后端这版本,还是要等 sidebar 适配的 UI 一起?

好的,等 UI 一起吧。

@nengqi

nengqi commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

@xingkaixin 谢谢方向反馈。SQLite-backed alias + 所有 agent 适用是一个干净的统一抽象,但跟这个 PR 的设计核心冲突——这个 PR 故意写 cc 自己的 custom-title row 是为了换取「和 claude --name 双向同步」(cc 终端里改的名字 codesesh 能看到,反过来也行,共用一行 jsonl 数据,无独立存储)。这个语义在 SQLite 抽象层下没法保留,因为统一表做不到 per-agent 自定义存储位置。

所以这个 PR 不再适合直接演化成你想要的形态,close 掉避免占着 PR 列表 noise。如果之后你实现 SQLite alias 表后还想要 cc 这个 agent 上的双向同步作为 nice-to-have,这里的 setSessionAlias 实现 (jsonl prepend with timestamp preservation + 子序列 collapse + atomic temp+rename) 可以作为参考,branch feat/session-alias 保留不删,commit 4e2487d 是最终状态。

PR #6 / PR #7 是独立的方向 (web UI),跟 alias 无关,继续等你的 review。

@nengqi nengqi closed this May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants