Skip to content

Support clickable internal links to other conversations (controller://)#239

Open
germanescobar wants to merge 2 commits into
mainfrom
issue-227
Open

Support clickable internal links to other conversations (controller://)#239
germanescobar wants to merge 2 commits into
mainfrom
issue-227

Conversation

@germanescobar

Copy link
Copy Markdown
Owner

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 in shared/ so both renderers stay in sync.

Supports two forms:

  • Full: controller://project/<p>/worktree/<w>/session/<s> — navigates directly.
  • Short: controller://session/<s> — resolved server-side to its owning project/worktree first.

Changes

  • shared/conversation-links.tsparseControllerUri() (full + short form, also accepts plural sessions) and CONTROLLER_URI_PATTERN for detecting URIs in text. Pure/dependency-free. Unit-tested in shared/conversation-links.test.ts.
  • client/src/pages/SessionView.tsx — a remarkControllerLinks plugin turns bare controller:// URIs into link nodes (remark-gfm only autolinks http/www/email); a custom urlTransform preserves the scheme (react-markdown's default sanitizer would strip it); MarkdownLink routes controller:// clicks through a new OpenConversationContext.
  • client/src/App.tsxhandleOpenConversation() navigates directly for the full form, resolves the short form first, and toasts on stale/unknown targets.
  • ServerfindSessionLocation() + GET /api/links/sessions/:sessionId map a bare session id to its owning project/worktree (404 when missing/archived). Client wrapper resolveSessionLink().

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).
  • Server typecheck clean; client typecheck clean (only pre-existing import.meta.env errors remain); vite build succeeds.
  • ⚠️ Not yet manually verified against a real conversation containing a controller:// reference (acceptance criterion) — needs a running instance.

🤖 Generated with Claude Code

germanescobar and others added 2 commits June 25, 2026 13:37
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>
@germanescobar

Copy link
Copy Markdown
Owner Author

@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: 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".

Comment thread server/lib/sessions.ts
): Promise<SessionLocation | null> {
const projects = await getProjects();
for (const project of projects) {
const session = await getSession(project.path, sessionId);

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

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.

Support clickable internal links to other conversations (controller://)

1 participant