Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/honest-session-link.md
Original file line number Diff line number Diff line change
@@ -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 <run>` session and open it from claude.ai/code, which is a separate process from an orchestration run. Corrects the overpromise from #212. Closes #221.
32 changes: 23 additions & 9 deletions packages/framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,31 @@ as it looked, without running the agent again. Add `--cwd <dir>` 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 <id>` 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)

Expand Down
31 changes: 15 additions & 16 deletions packages/framework/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<CliOptions, 'sessionLink'>, fake: boolean): string | undefined {
if (opts.sessionLink) return opts.sessionLink
Expand Down Expand Up @@ -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 <url> 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 <url> 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.

Expand Down
13 changes: 11 additions & 2 deletions packages/framework/src/dashboard/page.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -198,8 +201,14 @@ function renderRationale(e) {
function setSessionLink(sessionId, sessionLink) {
const el = $('session-link');
if (sessionLink) {
el.innerHTML = '\\u25b6 <a href="' + esc(sessionLink) + '" target="_blank" rel="noopener">live session</a>';
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 ? ' <code>' + esc(sessionId) + '</code>' : '';
el.innerHTML = '\\u25b6 <a href="' + esc(sessionLink) + '" target="_blank" rel="noopener">' + label + '</a>' + idTail;
el.title = generic ? 'Generic Claude Code entry point, not a live session link' : (sessionId || '');
} else if (sessionId) {
el.innerHTML = 'session <code>' + esc(sessionId) + '</code>';
}
Expand Down
13 changes: 13 additions & 0 deletions packages/framework/src/dashboard/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions packages/framework/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Loading