Support clickable internal links to other conversations (controller://)#239
Support clickable internal links to other conversations (controller://)#239germanescobar wants to merge 2 commits into
Conversation
Detect controller:// URIs in transcript output and render them as clickable links that navigate to the referenced conversation in both the Electron and web renderers (same SPA, same in-app navigation). - shared/conversation-links.ts: parseControllerUri() handles the full (project/worktree/session) and short (session-only) forms, plus a detection pattern. Shared so both renderers parse identically. - SessionView: a remark plugin linkifies bare controller:// URIs (gfm only autolinks http/www/email), a custom urlTransform preserves the scheme, and MarkdownLink routes clicks through OpenConversationContext. - App: handleOpenConversation navigates directly for the full form and resolves the short form first; stale targets surface a toast. - Server: findSessionLocation + GET /api/links/sessions/:id resolve a bare session id to its owning project/worktree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Agents routinely emit the link inside backticks (e.g. `controller://...`), which markdown renders as a code span. The original text-node linkifier skipped code, so the most common real output stayed non-clickable. Convert an inlineCode node that is exactly a controller:// URI into a link while keeping the monospace <code> text. Verified end-to-end in the running app: the link is clickable and deep-links to the referenced session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28bd8649a8
ℹ️ 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".
| ): Promise<SessionLocation | null> { | ||
| const projects = await getProjects(); | ||
| for (const project of projects) { | ||
| const session = await getSession(project.path, sessionId); |
There was a problem hiding this comment.
Scan all worktrees for short session links
For sessions created in a secondary worktree, the session state is saved and later listed/fetched using that worktree's path (persistSessionStart calls saveSession(worktreePath, ...), and the list route reads getSessionSummaries(worktree.path)). This resolver only probes project.path, so controller://session/<id> links to any non-main-worktree conversation return 404 even though the session exists; resolve through the project's worktree registry and check each worktree.path instead.
Useful? React with 👍 / 👎.
Closes #227.
Summary
controller://URIs in conversation output are now rendered as clickable links that navigate to the referenced conversation in both the Electron and web renderers (both load the same SPA over http, so a single in-app navigation path serves both). Parsing lives inshared/so both renderers stay in sync.Supports two forms:
controller://project/<p>/worktree/<w>/session/<s>— navigates directly.controller://session/<s>— resolved server-side to its owning project/worktree first.Changes
shared/conversation-links.ts—parseControllerUri()(full + short form, also accepts pluralsessions) andCONTROLLER_URI_PATTERNfor detecting URIs in text. Pure/dependency-free. Unit-tested inshared/conversation-links.test.ts.client/src/pages/SessionView.tsx— aremarkControllerLinksplugin turns barecontroller://URIs into link nodes (remark-gfm only autolinks http/www/email); a customurlTransformpreserves the scheme (react-markdown's default sanitizer would strip it);MarkdownLinkroutescontroller://clicks through a newOpenConversationContext.client/src/App.tsx—handleOpenConversation()navigates directly for the full form, resolves the short form first, and toasts on stale/unknown targets.findSessionLocation()+GET /api/links/sessions/:sessionIdmap a bare session id to its owning project/worktree (404 when missing/archived). Client wrapperresolveSessionLink().Invalid/stale URIs degrade gracefully: unparseable URIs render as plain text; unresolvable ones surface a toast.
OS-level
controller://protocol handling (deep-linking from outside the app) is intentionally out of scope — this covers links inside conversation content.Validation
npm test→ 371 pass / 0 fail (incl. new parser tests).import.meta.enverrors remain);vite buildsucceeds.controller://reference (acceptance criterion) — needs a running instance.🤖 Generated with Claude Code