From 67736386c8aba5c313797a28938ac1767aee6d3b Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sat, 4 Jul 2026 22:43:27 +0300 Subject: [PATCH] feat(framework): label the dashboard session link honestly (#221) Our headless runs are not Remote-Controlled, so the default claude.ai/code link is a generic entry point, not a live per-run session. Label it "Open Claude Code" and keep "live session" only for a real --session-link. Rewrite the README session section to document the user-initiated claude auth login + --remote-control steer path. Corrects #212's overpromise; follow-up to the #214 de-risk. Closes #221 --- .changeset/honest-session-link.md | 9 ++++++ packages/framework/README.md | 32 +++++++++++++------ packages/framework/src/cli.ts | 31 +++++++++--------- packages/framework/src/dashboard/page.ts | 13 ++++++-- .../framework/src/dashboard/server.test.ts | 13 ++++++++ packages/framework/src/events.ts | 8 +++++ 6 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 .changeset/honest-session-link.md diff --git a/.changeset/honest-session-link.md b/.changeset/honest-session-link.md new file mode 100644 index 0000000..09e64eb --- /dev/null +++ b/.changeset/honest-session-link.md @@ -0,0 +1,9 @@ +--- +'@gemstack/framework': patch +--- + +Label the dashboard session link honestly + +Our runs are headless, which is deliberately not Remote-Controlled, so the default `https://claude.ai/code` link is a generic entry point, not a live per-run session. The dashboard now labels it **Open Claude Code** instead of "live session"; the "live session" label is kept only for a real user-supplied `--session-link`. The real session id (the local transcript id, usable with `claude --resume`) is still shown. + +The README's session section is rewritten to be accurate: to steer a session live in the browser you start your own interactive `claude auth login` + `claude --remote-control --name ` session and open it from claude.ai/code, which is a separate process from an orchestration run. Corrects the overpromise from #212. Closes #221. diff --git a/packages/framework/README.md b/packages/framework/README.md index c345b77..2274912 100644 --- a/packages/framework/README.md +++ b/packages/framework/README.md @@ -99,17 +99,31 @@ as it looked, without running the agent again. Add `--cwd ` to resume a run from another workspace. Pass `--no-persist` to skip writing state. We do not persist the agent's transcript; Claude Code owns that. -### Watching the live session +### Watching the session, and steering it live Every run surfaces the current Claude Code **session id** on the dashboard (and in -the CLI narration) as soon as the wrapped agent reports it. To open the live -session in a browser, enable [Remote Control](https://code.claude.com/docs/en/remote-control) -(`claude remote-control` / `--remote-control`; requires a claude.ai subscription -and Claude Code v2.1.51+) and find the session by id/name at -[claude.ai/code](https://claude.ai/code) — the dashboard's session link points -there by default. If you have a direct per-session URL scheme, pass it with -`--session-link "https://.../{sessionId}"`; `{sessionId}` is filled in with the -real Claude session id once it is known. +the CLI narration) as soon as the wrapped agent reports it. That id is the local +transcript id: use it with `claude --resume ` to reopen the transcript. By +default the dashboard also shows an **Open Claude Code** link to +[claude.ai/code](https://claude.ai/code), a generic entry point (not a live link +to this run). + +We drive Claude Code **headless**, which is deliberately not Remote-Controlled, so +there is no per-session URL to deep-link into (`--remote-control` is silently +ignored in headless mode, emits no URL, and +[Remote Control](https://code.claude.com/docs/en/remote-control) refuses +automation tokens anyway). To actually steer a session live in the browser, start +your **own** interactive session and open it from claude.ai/code: + +```bash +claude auth login # full-scope login (Remote Control needs it) +claude --remote-control --name my-run # interactive; find "my-run" at claude.ai/code +``` + +That is a separate process from an orchestration run, not the same agent. If you do +have a real per-session URL scheme, point the dashboard at it with +`--session-link "https://.../{sessionId}"` (`{sessionId}` fills in with the real +Claude session id once known), and the dashboard labels it a **live session**. ## Extensions (#190) diff --git a/packages/framework/src/cli.ts b/packages/framework/src/cli.ts index bf0e348..581bd7e 100644 --- a/packages/framework/src/cli.ts +++ b/packages/framework/src/cli.ts @@ -4,7 +4,7 @@ import { cloudflareTarget, dokployTarget, nodeLedgerFs, saveLedger, type DeployT import { ClaudeCodeDriver, type ClaudeCodeDriverOptions, type Driver, type PermissionMode } from './driver/index.js' import { hostExecutor } from './host-exec.js' import { startDashboard, type Dashboard } from './dashboard/index.js' -import { formatFrameworkEvent, type FrameworkEvent } from './events.js' +import { formatFrameworkEvent, CLAUDE_CODE_SESSION_LINK, type FrameworkEvent } from './events.js' import { runFramework, type DeployDecision, @@ -18,19 +18,18 @@ import { preflight } from './preflight.js' import { RunStore } from './store/index.js' /** - * The claude.ai/code session list — the default link shown for a live run. It is - * the page where a Claude Code session appears *when Remote Control is enabled* - * (`claude remote-control` / `--remote-control`, a claude.ai subscription, Claude - * Code v2.1.51+; https://code.claude.com/docs/en/remote-control). We drive Claude - * Code headless, so there is no per-session deep link to construct — pass - * `--session-link "...{sessionId}..."` if you have a real one. + * The default link shown for a live run: the generic Claude Code entry point, + * surfaced as "Open Claude Code" (not a per-run live session). We drive Claude + * Code headless, which is not Remote-Controlled, so there is no per-session deep + * link to construct (#214). Pass `--session-link "...{sessionId}..."` if you + * wire up a real one. */ -export const CLAUDE_CODE_SESSION_LIST = 'https://claude.ai/code' +export const CLAUDE_CODE_SESSION_LIST = CLAUDE_CODE_SESSION_LINK /** * The session link to show for a run: the user's `--session-link` if given, else - * the claude.ai/code list for a live run (nothing for `--fake`, which has no real - * session). Pure, so the default is unit-testable without a live run. + * the generic Claude Code entry point for a live run (nothing for `--fake`, which + * has no real session). Pure, so the default is unit-testable without a live run. */ export function chooseSessionLink(opts: Pick, fake: boolean): string | undefined { if (opts.sessionLink) return opts.sessionLink @@ -86,12 +85,12 @@ Options: (read-only replay; no new agent run). Survives a restart. --no-persist Do not write the orchestration state to .framework/. --skip-preflight Skip the prerequisite checks before a live run. - --session-link Link to the live agent session (shown on the dashboard). - Defaults to https://claude.ai/code for a live run, where - the Claude Code session appears when Remote Control is on - (see code.claude.com/docs/en/remote-control). Pass your own - URL, using {sessionId} to template in the real Claude - session id, e.g. "https://example.com/s/{sessionId}". + --session-link A real per-session link to the live agent session, shown + on the dashboard. Our runs are headless (not Remote- + Controlled), so by default the dashboard only offers the + generic "Open Claude Code" entry point. Pass your own URL, + using {sessionId} to template in the real Claude session + id, e.g. "https://example.com/s/{sessionId}". -h, --help Show this help. -v, --version Print the version. diff --git a/packages/framework/src/dashboard/page.ts b/packages/framework/src/dashboard/page.ts index da16a80..bfadb8a 100644 --- a/packages/framework/src/dashboard/page.ts +++ b/packages/framework/src/dashboard/page.ts @@ -1,3 +1,5 @@ +import { CLAUDE_CODE_SESSION_LINK } from '../events.js' + /** * The single self-contained dashboard page: HTML + inline CSS + inline JS, no * assets, no build step. The client opens an `EventSource` to `/events` and @@ -117,6 +119,7 @@ function clientScript(stoppable: boolean): string { // Runs in the browser. Keep it dependency-free. return ` const STOPPABLE = ${stoppable ? 'true' : 'false'}; +const GENERIC_SESSION_LINK = ${JSON.stringify(CLAUDE_CODE_SESSION_LINK)}; let ended = false; const $ = id => document.getElementById(id); const log = line => { @@ -198,8 +201,14 @@ function renderRationale(e) { function setSessionLink(sessionId, sessionLink) { const el = $('session-link'); if (sessionLink) { - el.innerHTML = '\\u25b6 live session'; - if (sessionId) el.title = sessionId; + // The generic default is not a live per-run session (our runs are headless, + // not Remote-Controlled), so label it honestly. A real --session-link keeps + // "live session". Show the session id alongside when we know it. + const generic = sessionLink === GENERIC_SESSION_LINK; + const label = generic ? 'Open Claude Code' : 'live session'; + const idTail = sessionId ? ' ' + esc(sessionId) + '' : ''; + el.innerHTML = '\\u25b6 ' + label + '' + idTail; + el.title = generic ? 'Generic Claude Code entry point, not a live session link' : (sessionId || ''); } else if (sessionId) { el.innerHTML = 'session ' + esc(sessionId) + ''; } diff --git a/packages/framework/src/dashboard/server.test.ts b/packages/framework/src/dashboard/server.test.ts index f163ae5..e74a065 100644 --- a/packages/framework/src/dashboard/server.test.ts +++ b/packages/framework/src/dashboard/server.test.ts @@ -78,6 +78,19 @@ test('dashboard replays buffered events and streams them over SSE', async () => } }) +test('page labels the generic default "Open Claude Code", not a live session (#214)', async () => { + const dash = await startDashboard({ port: 0 }) + try { + const { body } = await fetchText(dash.url + '/') + // Honest label: the generic entry point is not a per-run live session. + assert.match(body, /GENERIC_SESSION_LINK = "https:\/\/claude\.ai\/code"/) + assert.match(body, /'Open Claude Code'/) + assert.match(body, /'live session'/) // still used for a real --session-link + } finally { + await dash.close() + } +}) + test('dashboard returns 404 for unknown paths', async () => { const dash = await startDashboard({ port: 0 }) try { diff --git a/packages/framework/src/events.ts b/packages/framework/src/events.ts index 12107ac..da558bc 100644 --- a/packages/framework/src/events.ts +++ b/packages/framework/src/events.ts @@ -119,3 +119,11 @@ export function hasSessionIdPlaceholder(template: string): boolean { export function resolveSessionLink(template: string, sessionId: string): string { return template.split(SESSION_ID_PLACEHOLDER).join(sessionId) } + +/** + * The generic Claude Code entry point. It is NOT a per-run live session: a + * headless run is not Remote-Controlled, so there is no deep link to build (see + * #214). We surface this only as an "Open Claude Code" affordance; a real live + * link comes from an explicit `--session-link`. + */ +export const CLAUDE_CODE_SESSION_LINK = 'https://claude.ai/code'