Skip to content

Commit 6253c19

Browse files
author
杨艺轩
committed
feat(web-session): support Claude Code Router runtime
1 parent 25d5046 commit 6253c19

31 files changed

Lines changed: 750 additions & 61 deletions

api/web_session.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ func (c *webSessionController) registerHTTP(app *fiber.App, group *huma.Group) {
200200
Body struct {
201201
WorktreeID string `json:"worktreeId"`
202202
Agent string `json:"agent"`
203+
ClaudeRuntime string `json:"claudeRuntime"`
203204
Model string `json:"model"`
204205
ReasoningEffort string `json:"reasoningEffort"`
205206
WorkflowMode string `json:"workflowMode"`
@@ -243,6 +244,7 @@ func (c *webSessionController) registerHTTP(app *fiber.App, group *huma.Group) {
243244
ProjectID: input.ProjectID,
244245
WorktreeID: input.Body.WorktreeID,
245246
Agent: websession.Agent(input.Body.Agent),
247+
ClaudeRuntime: websession.ClaudeRuntime(input.Body.ClaudeRuntime),
246248
Model: input.Body.Model,
247249
ReasoningEffort: websession.ReasoningEffort(input.Body.ReasoningEffort),
248250
WorkflowMode: workflowMode,

model/sqlc_gen/schema_sqlite.sql

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ CREATE UNIQUE INDEX "idx_session_type" ON "ai_sessions"("session_id","type");
5454
CREATE INDEX "idx_ai_sessions_deleted_at" ON "ai_sessions"("deleted_at");
5555

5656

57-
CREATE TABLE "web_sessions" ("id" text NOT NULL,"created_at" datetime,"updated_at" datetime,"deleted_at" datetime,"project_id" text NOT NULL,"worktree_id" text,"order_index" real NOT NULL DEFAULT 0,"agent" text NOT NULL,"backend" text NOT NULL DEFAULT "legacy_exec","title" text NOT NULL,"title_auto" boolean NOT NULL DEFAULT false,"model" text,"reasoning_effort" text,"workflow_mode" text NOT NULL DEFAULT "default","permission_level" text NOT NULL DEFAULT "elevated","auto_retry_enabled" boolean NOT NULL DEFAULT false,"auto_retry_scope" text NOT NULL DEFAULT "network_only","auto_retry_preset" text NOT NULL DEFAULT "gentle_stop","permission_mode" text,"cwd" text NOT NULL,"native_session_id" text,"status" text NOT NULL,"assistant_state" text,"has_unread" boolean NOT NULL DEFAULT false,"archived_at" datetime,"activity_at" datetime,"status_updated_at" datetime,"assistant_state_updated_at" datetime,"source_kind" text NOT NULL DEFAULT "codex_app_server","sync_state" text NOT NULL DEFAULT "missing","last_sync_mode" text,"source_created_at" datetime,"source_updated_at" datetime,"last_synced_at" datetime,"thread_path" text,"thread_preview" text,"turn_count" integer NOT NULL DEFAULT 0,"item_count" integer NOT NULL DEFAULT 0,"last_message_at" datetime,"last_event_seq" integer NOT NULL DEFAULT 0,"total_input_tokens" integer NOT NULL DEFAULT 0,"total_cached_input_tokens" integer NOT NULL DEFAULT 0,"total_output_tokens" integer NOT NULL DEFAULT 0,"total_cost" real NOT NULL DEFAULT 0,"last_completed_input_tokens" integer NOT NULL DEFAULT 0,"last_completed_cached_input_tokens" integer NOT NULL DEFAULT 0,"last_completed_output_tokens" integer NOT NULL DEFAULT 0,"latest_turn_input_tokens" integer NOT NULL DEFAULT 0,"latest_turn_cached_input_tokens" integer NOT NULL DEFAULT 0,"latest_turn_output_tokens" integer NOT NULL DEFAULT 0,"latest_turn_usage_updated_at" datetime,"latest_token_count_input_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_cached_input_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_output_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_total_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_updated_at" datetime,"session_context_window_tokens" integer NOT NULL DEFAULT 0,"session_context_window_observed_at" datetime,"context_baseline_input_tokens" integer NOT NULL DEFAULT 0,"context_baseline_cached_input_tokens" integer NOT NULL DEFAULT 0,"context_baseline_output_tokens" integer NOT NULL DEFAULT 0,"last_context_compaction_at" datetime,"auto_retry_attempt" integer NOT NULL DEFAULT 0,"auto_retry_next_at" datetime,"auto_retry_last_error_code" text,"last_error" text,"sync_error" text,PRIMARY KEY ("id"));
57+
CREATE TABLE "web_sessions" ("id" text NOT NULL,"created_at" datetime,"updated_at" datetime,"deleted_at" datetime,"project_id" text NOT NULL,"worktree_id" text,"order_index" real NOT NULL DEFAULT 0,"agent" text NOT NULL,"claude_runtime" text NOT NULL DEFAULT "claude","backend" text NOT NULL DEFAULT "legacy_exec","title" text NOT NULL,"title_auto" boolean NOT NULL DEFAULT false,"model" text,"reasoning_effort" text,"workflow_mode" text NOT NULL DEFAULT "default","permission_level" text NOT NULL DEFAULT "elevated","auto_retry_enabled" boolean NOT NULL DEFAULT false,"auto_retry_scope" text NOT NULL DEFAULT "network_only","auto_retry_preset" text NOT NULL DEFAULT "gentle_stop","permission_mode" text,"cwd" text NOT NULL,"native_session_id" text,"status" text NOT NULL,"assistant_state" text,"has_unread" boolean NOT NULL DEFAULT false,"archived_at" datetime,"activity_at" datetime,"status_updated_at" datetime,"assistant_state_updated_at" datetime,"source_kind" text NOT NULL DEFAULT "codex_app_server","sync_state" text NOT NULL DEFAULT "missing","last_sync_mode" text,"source_created_at" datetime,"source_updated_at" datetime,"last_synced_at" datetime,"thread_path" text,"thread_preview" text,"turn_count" integer NOT NULL DEFAULT 0,"item_count" integer NOT NULL DEFAULT 0,"last_message_at" datetime,"last_event_seq" integer NOT NULL DEFAULT 0,"total_input_tokens" integer NOT NULL DEFAULT 0,"total_cached_input_tokens" integer NOT NULL DEFAULT 0,"total_output_tokens" integer NOT NULL DEFAULT 0,"total_cost" real NOT NULL DEFAULT 0,"last_completed_input_tokens" integer NOT NULL DEFAULT 0,"last_completed_cached_input_tokens" integer NOT NULL DEFAULT 0,"last_completed_output_tokens" integer NOT NULL DEFAULT 0,"latest_turn_input_tokens" integer NOT NULL DEFAULT 0,"latest_turn_cached_input_tokens" integer NOT NULL DEFAULT 0,"latest_turn_output_tokens" integer NOT NULL DEFAULT 0,"latest_turn_usage_updated_at" datetime,"latest_token_count_input_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_cached_input_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_output_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_total_tokens" integer NOT NULL DEFAULT 0,"latest_token_count_updated_at" datetime,"session_context_window_tokens" integer NOT NULL DEFAULT 0,"session_context_window_observed_at" datetime,"context_baseline_input_tokens" integer NOT NULL DEFAULT 0,"context_baseline_cached_input_tokens" integer NOT NULL DEFAULT 0,"context_baseline_output_tokens" integer NOT NULL DEFAULT 0,"last_context_compaction_at" datetime,"auto_retry_attempt" integer NOT NULL DEFAULT 0,"auto_retry_next_at" datetime,"auto_retry_last_error_code" text,"last_error" text,"sync_error" text,PRIMARY KEY ("id"));
5858
CREATE INDEX "idx_web_sessions_source_updated_at" ON "web_sessions"("source_updated_at");
5959
CREATE INDEX "idx_web_sessions_sync_state" ON "web_sessions"("sync_state");
6060
CREATE INDEX "idx_web_sessions_status_updated_at" ON "web_sessions"("status_updated_at");
@@ -102,4 +102,3 @@ CREATE INDEX "idx_task_ai_sessions_ai_session_id" ON "task_ai_sessions"("ai_sess
102102
CREATE UNIQUE INDEX "idx_task_ai_session" ON "task_ai_sessions"("task_id","ai_session_id");
103103
CREATE INDEX "idx_task_ai_sessions_task_id" ON "task_ai_sessions"("task_id");
104104
CREATE INDEX "idx_task_ai_sessions_deleted_at" ON "task_ai_sessions"("deleted_at");
105-

model/tables/web_session.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type WebSessionTable struct {
1616
OrderIndex float64 `gorm:"type:real;not null;default:0;index" json:"orderIndex"`
1717

1818
Agent string `gorm:"type:text;not null;index" json:"agent"`
19+
ClaudeRuntime string `gorm:"type:text;not null;default:claude" json:"claudeRuntime"`
1920
Backend string `gorm:"type:text;not null;default:legacy_exec" json:"-"`
2021
Title string `gorm:"type:text;not null" json:"title"`
2122
TitleAuto bool `gorm:"type:boolean;not null;default:false" json:"-"`

packages/codekanban-cli/src/core.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const VALUE_FLAGS = new Set([
2020
'--prompt',
2121
'--text',
2222
'--agent',
23+
'--claude-runtime',
2324
'--model',
2425
'--profile',
2526
'--sandbox',

packages/codekanban-cli/src/runtime.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ function readFlagValue(argv, index, flag) {
1212
return value;
1313
}
1414

15+
function readRawFlagValue(argv, index, flag) {
16+
const value = argv[index + 1];
17+
if (value == null) {
18+
throw new Error(`${flag} requires a value`);
19+
}
20+
return value;
21+
}
22+
1523
function parseIntegerFlag(value, fieldName) {
1624
if (value == null || value === '') {
1725
return undefined;
@@ -83,6 +91,10 @@ function parseCliArgs(argv) {
8391
flags.agent = readFlagValue(argv, index, token);
8492
index += 1;
8593
break;
94+
case '--claude-runtime':
95+
flags.claudeRuntime = readFlagValue(argv, index, token);
96+
index += 1;
97+
break;
8698
case '--model':
8799
flags.model = readFlagValue(argv, index, token);
88100
index += 1;
@@ -108,7 +120,7 @@ function parseCliArgs(argv) {
108120
index += 1;
109121
break;
110122
case '--extra-arg':
111-
flags.extraArgs.push(readFlagValue(argv, index, token));
123+
flags.extraArgs.push(readRawFlagValue(argv, index, token));
112124
index += 1;
113125
break;
114126
case '--title':
@@ -282,6 +294,7 @@ Common options:
282294
--project-id <id> Project identifier
283295
--path <path> Local project path
284296
--session-id <id> Session identifier
297+
--claude-runtime <rt> Claude launcher for workflow terminal mode: claude or ccr
285298
--help Show this help text
286299
287300
Examples:
@@ -620,6 +633,7 @@ export async function runCli(argv, options = {}) {
620633
if (scope === 'workflow' && action === 'command') {
621634
const result = buildAgentLaunchSpec({
622635
agent: flags.agent,
636+
claudeRuntime: flags.claudeRuntime,
623637
profile: flags.profile,
624638
permissions: buildPermissions(flags),
625639
extraArgs: flags.extraArgs,
@@ -652,6 +666,7 @@ export async function runCli(argv, options = {}) {
652666
worktreeId: flags.worktreeId,
653667
prompt: flags.prompt,
654668
agent: flags.agent,
669+
claudeRuntime: flags.claudeRuntime,
655670
profile: flags.profile,
656671
permissions: buildPermissions(flags),
657672
extraArgs: flags.extraArgs,

packages/codekanban-cli/test/runtime.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,59 @@ test('CLI --help prints usage text', { concurrency: false }, async () => {
7979
assert.equal(result.stderr, '');
8080
});
8181

82+
test('CLI workflow command supports Claude Code Router runtime', { concurrency: false }, async () => {
83+
const result = await runCliCaptured([
84+
'workflow',
85+
'command',
86+
'--agent',
87+
'claude',
88+
'--claude-runtime',
89+
'ccr',
90+
'--extra-arg',
91+
'--model',
92+
'--extra-arg',
93+
'sonnet',
94+
'--prompt',
95+
'Hello',
96+
]);
97+
98+
assert.equal(result.exitCode, 0);
99+
const payload = JSON.parse(result.stdout);
100+
assert.equal(payload.agent, 'claude');
101+
assert.equal(payload.claudeRuntime, 'ccr');
102+
assert.equal(payload.command, 'ccr code --model sonnet');
103+
assert.deepEqual(payload.argv, ['ccr', 'code', '--model', 'sonnet']);
104+
});
105+
106+
test('CLI workflow start forwards Claude runtime to the SDK client', { concurrency: false }, async () => {
107+
const calls = [];
108+
const result = await runCliCaptured([
109+
'workflow',
110+
'start',
111+
'--base-url',
112+
'http://127.0.0.1:3000',
113+
'--project-id',
114+
'p1',
115+
'--agent',
116+
'claude',
117+
'--claude-runtime',
118+
'ccr',
119+
'--prompt',
120+
'Hello',
121+
], {
122+
clientFactory: () => ({
123+
async startWorkflow(input) {
124+
calls.push(input);
125+
return { command: 'ccr code', claudeRuntime: input.claudeRuntime };
126+
},
127+
}),
128+
});
129+
130+
assert.equal(result.exitCode, 0);
131+
assert.equal(calls[0].claudeRuntime, 'ccr');
132+
assert.equal(JSON.parse(result.stdout).claudeRuntime, 'ccr');
133+
});
134+
82135

83136
test('CLI web-session create prints the created session JSON', { concurrency: false }, async () => {
84137
const handlers = new Map([

packages/node-sdk/src/client.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,7 @@ export class CodeKanbanClient {
10031003
body: {
10041004
worktreeId: ensureString(worktree?.id, "worktreeId"),
10051005
agent,
1006+
claudeRuntime: ensureOptionalString(input.claudeRuntime) || "claude",
10061007
model:
10071008
ensureOptionalString(input.model) || defaultWebSessionModel(agent),
10081009
reasoningEffort:
@@ -1878,6 +1879,7 @@ export class CodeKanbanClient {
18781879
worktree,
18791880
terminalSession: terminal,
18801881
agent: launch.agent,
1882+
claudeRuntime: launch.claudeRuntime,
18811883
profile: launch.profile,
18821884
command: launch.command,
18831885
prompt: launch.prompt,

packages/node-sdk/src/command-builder.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-acces
55
export const APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'];
66
export const WORKFLOW_PROFILES = ['plan', 'standard', 'yolo'];
77
export const AGENTS = ['codex', 'claude'];
8+
export const CLAUDE_RUNTIMES = ['claude', 'ccr'];
89

910
const KNOWN_STRUCTURED_FLAGS = new Set([
1011
'-s',
@@ -60,15 +61,18 @@ export function buildAgentLaunchSpec(options = {}) {
6061
const extraArgs = ensureArrayOfStrings(options.extraArgs, 'extraArgs');
6162

6263
if (agent === 'claude') {
64+
const claudeRuntime =
65+
validateEnum(options.claudeRuntime || 'claude', CLAUDE_RUNTIMES, 'claudeRuntime') || 'claude';
6366
if (profile !== 'standard') {
6467
throw new CodeKanbanValidationError('claude only supports the standard profile in v1');
6568
}
6669
if (options.permissions) {
6770
throw new CodeKanbanValidationError('structured permissions are only supported for codex in v1');
6871
}
69-
const argv = ['claude', ...extraArgs];
72+
const argv = claudeRuntime === 'ccr' ? ['ccr', 'code', ...extraArgs] : ['claude', ...extraArgs];
7073
return {
7174
agent,
75+
claudeRuntime,
7276
profile,
7377
argv,
7478
command: toCommandString(argv),

packages/node-sdk/src/web-session-command-channel.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class WebSessionCommandChannel {
119119
pid: ensureString(input.projectId, "projectId"),
120120
wid: ensureOptionalString(input.worktreeId),
121121
ag: ensureString(input.agent, "agent"),
122+
cr: ensureOptionalString(input.claudeRuntime),
122123
md: ensureOptionalString(input.model),
123124
re: ensureOptionalString(input.reasoningEffort),
124125
wm: ensureOptionalString(input.workflowMode),
@@ -230,6 +231,16 @@ export class WebSessionCommandChannel {
230231
});
231232
}
232233

234+
async updateClaudeRuntime(sessionId, input = {}) {
235+
return await this._executeCommand({
236+
operation: "set_cr",
237+
sessionId,
238+
payload: {
239+
cr: ensureString(input.claudeRuntime, "claudeRuntime"),
240+
},
241+
});
242+
}
243+
233244
async updateReasoningEffort(sessionId, input = {}) {
234245
return await this._executeCommand({
235246
operation: "set_re",

packages/node-sdk/src/web-session-shared.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ export function normalizeWebSessionSummaryFromWire(value) {
264264
worktreeId: trimmedString(value?.wid) || null,
265265
orderIndex: numberValue(value?.oi, 0),
266266
agent: trimmedString(value?.ag) || "codex",
267+
claudeRuntime: trimmedString(value?.cr) === "ccr" ? "ccr" : "claude",
267268
title: trimmedString(value?.ttl),
268269
model: trimmedString(value?.md),
269270
reasoningEffort: trimmedString(value?.re) || "default",

0 commit comments

Comments
 (0)