Skip to content

Two-way terminal <-> dashboard links + resume button - #100

Merged
JayantDevkar merged 3 commits into
mainfrom
terminal-two-way
Sep 18, 2026
Merged

JayantDevkar merged 3 commits into
mainfrom
terminal-two-way

Conversation

@the-non-expert

@the-non-expert the-non-expert commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

What this does

Two related features, both about connecting a terminal to the Karma dashboard in both directions:

  1. Terminal → Dashboard: every session gets a short link (http://localhost:5180/s/<8-char-id>) that opens straight to that session's timeline. A karma shell script is included so you can jump from any terminal to its Karma page with one command, and scripts/example-karma-statusline.py is an optional copy-from reference if you want the link always visible in your status line — documented in SETUP.md, not auto-installed. Claude Code's statusLine is a personal, global setting shared across every project, so Karma never writes to it for you.

  2. Dashboard → Terminal (resume): the "resume" button on a session card, and on the session detail page, now actually does something instead of just copying a command. If the session is still running, it focuses that terminal (same as the existing focus-terminal feature). If the session has ended, it opens a new terminal, cd's into the right folder, and runs claude --resume for you.

The link looks different depending on your terminal

Not every terminal can turn a short label into a clickable link the same way:

  • iTerm2, WezTerm, Kitty, Ghostty: shows a real button-like link, e.g. "See this in Karma" — the actual URL is hidden underneath it.
  • Apple's Terminal.app: shows a label followed by the plain URL. A plain click does nothing. Apple has never documented which modifier opens a link here, and it can vary — on the machine this was tested on, holding Option and clicking once opened it; if that doesn't work, try Cmd+double-click or right-click the URL and choose "Open URL".

This isn't a downgrade I chose. Apple's Terminal.app has never implemented OSC 8, the escape-code standard that lets a terminal turn a short label into a link with a different URL hidden underneath. Terminal.app can only make a URL clickable if the URL itself is visible on screen — there's no setting or software update that changes this. Confirmed in Claude Code's own docs. Switching to iTerm2 or another modern terminal is the only way to get the short button look.

One thing to know before using this

These links only work if Karma is actually running when you click them:

  • Either Karma's dev servers are up (api on 8020, frontend on 5180), or
  • The Karma desktop app's "start at login" is turned on.

If neither is true, clicking a short link (or the resume button) just shows a browser error or a blank page, because there's no server to answer. That's expected for a locally-hosted dashboard, not a bug in this PR.

Testing

  • New API tests for the short-link resolver and the resume-or-focus endpoint, plus unit tests for the new terminal-launch service. All passing (full suite green aside from a pre-existing, unrelated flaky test file).
  • Manually tested end to end on iTerm2 by the author: short link opens the right session's timeline, resume launches a new terminal and reconnects the session.

🤖 Generated with Claude Code

the-non-expert and others added 3 commits August 28, 2026 17:16
Terminal -> Dashboard: every session gets a short link
(http://localhost:5180/s/<8-char-id>) that opens its timeline. Includes
a `karma` shell script and a `GET /sessions/resolve/{short_id}` endpoint
that also checks live-session records, so the link works even seconds
into a brand new session, before its transcript file exists yet.

Dashboard -> Terminal: the "resume" button (session card + session
detail page) now does something instead of just copying a command. If
the session is still running, it focuses that terminal. If it has
ended, it opens a new terminal, cds into the project, and runs
`claude --resume` for you.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's Python Lint job installs unpinned latest ruff (0.16.5), which
wraps multi-line lambda-with-`or` bodies in parens differently than
the repo's pinned dev ruff (0.14.11). Reformatted with 0.16.5 to match
CI; both versions now pass clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nstalled)

Adds a "Karma Link in Your Status Line" section to SETUP.md, next to
the existing Open Terminal Button docs, covering scripts/karma and a
new copy-from example script for the statusline.

This is documentation only, not an installer: Claude Code's statusLine
is a personal, global setting shared across every project, so Karma
never writes to it automatically. scripts/example-karma-statusline.py
is a minimal reference — point statusLine.command at it directly if
you have none, or copy the karma_segment() function into an existing
custom statusline instead of overwriting it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@JayantDevkar JayantDevkar left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approving. The thing I cared most about here — an HTTP endpoint that spawns a terminal — holds up.

Checked and clean

  • Command injection: _build_command shlex.quotes cwd and uuid, then _applescript_str escapes for AppleScript. Round-tripped 7 hostile paths ("; do shell script "…"; ", $(), backticks, embedded newlines) through real osascript — all came back byte-identical, payload stays inside single quotes.
  • The request body carries nothing: uuid is gated by _UUID_RE, cwd comes only from on-disk session data, missing dir → 409.
  • Cross-origin POST → 403 via the same _reject_cross_origin guard as focus-terminal.
  • Short-id resolver: regex allows no glob metacharacters; redirect has a hardcoded /projects/ prefix so no open redirect; /resolve/{short_id} isn't shadowed by /{uuid}.
  • Non-darwin returns launched=False with a readable message, no crash. 24/24 new tests pass locally.

Follow-ups (none blocking, leaving them here rather than inline)

  1. api/routers/sessions.py:1927 — the origin guard trusts all of cors_origins, which includes :3000, :3001, :5173. Same guard as focus-terminal, so not a regression, but the impact went from "raise a window" to "start a process". #101 adds a narrower rc_trusted_origins (:5180 only) for exactly this reason — once that lands, this endpoint should use the same gate.
  2. scripts/karma:40 — under set -euo pipefail, a failing short_id="$(python3 …)" kills the script before the dashboard fallback at :66 (no python3 on PATH → raw "command not found"). Also os.path.getmtime(path) at :54 sits outside the try, so a live-session file deleted between glob and getmtime aborts the whole command. short_id="$(python3 … || true)" plus moving getmtime into the try fixes both.
  3. sessions.py:1935 — dedupe keys on the original uuid's live record. If claude --resume allocates a new session id (Karma's own chain model suggests it can), the original never goes live again and each click spawns another window. Not verified — I didn't run a real resume. A short per-uuid server-side cooldown would close it cheaply.
  4. sessions.py:1885-1896 — an ambiguous 8-char prefix silently resolves to the newest match. Fine today; worth returning a 409/chooser if it ever bites.
  5. frontend/src/routes/s/[short_id]/+server.ts:32 — unguarded res.json() turns a non-JSON 200 into a bare 500 instead of your 502.

