@myagentroam/node 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-agent-sdk.js +1 -1
- package/dist/codex-app-server.js +1 -1
- package/dist/connector.d.ts +3 -0
- package/dist/connector.js +304 -62
- package/dist/connector.js.map +1 -1
- package/dist/native-session-history.js +6 -1
- package/dist/native-session-history.js.map +1 -1
- package/dist/runner-command-engine.d.ts +9 -0
- package/dist/runner-command-engine.js +24 -0
- package/dist/runner-command-engine.js.map +1 -1
- package/dist/runner-profiles.js +12 -0
- package/dist/runner-profiles.js.map +1 -1
- package/dist/workspace.d.ts +9 -2
- package/dist/workspace.js +41 -0
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
package/dist/claude-agent-sdk.js
CHANGED
|
@@ -216,7 +216,7 @@ function claudePermissionMode(access) {
|
|
|
216
216
|
function channelMcpServer(input) {
|
|
217
217
|
return createSdkMcpServer({
|
|
218
218
|
name: 'myagentroam',
|
|
219
|
-
version: '0.1.
|
|
219
|
+
version: '0.1.3',
|
|
220
220
|
instructions: 'Use the myagentroam_channel_reply tool only to send a concise reply to the Admin Channel.',
|
|
221
221
|
alwaysLoad: true,
|
|
222
222
|
tools: [
|
package/dist/codex-app-server.js
CHANGED
|
@@ -98,7 +98,7 @@ export class CodexAppServerClient {
|
|
|
98
98
|
this.failAll(new Error('CODEX_APP_SERVER_EXITED'));
|
|
99
99
|
});
|
|
100
100
|
await this.request('initialize', {
|
|
101
|
-
clientInfo: { name: 'myagentroam', title: 'MyAgentRoam', version: '0.1.
|
|
101
|
+
clientInfo: { name: 'myagentroam', title: 'MyAgentRoam', version: '0.1.3' },
|
|
102
102
|
capabilities: { experimentalApi: true }
|
|
103
103
|
});
|
|
104
104
|
this.notify('initialized', {});
|
package/dist/connector.d.ts
CHANGED
|
@@ -98,6 +98,7 @@ export declare class NodeConnector {
|
|
|
98
98
|
/** Bounded Runner diagnostic paired with a fixed external resume reason code. */
|
|
99
99
|
private readonly externalResumeFailureDetails;
|
|
100
100
|
private readonly claudeApprovals;
|
|
101
|
+
private readonly claudeUserInputs;
|
|
101
102
|
private readonly commandStates;
|
|
102
103
|
private readonly runnerUsageReader;
|
|
103
104
|
private codexFastEnabled;
|
|
@@ -262,6 +263,7 @@ export declare class NodeConnector {
|
|
|
262
263
|
private deliverClaudeChannel;
|
|
263
264
|
private cancelCodexTurn;
|
|
264
265
|
private respondCodexUserInput;
|
|
266
|
+
private respondUserInput;
|
|
265
267
|
/** Returns true when cancellation already made this Run terminal. */
|
|
266
268
|
private completeCancelledRun;
|
|
267
269
|
private presentSession;
|
|
@@ -315,6 +317,7 @@ export declare class NodeConnector {
|
|
|
315
317
|
private workbenchManifest;
|
|
316
318
|
private resumeDelivery;
|
|
317
319
|
private requestClaudeApproval;
|
|
320
|
+
private requestClaudeUserInput;
|
|
318
321
|
/**
|
|
319
322
|
* An approval is an individual Runner callback. Keep its exact payload in
|
|
320
323
|
* the process-local snapshot and emit a Run event so an already open
|
package/dist/connector.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadNodeConfig, nodeDatabasePath, saveNodeConfig } from './config.js';
|
|
|
9
9
|
import { NodeMetrics, nodeLog } from './operational.js';
|
|
10
10
|
import { assertLocalSqlitePath } from './storage.js';
|
|
11
11
|
import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
|
|
12
|
-
import { inspectWorkspace, isWorkspaceChangeVisible, listWorkspaceDirectories, listWorkspaceFiles, preflightWorkspaceUpload, readCurrentChangeDiff, readCurrentChangesSummary, readGitSummary, readRestrictedDiff, readAllowedTextFile, readWorkspaceTextFile, restoreCurrentChange, WorkspaceFileIndex, workspaceUploadTemporaryName } from './workspace.js';
|
|
12
|
+
import { inspectWorkspace, isWorkspaceChangeVisible, listWorkspaceDirectories, listWorkspaceFiles, preflightWorkspaceUpload, listInitializedSubmodulePaths, readCurrentChangeDiff, readCurrentChangesSummary, readGitSummary, readRestrictedDiff, readAllowedTextFile, readWorkspaceTextFile, resolveGitRepository, restoreCurrentChange, WorkspaceFileIndex, workspaceUploadTemporaryName } from './workspace.js';
|
|
13
13
|
import { ReliableWorkbenchEventBuffer } from './event-buffer.js';
|
|
14
14
|
import { CodexAppServerClient, codexSessionControl } from './codex-app-server.js';
|
|
15
15
|
import { ClaudeAgentSdkAdapter, denyPermission } from './claude-agent-sdk.js';
|
|
@@ -19,7 +19,7 @@ import { TerminalManager } from './terminal.js';
|
|
|
19
19
|
import { discoverAllNativeSessions, discoverNativeSessions, readCodexNativeContextUsage, readCodexNativeTaskActivity, readNativeSession, removeNativeSession } from './native-session-history.js';
|
|
20
20
|
import { declaredRunnerProfiles, supportsRunnerConfiguration } from './runner-profiles.js';
|
|
21
21
|
import { NodeRuntimeState } from './runtime-state.js';
|
|
22
|
-
import { CommandStateStore, parseCommandInvocation } from './runner-command-engine.js';
|
|
22
|
+
import { CommandStateStore, parseCommandInvocation, withClaudePlanTag, stripClaudePlanTag } from './runner-command-engine.js';
|
|
23
23
|
import { RunnerUsageReader } from './runner-usage.js';
|
|
24
24
|
const HEARTBEAT_MS = 15_000;
|
|
25
25
|
const WORKSPACE_FILE_INDEX_CACHE_LIMIT = 8;
|
|
@@ -31,6 +31,7 @@ const CAPABILITY_REFRESH_MS = 60_000;
|
|
|
31
31
|
const SESSION_WATCH_INTERVAL_MS = 2_000;
|
|
32
32
|
const SESSION_WATCH_TTL_MS = 45_000;
|
|
33
33
|
const SESSION_WATCH_MAX_FAILURES = 3;
|
|
34
|
+
const CLAUDE_CONTEXT_CACHE_MS = 30_000;
|
|
34
35
|
const SESSION_WATCH_INITIAL_TURN_LIMIT = 30;
|
|
35
36
|
const RETRY_MIN_MS = 500;
|
|
36
37
|
const RETRY_MAX_MS = 30_000;
|
|
@@ -159,6 +160,7 @@ export class NodeConnector {
|
|
|
159
160
|
/** Bounded Runner diagnostic paired with a fixed external resume reason code. */
|
|
160
161
|
externalResumeFailureDetails = new Map();
|
|
161
162
|
claudeApprovals = new Map();
|
|
163
|
+
claudeUserInputs = new Map();
|
|
162
164
|
commandStates = new CommandStateStore();
|
|
163
165
|
runnerUsageReader;
|
|
164
166
|
codexFastEnabled = false;
|
|
@@ -235,6 +237,11 @@ export class NodeConnector {
|
|
|
235
237
|
approval.resolve('REJECTED');
|
|
236
238
|
}
|
|
237
239
|
this.claudeApprovals.clear();
|
|
240
|
+
for (const pending of this.claudeUserInputs.values()) {
|
|
241
|
+
clearTimeout(pending.timer);
|
|
242
|
+
pending.reject('NODE_STOPPED');
|
|
243
|
+
}
|
|
244
|
+
this.claudeUserInputs.clear();
|
|
238
245
|
for (const timer of this.queuePauseWindows.values())
|
|
239
246
|
clearTimeout(timer);
|
|
240
247
|
this.queuePauseWindows.clear();
|
|
@@ -1081,16 +1088,20 @@ export class NodeConnector {
|
|
|
1081
1088
|
return this.currentChangesForWorkspace(workspace);
|
|
1082
1089
|
}
|
|
1083
1090
|
if (operation === 'workspace.change.diff') {
|
|
1084
|
-
if (typeof input.workspaceId !== 'string' ||
|
|
1091
|
+
if (typeof input.workspaceId !== 'string' ||
|
|
1092
|
+
typeof input.path !== 'string' ||
|
|
1093
|
+
(input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string'))
|
|
1085
1094
|
throw new Error('GIT_DIFF_INVALID');
|
|
1086
1095
|
const workspace = this.requireWorkspace(input.workspaceId);
|
|
1087
1096
|
if (workspace.kind !== 'GIT_WORKSPACE')
|
|
1088
1097
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
1089
|
-
|
|
1098
|
+
const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
|
|
1099
|
+
return { diff: await readCurrentChangeDiff(repository, input.path) };
|
|
1090
1100
|
}
|
|
1091
1101
|
if (operation === 'workspace.change.restore') {
|
|
1092
1102
|
if (typeof input.workspaceId !== 'string' ||
|
|
1093
1103
|
typeof input.path !== 'string' ||
|
|
1104
|
+
(input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string') ||
|
|
1094
1105
|
(input.state !== 'STAGED' && input.state !== 'UNSTAGED' && input.state !== 'UNTRACKED'))
|
|
1095
1106
|
throw new Error('GIT_CHANGE_RESTORE_UNAVAILABLE');
|
|
1096
1107
|
const workspace = this.requireWorkspace(input.workspaceId);
|
|
@@ -1098,7 +1109,8 @@ export class NodeConnector {
|
|
|
1098
1109
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
1099
1110
|
if (this.runtime.hasActiveWorkspaceLease(workspace.id))
|
|
1100
1111
|
throw new Error('WORKSPACE_BUSY');
|
|
1101
|
-
await
|
|
1112
|
+
const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
|
|
1113
|
+
await restoreCurrentChange(repository, input.path, input.state);
|
|
1102
1114
|
// A read started before the write must not satisfy the explicit post-write refresh.
|
|
1103
1115
|
this.invalidateWorkspaceWatchTopics(workspace.id);
|
|
1104
1116
|
return { restored: true };
|
|
@@ -1563,12 +1575,16 @@ export class NodeConnector {
|
|
|
1563
1575
|
const runnerInput = withNonImageAttachmentPrompt(input.content, attachmentPaths);
|
|
1564
1576
|
this.runtime.updateQueuedMessage(created.run.id, runnerInput);
|
|
1565
1577
|
const commandStates = this.commandStates.list(session.id);
|
|
1578
|
+
const planActive = commandStates.some((state) => state.commandId === 'plan');
|
|
1566
1579
|
this.queuedRunStarts.set(created.run.id, {
|
|
1567
1580
|
runId: created.run.id,
|
|
1568
1581
|
sessionId: created.run.sessionId,
|
|
1569
1582
|
workspaceId: created.run.workspaceId,
|
|
1570
1583
|
runner: created.run.runner,
|
|
1571
|
-
|
|
1584
|
+
// Claude 标签式 Plan Mode 只包装发往 Runner 的输入,用户气泡保持原文。
|
|
1585
|
+
input: created.run.runner === 'claude-code' && planActive
|
|
1586
|
+
? withClaudePlanTag(runnerInput)
|
|
1587
|
+
: runnerInput,
|
|
1572
1588
|
attachments: runnerImageAttachments,
|
|
1573
1589
|
attachmentPaths,
|
|
1574
1590
|
cwd: session.cwd,
|
|
@@ -1577,9 +1593,7 @@ export class NodeConnector {
|
|
|
1577
1593
|
effort: session.effort,
|
|
1578
1594
|
// Plan 只切换 Codex 协作模式,不得覆盖会话已选权限。
|
|
1579
1595
|
access: session.access,
|
|
1580
|
-
collaborationMode:
|
|
1581
|
-
? 'plan'
|
|
1582
|
-
: 'default',
|
|
1596
|
+
collaborationMode: planActive ? 'plan' : 'default',
|
|
1583
1597
|
...(this.codexFastEnabled ? { serviceTier: 'fast' } : {})
|
|
1584
1598
|
});
|
|
1585
1599
|
this.runtime.resumeSessionQueue(session.id);
|
|
@@ -1651,7 +1665,12 @@ export class NodeConnector {
|
|
|
1651
1665
|
const runnerInput = withNonImageAttachmentPrompt(input.content.trim(), pending?.attachmentPaths ?? []);
|
|
1652
1666
|
this.runtime.updateQueuedMessage(input.runId, runnerInput);
|
|
1653
1667
|
if (pending !== undefined)
|
|
1654
|
-
this.queuedRunStarts.set(input.runId, {
|
|
1668
|
+
this.queuedRunStarts.set(input.runId, {
|
|
1669
|
+
...pending,
|
|
1670
|
+
input: pending.runner === 'claude-code' && pending.collaborationMode === 'plan'
|
|
1671
|
+
? withClaudePlanTag(runnerInput)
|
|
1672
|
+
: runnerInput
|
|
1673
|
+
});
|
|
1655
1674
|
this.emitWorkspaceQueue(run.workspaceId, run.sessionId);
|
|
1656
1675
|
return { queue: this.presentWorkspaceQueue(run.workspaceId, run.sessionId) };
|
|
1657
1676
|
}
|
|
@@ -1750,7 +1769,7 @@ export class NodeConnector {
|
|
|
1750
1769
|
typeof input.requestId !== 'string' ||
|
|
1751
1770
|
!isPlainRecord(input.answers))
|
|
1752
1771
|
throw new Error('USER_INPUT_INVALID');
|
|
1753
|
-
this.
|
|
1772
|
+
this.respondUserInput(input.runId, input.requestId, input.answers);
|
|
1754
1773
|
return { accepted: true };
|
|
1755
1774
|
}
|
|
1756
1775
|
if (operation === 'session.interrupt') {
|
|
@@ -2088,8 +2107,6 @@ export class NodeConnector {
|
|
|
2088
2107
|
typeof input.overwrite !== 'boolean' ||
|
|
2089
2108
|
(composerAttachment !== undefined && input.directory !== undefined))
|
|
2090
2109
|
throw new Error('UPLOAD_INVALID');
|
|
2091
|
-
if (composerAttachment === undefined && this.runtime.hasActiveWorkspaceLease(workspace.id))
|
|
2092
|
-
throw new Error('WORKSPACE_BUSY');
|
|
2093
2110
|
if (composerAttachment !== undefined) {
|
|
2094
2111
|
if (!validComposerAttachmentName(input.name) ||
|
|
2095
2112
|
input.size > MAX_COMPOSER_ATTACHMENT_BYTES ||
|
|
@@ -2300,24 +2317,56 @@ export class NodeConnector {
|
|
|
2300
2317
|
return existing;
|
|
2301
2318
|
const version = this.workspaceChangesVersions.get(workspace.id) ?? 0;
|
|
2302
2319
|
const request = this.workspaceChangesLimiter.run(async () => {
|
|
2303
|
-
const
|
|
2304
|
-
const
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
.map((
|
|
2320
|
+
const repositoryPaths = ['', ...(await listInitializedSubmodulePaths(workspace.path))];
|
|
2321
|
+
const readRepository = async (repositoryPath) => {
|
|
2322
|
+
const repository = repositoryPath === ''
|
|
2323
|
+
? workspace.path
|
|
2324
|
+
: await resolveGitRepository(workspace.path, repositoryPath);
|
|
2325
|
+
const summary = await readCurrentChangesSummary(repository);
|
|
2326
|
+
const visibleChanges = (await Promise.all(summary.changes.map(async (change) => ({
|
|
2327
|
+
change,
|
|
2328
|
+
visible: await isWorkspaceChangeVisible(repository, change.path)
|
|
2329
|
+
}))))
|
|
2330
|
+
.filter(({ visible }) => visible)
|
|
2331
|
+
.map(({ change }) => change);
|
|
2332
|
+
return {
|
|
2333
|
+
repositoryPath,
|
|
2334
|
+
repositoryName: repositoryPath === '' ? '根仓库' : repositoryPath,
|
|
2335
|
+
branch: summary.branch,
|
|
2336
|
+
changes: visibleChanges
|
|
2337
|
+
};
|
|
2338
|
+
};
|
|
2339
|
+
const root = await readRepository('');
|
|
2340
|
+
const submoduleRepositories = (await Promise.all(repositoryPaths.slice(1).map(async (repositoryPath) => {
|
|
2341
|
+
try {
|
|
2342
|
+
return await readRepository(repositoryPath);
|
|
2343
|
+
}
|
|
2344
|
+
catch {
|
|
2345
|
+
return undefined;
|
|
2346
|
+
}
|
|
2347
|
+
}))).filter((repository) => repository !== undefined);
|
|
2348
|
+
const repositories = [root, ...submoduleRepositories];
|
|
2310
2349
|
const value = {
|
|
2311
|
-
branch:
|
|
2312
|
-
changes:
|
|
2350
|
+
branch: root.branch,
|
|
2351
|
+
changes: root.changes.map((change) => ({
|
|
2313
2352
|
...change,
|
|
2314
2353
|
additions: null,
|
|
2315
2354
|
deletions: null,
|
|
2316
|
-
// Current status does not read file bodies. Detailed binary/diff data
|
|
2317
|
-
// remains an explicit on-demand Git request rather than a background
|
|
2318
|
-
// scan of the Workspace.
|
|
2319
2355
|
binary: false,
|
|
2320
2356
|
diffAvailable: change.state !== 'UNTRACKED'
|
|
2357
|
+
})),
|
|
2358
|
+
repositories: repositories.map((repository) => ({
|
|
2359
|
+
...repository,
|
|
2360
|
+
changes: repository.changes.map((change) => ({
|
|
2361
|
+
...change,
|
|
2362
|
+
additions: null,
|
|
2363
|
+
deletions: null,
|
|
2364
|
+
// Current status does not read file bodies. Detailed binary/diff data
|
|
2365
|
+
// remains an explicit on-demand Git request rather than a background
|
|
2366
|
+
// scan of the Workspace.
|
|
2367
|
+
binary: false,
|
|
2368
|
+
diffAvailable: change.state !== 'UNTRACKED'
|
|
2369
|
+
}))
|
|
2321
2370
|
}))
|
|
2322
2371
|
};
|
|
2323
2372
|
if ((this.workspaceChangesVersions.get(workspace.id) ?? 0) === version)
|
|
@@ -3313,6 +3362,8 @@ export class NodeConnector {
|
|
|
3313
3362
|
this.emitRunEvent(runId, 'run.rejected', { code: 'EXTERNAL_THREAD_OBSERVE_ONLY' }, 'FAILED');
|
|
3314
3363
|
return;
|
|
3315
3364
|
}
|
|
3365
|
+
if (typeof payload.externalSessionId === 'string')
|
|
3366
|
+
this.completedClaudeContextReads.delete(`claude:${cwd}:${payload.externalSessionId}`);
|
|
3316
3367
|
this.activeRuns.add(runId);
|
|
3317
3368
|
void (async () => {
|
|
3318
3369
|
try {
|
|
@@ -3452,7 +3503,10 @@ export class NodeConnector {
|
|
|
3452
3503
|
dataBase64: attachment.dataBase64
|
|
3453
3504
|
}))
|
|
3454
3505
|
}),
|
|
3455
|
-
onPermission: (toolName, toolInput) =>
|
|
3506
|
+
onPermission: (toolName, toolInput, options) => toolName === 'AskUserQuestion'
|
|
3507
|
+
? (toolKinds.set(options.toolUseID, 'user_input_request'),
|
|
3508
|
+
this.requestClaudeUserInput(runId, toolInput, options.toolUseID))
|
|
3509
|
+
: this.requestClaudeApproval(runId, toolName, toolInput),
|
|
3456
3510
|
onChannelReply: (content) => this.emitRunEvent(runId, 'channel.reply', { content }),
|
|
3457
3511
|
onMessage: (message) => {
|
|
3458
3512
|
const record = message;
|
|
@@ -3475,7 +3529,15 @@ export class NodeConnector {
|
|
|
3475
3529
|
}
|
|
3476
3530
|
if (record.type === 'assistant') {
|
|
3477
3531
|
const text = extractClaudeText(record);
|
|
3478
|
-
|
|
3532
|
+
const planText = claudePlanText(text);
|
|
3533
|
+
if (planText !== undefined)
|
|
3534
|
+
this.emitConversationItem(runId, {
|
|
3535
|
+
itemId: boundedConversationItemId('claude-plan', typeof record.uuid === 'string' ? record.uuid : `${runId}:plan`),
|
|
3536
|
+
kind: 'plan',
|
|
3537
|
+
status: 'COMPLETED',
|
|
3538
|
+
payload: { explanation: compactRunnerText(planText, 20_000), steps: [] }
|
|
3539
|
+
});
|
|
3540
|
+
else if (text.length > 0)
|
|
3479
3541
|
this.emitRunEvent(runId, 'text.delta', { text });
|
|
3480
3542
|
for (const [index, text] of extractClaudeThinkingSummaries(record).entries()) {
|
|
3481
3543
|
this.emitConversationItem(runId, {
|
|
@@ -3486,6 +3548,10 @@ export class NodeConnector {
|
|
|
3486
3548
|
});
|
|
3487
3549
|
}
|
|
3488
3550
|
for (const tool of extractClaudeToolUses(record)) {
|
|
3551
|
+
if (tool.name === 'AskUserQuestion') {
|
|
3552
|
+
toolKinds.set(tool.id, 'user_input_request');
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3489
3555
|
const itemId = boundedConversationItemId('claude-tool', tool.id);
|
|
3490
3556
|
const kind = isClaudeCommandTool(tool.name) ? 'command_execution' : 'tool_call';
|
|
3491
3557
|
toolKinds.set(tool.id, kind);
|
|
@@ -3536,6 +3602,8 @@ export class NodeConnector {
|
|
|
3536
3602
|
if (record.type === 'user') {
|
|
3537
3603
|
for (const result of extractClaudeToolResults(record)) {
|
|
3538
3604
|
const kind = toolKinds.get(result.toolUseId) ?? 'tool_call';
|
|
3605
|
+
if (kind === 'user_input_request')
|
|
3606
|
+
continue;
|
|
3539
3607
|
this.emitConversationItem(runId, {
|
|
3540
3608
|
itemId: boundedConversationItemId('claude-tool', result.toolUseId),
|
|
3541
3609
|
kind,
|
|
@@ -3684,6 +3752,41 @@ export class NodeConnector {
|
|
|
3684
3752
|
merge: true
|
|
3685
3753
|
});
|
|
3686
3754
|
}
|
|
3755
|
+
respondUserInput(runId, requestId, rawAnswers) {
|
|
3756
|
+
if (this.codexUserInputs.has(requestId)) {
|
|
3757
|
+
this.respondCodexUserInput(runId, requestId, rawAnswers);
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
const pending = this.claudeUserInputs.get(requestId);
|
|
3761
|
+
if (pending === undefined || pending.runId !== runId)
|
|
3762
|
+
throw new Error('USER_INPUT_NOT_FOUND');
|
|
3763
|
+
const answers = {};
|
|
3764
|
+
for (const question of pending.questions) {
|
|
3765
|
+
const values = rawAnswers[question.id];
|
|
3766
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > 8)
|
|
3767
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3768
|
+
const normalized = values.map((value) => {
|
|
3769
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 500)
|
|
3770
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3771
|
+
if (!question.allowOther && !question.options.some((option) => option.value === value))
|
|
3772
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3773
|
+
return value;
|
|
3774
|
+
});
|
|
3775
|
+
if (!question.multiSelect && normalized.length !== 1)
|
|
3776
|
+
throw new Error('USER_INPUT_INVALID');
|
|
3777
|
+
answers[question.prompt] = normalized.join(', ');
|
|
3778
|
+
}
|
|
3779
|
+
clearTimeout(pending.timer);
|
|
3780
|
+
this.claudeUserInputs.delete(requestId);
|
|
3781
|
+
pending.resolve(answers);
|
|
3782
|
+
this.emitConversationItem(runId, {
|
|
3783
|
+
itemId: boundedConversationItemId('claude-input', requestId),
|
|
3784
|
+
kind: 'user_input_request',
|
|
3785
|
+
status: 'COMPLETED',
|
|
3786
|
+
payload: { requestId, questions: pending.questions },
|
|
3787
|
+
merge: true
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3687
3790
|
/** Returns true when cancellation already made this Run terminal. */
|
|
3688
3791
|
completeCancelledRun(runId, cwd) {
|
|
3689
3792
|
const run = this.runtime.getRun(runId);
|
|
@@ -3980,7 +4083,10 @@ export class NodeConnector {
|
|
|
3980
4083
|
// Context detail is optional Runner metadata. Cache the miss briefly to
|
|
3981
4084
|
// avoid spawning one Claude process per browser polling tick.
|
|
3982
4085
|
}
|
|
3983
|
-
this.completedClaudeContextReads.set(key, {
|
|
4086
|
+
this.completedClaudeContextReads.set(key, {
|
|
4087
|
+
context,
|
|
4088
|
+
expiresAt: Date.now() + CLAUDE_CONTEXT_CACHE_MS
|
|
4089
|
+
});
|
|
3984
4090
|
this.emitWorkbenchEvent('session', {
|
|
3985
4091
|
sessionId: session.id,
|
|
3986
4092
|
workspaceId: session.workspaceId,
|
|
@@ -3991,9 +4097,27 @@ export class NodeConnector {
|
|
|
3991
4097
|
/** Executes only Node-declared interactions; the browser never supplies behavior. */
|
|
3992
4098
|
async executeRunnerCommand(session, command, input) {
|
|
3993
4099
|
if (session.runner === 'claude-code') {
|
|
3994
|
-
if (command.id
|
|
4100
|
+
if (command.id === 'compact')
|
|
4101
|
+
return this.startClaudeCompaction(session, input);
|
|
4102
|
+
if (command.id !== 'plan')
|
|
3995
4103
|
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
3996
|
-
|
|
4104
|
+
// 标签式 Plan Mode 与 SDK 原生 plan 权限不叠加。
|
|
4105
|
+
if (session.access === 'plan')
|
|
4106
|
+
throw new Error('RUNNER_COMMAND_UNAVAILABLE');
|
|
4107
|
+
const current = this.commandStates.get(session.id, 'plan');
|
|
4108
|
+
if (current?.active)
|
|
4109
|
+
this.commandStates.clear(session.id, 'plan');
|
|
4110
|
+
else
|
|
4111
|
+
this.commandStates.set(session.id, {
|
|
4112
|
+
commandId: 'plan',
|
|
4113
|
+
active: true,
|
|
4114
|
+
label: 'Plan Mode',
|
|
4115
|
+
detail: '后续消息将以标签式规划模式运行,只读规划并输出计划卡。',
|
|
4116
|
+
closable: true,
|
|
4117
|
+
closeInput: '/plan',
|
|
4118
|
+
continuation: { label: 'Implement', prompt: '请根据上述计划开始实施。' }
|
|
4119
|
+
});
|
|
4120
|
+
return { command: command.id, states: this.commandStates.list(session.id) };
|
|
3997
4121
|
}
|
|
3998
4122
|
if (command.id === 'plan') {
|
|
3999
4123
|
await this.codexClient.start();
|
|
@@ -4212,6 +4336,45 @@ export class NodeConnector {
|
|
|
4212
4336
|
});
|
|
4213
4337
|
});
|
|
4214
4338
|
}
|
|
4339
|
+
requestClaudeUserInput(runId, toolInput, toolUseId) {
|
|
4340
|
+
const questions = claudeUserInputQuestions(toolInput);
|
|
4341
|
+
if (questions.length === 0)
|
|
4342
|
+
return Promise.resolve(denyPermission('USER_INPUT_INVALID'));
|
|
4343
|
+
const requestId = `claude:${runId}:${toolUseId}`;
|
|
4344
|
+
return new Promise((resolve) => {
|
|
4345
|
+
const input = { ...toolInput };
|
|
4346
|
+
const settleDeny = (message = 'USER_INPUT_EXPIRED') => resolve(denyPermission(message));
|
|
4347
|
+
const timer = setTimeout(() => {
|
|
4348
|
+
const pending = this.claudeUserInputs.get(requestId);
|
|
4349
|
+
if (pending === undefined)
|
|
4350
|
+
return;
|
|
4351
|
+
this.claudeUserInputs.delete(requestId);
|
|
4352
|
+
pending.reject('USER_INPUT_EXPIRED');
|
|
4353
|
+
this.emitConversationItem(runId, {
|
|
4354
|
+
itemId: boundedConversationItemId('claude-input', requestId),
|
|
4355
|
+
kind: 'user_input_request',
|
|
4356
|
+
status: 'FAILED',
|
|
4357
|
+
payload: { requestId, questions },
|
|
4358
|
+
merge: true
|
|
4359
|
+
});
|
|
4360
|
+
}, this.claudeApprovalTimeoutMs);
|
|
4361
|
+
this.claudeUserInputs.set(requestId, {
|
|
4362
|
+
runId,
|
|
4363
|
+
requestId,
|
|
4364
|
+
questions,
|
|
4365
|
+
input,
|
|
4366
|
+
resolve: (answers) => resolve({ behavior: 'allow', updatedInput: { ...input, answers } }),
|
|
4367
|
+
reject: settleDeny,
|
|
4368
|
+
timer
|
|
4369
|
+
});
|
|
4370
|
+
this.emitConversationItem(runId, {
|
|
4371
|
+
itemId: boundedConversationItemId('claude-input', requestId),
|
|
4372
|
+
kind: 'user_input_request',
|
|
4373
|
+
status: 'PENDING',
|
|
4374
|
+
payload: { requestId, questions }
|
|
4375
|
+
});
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4215
4378
|
/**
|
|
4216
4379
|
* An approval is an individual Runner callback. Keep its exact payload in
|
|
4217
4380
|
* the process-local snapshot and emit a Run event so an already open
|
|
@@ -5045,9 +5208,24 @@ function nativeConversationPage(session, history, input, runtimeTurns = []) {
|
|
|
5045
5208
|
const turnId = `${session.id}:native:${index}`;
|
|
5046
5209
|
const isToolCall = entry.kind === 'tool_call';
|
|
5047
5210
|
const isCommand = isToolCall && isClaudeCommandTool(entry.toolName);
|
|
5211
|
+
const userInputQuestions = session.runner === 'claude-code' &&
|
|
5212
|
+
isToolCall &&
|
|
5213
|
+
entry.toolName === 'AskUserQuestion' &&
|
|
5214
|
+
isPlainRecord(entry.input)
|
|
5215
|
+
? claudeUserInputQuestions(entry.input)
|
|
5216
|
+
: [];
|
|
5217
|
+
const isUserInputRequest = userInputQuestions.length > 0;
|
|
5048
5218
|
const clientMessageId = !isToolCall && entry.role === 'USER'
|
|
5049
5219
|
? nativeUserClientMessageId(entry, time, runtimeTurns)
|
|
5050
5220
|
: null;
|
|
5221
|
+
// Claude 标签式 Plan Mode:用户正文剥离注入的规划指令标签,
|
|
5222
|
+
// 完整的 <proposed_plan> Agent 文本归一化为计划卡。
|
|
5223
|
+
const entryText = entry.kind === 'message'
|
|
5224
|
+
? entry.role === 'USER'
|
|
5225
|
+
? stripClaudePlanTag(entry.text)
|
|
5226
|
+
: entry.text
|
|
5227
|
+
: '';
|
|
5228
|
+
const planText = entry.kind === 'message' && entry.role !== 'USER' ? claudePlanText(entryText) : undefined;
|
|
5051
5229
|
const item = {
|
|
5052
5230
|
id: `${turnId}:item`,
|
|
5053
5231
|
sessionId: session.id,
|
|
@@ -5056,38 +5234,49 @@ function nativeConversationPage(session, history, input, runtimeTurns = []) {
|
|
|
5056
5234
|
parentItemId: null,
|
|
5057
5235
|
sourceSequence: index,
|
|
5058
5236
|
revision: 0,
|
|
5059
|
-
kind:
|
|
5060
|
-
?
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
: '
|
|
5237
|
+
kind: isUserInputRequest
|
|
5238
|
+
? 'user_input_request'
|
|
5239
|
+
: isToolCall
|
|
5240
|
+
? isCommand
|
|
5241
|
+
? 'command_execution'
|
|
5242
|
+
: 'tool_call'
|
|
5243
|
+
: entry.role === 'USER'
|
|
5244
|
+
? 'user_message'
|
|
5245
|
+
: planText !== undefined
|
|
5246
|
+
? 'plan'
|
|
5247
|
+
: 'assistant_message',
|
|
5066
5248
|
status: 'COMPLETED',
|
|
5067
|
-
payload:
|
|
5068
|
-
?
|
|
5069
|
-
? {
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5249
|
+
payload: isUserInputRequest
|
|
5250
|
+
? {
|
|
5251
|
+
requestId: `native:${entry.kind === 'tool_call' ? entry.toolUseId : `item-${index}`}`,
|
|
5252
|
+
questions: userInputQuestions
|
|
5253
|
+
}
|
|
5254
|
+
: isToolCall
|
|
5255
|
+
? isCommand
|
|
5256
|
+
? {
|
|
5257
|
+
command: nativeClaudeCommand(entry),
|
|
5258
|
+
cwd: null,
|
|
5259
|
+
outputPreview: entry.outputSummary ?? '',
|
|
5260
|
+
outputRef: null,
|
|
5261
|
+
exitCode: null,
|
|
5262
|
+
durationMs: null,
|
|
5263
|
+
truncated: entry.inputTruncated || entry.outputTruncated
|
|
5264
|
+
}
|
|
5265
|
+
: {
|
|
5266
|
+
namespace: session.runner,
|
|
5267
|
+
toolName: entry.toolName,
|
|
5268
|
+
title: entry.toolName,
|
|
5269
|
+
inputSummary: entry.inputSummary,
|
|
5270
|
+
outputSummary: entry.outputSummary,
|
|
5271
|
+
progressLabel: null,
|
|
5272
|
+
errorCode: entry.failed ? 'TOOL_EXECUTION_FAILED' : null,
|
|
5273
|
+
truncated: entry.inputTruncated || entry.outputTruncated
|
|
5274
|
+
}
|
|
5275
|
+
: entry.role === 'USER'
|
|
5276
|
+
? { clientMessageId, text: entryText, contexts: [], delivery: 'ACCEPTED' }
|
|
5277
|
+
: planText !== undefined
|
|
5278
|
+
? { explanation: compactRunnerText(planText, 20_000), steps: [] }
|
|
5279
|
+
: { text: entryText, format: 'markdown', source: 'native-file' },
|
|
5091
5280
|
startedAt: time,
|
|
5092
5281
|
completedAt: time
|
|
5093
5282
|
};
|
|
@@ -5124,7 +5313,8 @@ function nativeUserClientMessageId(entry, createdAt, runtimeTurns) {
|
|
|
5124
5313
|
continue;
|
|
5125
5314
|
const payload = user.payload;
|
|
5126
5315
|
const clientMessageId = typeof payload.clientMessageId === 'string' ? payload.clientMessageId : null;
|
|
5127
|
-
|
|
5316
|
+
// 标签式 Plan Mode 的指令标签只存在于原生 transcript,运行态气泡始终是原文。
|
|
5317
|
+
if (clientMessageId === null || payload.text !== stripClaudePlanTag(entry.text))
|
|
5128
5318
|
continue;
|
|
5129
5319
|
const runtimeCreatedAt = user.startedAt ?? turn.startedAt;
|
|
5130
5320
|
if (runtimeCreatedAt !== null &&
|
|
@@ -5338,6 +5528,20 @@ function codexPlanText(value) {
|
|
|
5338
5528
|
.trim();
|
|
5339
5529
|
return plan.length > 0 ? plan : undefined;
|
|
5340
5530
|
}
|
|
5531
|
+
/**
|
|
5532
|
+
* Claude 标签式 Plan Mode 的最终计划以 `<proposed_plan>` 完整包裹输出;
|
|
5533
|
+
* 未闭合或缺失标签的文本保持普通 assistant 消息。
|
|
5534
|
+
*/
|
|
5535
|
+
function claudePlanText(text) {
|
|
5536
|
+
const trimmed = text.trim();
|
|
5537
|
+
if (!trimmed.startsWith('<proposed_plan>') || !/<\/proposed_plan>\s*$/.test(trimmed))
|
|
5538
|
+
return undefined;
|
|
5539
|
+
const plan = trimmed
|
|
5540
|
+
.slice('<proposed_plan>'.length)
|
|
5541
|
+
.replace(/<\/proposed_plan>\s*$/, '')
|
|
5542
|
+
.trim();
|
|
5543
|
+
return plan.length > 0 ? plan : undefined;
|
|
5544
|
+
}
|
|
5341
5545
|
function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
|
|
5342
5546
|
if (!isPlainRecord(value) || typeof value.type !== 'string')
|
|
5343
5547
|
return undefined;
|
|
@@ -5592,6 +5796,44 @@ function codexUserInputQuestions(value) {
|
|
|
5592
5796
|
? []
|
|
5593
5797
|
: questions;
|
|
5594
5798
|
}
|
|
5799
|
+
function claudeUserInputQuestions(value) {
|
|
5800
|
+
if (!Array.isArray(value.questions) || value.questions.length === 0 || value.questions.length > 4)
|
|
5801
|
+
return [];
|
|
5802
|
+
const questions = value.questions.map((raw, index) => {
|
|
5803
|
+
if (!isPlainRecord(raw) || typeof raw.question !== 'string')
|
|
5804
|
+
return undefined;
|
|
5805
|
+
const options = Array.isArray(raw.options)
|
|
5806
|
+
? raw.options.flatMap((option) => {
|
|
5807
|
+
if (!isPlainRecord(option) || typeof option.label !== 'string')
|
|
5808
|
+
return [];
|
|
5809
|
+
const label = compactRunnerText(option.label, 500);
|
|
5810
|
+
return label.length === 0
|
|
5811
|
+
? []
|
|
5812
|
+
: [
|
|
5813
|
+
{
|
|
5814
|
+
value: label,
|
|
5815
|
+
label,
|
|
5816
|
+
description: typeof option.description === 'string'
|
|
5817
|
+
? compactRunnerText(option.description, 1_000)
|
|
5818
|
+
: null
|
|
5819
|
+
}
|
|
5820
|
+
];
|
|
5821
|
+
})
|
|
5822
|
+
: [];
|
|
5823
|
+
const multiSelect = raw.multiSelect === true;
|
|
5824
|
+
return {
|
|
5825
|
+
id: `claude-question-${index}`,
|
|
5826
|
+
prompt: compactRunnerText(raw.question, 5_000),
|
|
5827
|
+
input: options.length === 0 ? 'SHORT_TEXT' : multiSelect ? 'MULTI_SELECT' : 'SINGLE_SELECT',
|
|
5828
|
+
options,
|
|
5829
|
+
allowOther: options.length === 0,
|
|
5830
|
+
multiSelect
|
|
5831
|
+
};
|
|
5832
|
+
});
|
|
5833
|
+
return questions.some((question) => question === undefined)
|
|
5834
|
+
? []
|
|
5835
|
+
: questions;
|
|
5836
|
+
}
|
|
5595
5837
|
function codexItemStatus(value, completed) {
|
|
5596
5838
|
if (value === 'failed')
|
|
5597
5839
|
return 'FAILED';
|