Not checked: Windows/Linux at runtime, the iTerm2 branch at runtime, real claude --resume semantics.

@JayantDevkar
JayantDevkar merged commit a80d104 into main Sep 18, 2026
28 checks passed
the-non-expert added a commit that referenced this pull request Sep 19, 2026
…n, no orphan menu

Review feedback on #101, plus the rebase onto main (#99/#100/#102).

- Memoize the transcript scan on (path, st_mtime_ns, st_size). The single-
  session GET is polled ~1s per open tab and, when RC has never been used,
  found no bridge_status line anywhere — so every uncached hit re-read and
  decoded the whole multi-MB chain. A JSONL transcript is append-only, so an
  unchanged transcript is now scanned exactly once.
- Verify a tmux pane still owns the recorded pid before enabling the toggle.
  pid_is_live_claude accepts node/bun/deno, so a recycled pid in a recycled
  pane could have enabled RC on a *different* Claude session. The pane's
  pane_pid must now be that pid or an ancestor of it; unverifiable => refused.
- Require Origin on the toggle, not just "trusted if present". For a
  keystroke-injecting endpoint, no-Origin isn't worth the benefit of the
  doubt: this closes DNS-rebinding and the no-Origin-same-origin-POST corner.
- Drop the "Esc-dismisses" claim in read_remote_control_state — nothing here
  sends Esc. A stale "off" (session launched with --remote-control, no
  bridge_status line) meant the ON click opened the *disconnect* menu and left
  it open in an unfocused terminal, where the user's next Enter picks an item.
  Unconfirmed ON now raises the terminal like the OFF path and says so.

ConversationHeader: #100's ResumeSessionButton supersedes this branch's
live-session hide (it focuses the terminal when live, which is useful), so the
rebase keeps #100's version outright and drops the copy-resume block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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