linkshell-cli 0.5.34 → 0.5.35
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/cli/src/runtime/acp/acp-client.d.ts +38 -0
- package/dist/cli/src/runtime/acp/acp-client.js +134 -20
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +48 -0
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +267 -0
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +23 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +834 -90
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +3 -1
- package/dist/cli/src/runtime/acp/provider-resolver.js +13 -4
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +1 -0
- package/dist/cli/src/runtime/bridge-session.js +14 -3
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +1161 -939
- package/dist/shared-protocol/src/index.js +43 -1
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/acp-client.ts +153 -22
- package/src/runtime/acp/agent-process-discovery.ts +301 -0
- package/src/runtime/acp/agent-workspace.ts +844 -96
- package/src/runtime/acp/provider-resolver.ts +18 -3
- package/src/runtime/bridge-session.ts +12 -3
- package/web/assets/{FileBrowser-9wRS6a0E.js → FileBrowser-Dn_avjeH.js} +1 -1
- package/web/assets/{PortPreview-yvXMn63g.js → PortPreview-KYkYV_-p.js} +1 -1
- package/web/assets/{TerminalPanel-xtGQI_fv.js → TerminalPanel-BjxX8Jui.js} +1 -1
- package/web/assets/{UsageDashboard-ByQ6mehF.js → UsageDashboard-cYiZs8c8.js} +1 -1
- package/web/assets/index-6vyVf_xQ.js +35 -0
- package/web/assets/index-Ct5fhCTS.css +33 -0
- package/web/index.html +2 -2
- package/web/assets/index-Vtyd7s5T.css +0 -33
- package/web/assets/index-pqslvKcS.js +0 -35
|
@@ -12,6 +12,14 @@ import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
|
12
12
|
import { aggregateUsage } from "../usage-report.js";
|
|
13
13
|
import type { AgentProtocol, AgentProvider } from "./provider-resolver.js";
|
|
14
14
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
15
|
+
import {
|
|
16
|
+
agentProviderLabel,
|
|
17
|
+
discoverAgentProcessesAsync,
|
|
18
|
+
isLiveConversationId,
|
|
19
|
+
isProtocolAgentProvider,
|
|
20
|
+
liveConversationId,
|
|
21
|
+
type AgentProcessSnapshot,
|
|
22
|
+
} from "./agent-process-discovery.js";
|
|
15
23
|
|
|
16
24
|
type AgentStatus = "unavailable" | "idle" | "running" | "waiting_permission" | "error";
|
|
17
25
|
type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
|
|
@@ -173,6 +181,21 @@ function normalizeMcpStatus(raw: unknown): AgentMcpServerStatus {
|
|
|
173
181
|
return "pending";
|
|
174
182
|
}
|
|
175
183
|
|
|
184
|
+
function mcpStatusFromRecord(rec: Record<string, unknown>): AgentMcpServerStatus {
|
|
185
|
+
if (normalizeMcpStatus(rec.authStatus ?? rec.auth) === "needs_auth") return "needs_auth";
|
|
186
|
+
return normalizeMcpStatus(rec.status ?? rec.state ?? rec.authStatus);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function mcpListFromProviderResult(value: unknown): AgentMcpServerDescriptor[] {
|
|
190
|
+
const raw = asRecord(value);
|
|
191
|
+
const list =
|
|
192
|
+
Array.isArray(value) ? value :
|
|
193
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
194
|
+
Array.isArray(raw?.servers) ? raw.servers :
|
|
195
|
+
value;
|
|
196
|
+
return parseMcpServerList(list);
|
|
197
|
+
}
|
|
198
|
+
|
|
176
199
|
// Parse an MCP server list from either an array of {name,status,...} objects or
|
|
177
200
|
// a {name: status} record — Claude's init `mcp_servers` shape isn't fixed.
|
|
178
201
|
function parseMcpServerList(value: unknown): AgentMcpServerDescriptor[] {
|
|
@@ -185,7 +208,7 @@ function parseMcpServerList(value: unknown): AgentMcpServerDescriptor[] {
|
|
|
185
208
|
if (!name) continue;
|
|
186
209
|
out.push({
|
|
187
210
|
name,
|
|
188
|
-
status:
|
|
211
|
+
status: mcpStatusFromRecord(rec),
|
|
189
212
|
error: firstString(rec, ["error", "message", "reason"]),
|
|
190
213
|
toolCount: firstNumber(rec, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(rec.tools) ? rec.tools.length : undefined),
|
|
191
214
|
});
|
|
@@ -202,7 +225,7 @@ function parseMcpServerList(value: unknown): AgentMcpServerDescriptor[] {
|
|
|
202
225
|
const inner = asRecord(raw);
|
|
203
226
|
out.push({
|
|
204
227
|
name,
|
|
205
|
-
status:
|
|
228
|
+
status: inner ? mcpStatusFromRecord(inner) : normalizeMcpStatus(raw),
|
|
206
229
|
error: inner ? firstString(inner, ["error", "message", "reason"]) : undefined,
|
|
207
230
|
toolCount: inner ? (firstNumber(inner, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(inner.tools) ? inner.tools.length : undefined)) : undefined,
|
|
208
231
|
});
|
|
@@ -456,6 +479,42 @@ function normalizePlanStatus(value: unknown): AgentPlanStep["status"] {
|
|
|
456
479
|
return "pending";
|
|
457
480
|
}
|
|
458
481
|
|
|
482
|
+
function applyThreadSettingsToConversation(conversation: AgentConversation, value: unknown): void {
|
|
483
|
+
const raw = asRecord(value);
|
|
484
|
+
const settings = asRecord(raw?.threadSettings) ?? asRecord(raw?.settings) ?? raw;
|
|
485
|
+
if (!settings) return;
|
|
486
|
+
const model = firstString(settings, ["model", "modelId", "currentModel"]);
|
|
487
|
+
if (model) conversation.model = model;
|
|
488
|
+
const effort = firstString(settings, ["effort", "reasoningEffort", "reasoning_effort"])
|
|
489
|
+
?? firstString(asRecord(asRecord(settings.collaborationMode ?? settings.collaboration_mode)?.settings), ["reasoning_effort", "reasoningEffort"]);
|
|
490
|
+
if (effort) conversation.reasoningEffort = effort;
|
|
491
|
+
const collab = asRecord(settings.collaborationMode ?? settings.collaboration_mode);
|
|
492
|
+
const mode = typeof settings.collaborationMode === "string"
|
|
493
|
+
? settings.collaborationMode
|
|
494
|
+
: typeof settings.collaboration_mode === "string"
|
|
495
|
+
? settings.collaboration_mode
|
|
496
|
+
: firstString(collab, ["mode"]);
|
|
497
|
+
if (mode === "plan") conversation.collaborationMode = "plan";
|
|
498
|
+
else if (mode === "default") conversation.collaborationMode = "default";
|
|
499
|
+
const permission = firstString(settings, ["permissionMode", "permission_mode"]);
|
|
500
|
+
if (permission === "read_only" || permission === "workspace_write" || permission === "full_access") {
|
|
501
|
+
conversation.permissionMode = permission;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const sandboxValue = settings.sandboxPolicy ?? settings.sandbox_policy;
|
|
505
|
+
const sandbox = asRecord(sandboxValue);
|
|
506
|
+
const sandboxType = typeof sandboxValue === "string" ? sandboxValue : firstString(sandbox, ["type"]);
|
|
507
|
+
if (!sandboxType) return;
|
|
508
|
+
const norm = sandboxType.replace(/-/g, "").toLowerCase();
|
|
509
|
+
conversation.permissionMode = norm.includes("readonly")
|
|
510
|
+
? "read_only"
|
|
511
|
+
: norm.includes("workspace")
|
|
512
|
+
? "workspace_write"
|
|
513
|
+
: norm.includes("danger") || norm.includes("full")
|
|
514
|
+
? "full_access"
|
|
515
|
+
: conversation.permissionMode;
|
|
516
|
+
}
|
|
517
|
+
|
|
459
518
|
function agentStatusFromThreadStatus(value: unknown): AgentStatus | undefined {
|
|
460
519
|
const raw = asRecord(value);
|
|
461
520
|
const status = typeof value === "string"
|
|
@@ -923,9 +982,7 @@ function previewText(text: string): string {
|
|
|
923
982
|
}
|
|
924
983
|
|
|
925
984
|
function providerLabel(provider: AgentProvider): string {
|
|
926
|
-
|
|
927
|
-
if (provider === "claude") return "Claude";
|
|
928
|
-
return "Custom";
|
|
985
|
+
return agentProviderLabel(provider);
|
|
929
986
|
}
|
|
930
987
|
|
|
931
988
|
interface AgentModelOption {
|
|
@@ -1314,6 +1371,27 @@ function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>):
|
|
|
1314
1371
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1315
1372
|
}
|
|
1316
1373
|
|
|
1374
|
+
function parseSkillCommands(provider: AgentProvider, value: unknown): AgentCommandDescriptor[] {
|
|
1375
|
+
const raw = asRecord(value);
|
|
1376
|
+
const groups =
|
|
1377
|
+
Array.isArray(value) ? value :
|
|
1378
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1379
|
+
Array.isArray(raw?.skills) ? [raw] :
|
|
1380
|
+
[];
|
|
1381
|
+
const skills: unknown[] = [];
|
|
1382
|
+
for (const group of groups) {
|
|
1383
|
+
if (typeof group === "string" && group) {
|
|
1384
|
+
skills.push(group);
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
const rec = asRecord(group);
|
|
1388
|
+
if (!rec) continue;
|
|
1389
|
+
if (Array.isArray(rec.skills)) skills.push(...rec.skills);
|
|
1390
|
+
else if (firstString(rec, ["name", "command", "id"])) skills.push(group);
|
|
1391
|
+
}
|
|
1392
|
+
return runtimeCommands(provider, skills);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1317
1395
|
function runtimeCommands(provider: AgentProvider, value: unknown): AgentCommandDescriptor[] {
|
|
1318
1396
|
const raw = asRecord(value);
|
|
1319
1397
|
const commandsValue =
|
|
@@ -1628,12 +1706,11 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1628
1706
|
const source = nestedThread ?? session;
|
|
1629
1707
|
const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
|
|
1630
1708
|
if (!id) continue;
|
|
1631
|
-
//
|
|
1632
|
-
// (
|
|
1633
|
-
//
|
|
1634
|
-
|
|
1635
|
-
const status
|
|
1636
|
-
reportedStatus === "running" || reportedStatus === "idle" ? reportedStatus : undefined;
|
|
1709
|
+
// Claude reports "running"/"idle" strings; Codex thread/list uses a tagged
|
|
1710
|
+
// object ({ type: "active"|"idle"|"notLoaded"|"systemError" }). Both map
|
|
1711
|
+
// through the same helper. Unknown / notLoaded stay undefined so the sync
|
|
1712
|
+
// site keeps its own status.
|
|
1713
|
+
const status = agentStatusFromThreadStatus(source.status);
|
|
1637
1714
|
result.push({
|
|
1638
1715
|
id,
|
|
1639
1716
|
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
@@ -1933,10 +2010,9 @@ export class AgentWorkspaceProxy {
|
|
|
1933
2010
|
private conversations = new Map<string, AgentConversation>();
|
|
1934
2011
|
private conversationByAgentSessionId = new Map<string, string>();
|
|
1935
2012
|
private timelines = new Map<string, AgentTimelineItem[]>();
|
|
1936
|
-
// Conversations the user
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
// forgotten conversation stays gone for the life of this workspace process.
|
|
2013
|
+
// Conversations the user forgot from the LinkShell list. Tombstone by
|
|
2014
|
+
// agentSessionId so syncProviderSessions cannot resurrect them. This is
|
|
2015
|
+
// NOT Codex thread/delete (that hard-deletes the on-disk rollout).
|
|
1940
2016
|
private deletedAgentSessionIds = new Set<string>();
|
|
1941
2017
|
// Opaque codex turns cursor per conversation, pointing at OLDER history to
|
|
1942
2018
|
// page through (captured at hydration, advanced on each history.request).
|
|
@@ -1950,6 +2026,13 @@ export class AgentWorkspaceProxy {
|
|
|
1950
2026
|
private itemConversationIds = new Map<string, string>();
|
|
1951
2027
|
private toolConversationIds = new Map<string, string>();
|
|
1952
2028
|
|
|
2029
|
+
private processPollTimer: ReturnType<typeof setInterval> | undefined;
|
|
2030
|
+
private processPollInFlight = false;
|
|
2031
|
+
private processMissCounts = new Map<string, number>();
|
|
2032
|
+
private lastDiscoveredProviders = new Set<string>();
|
|
2033
|
+
private static readonly PROCESS_POLL_MS = 3_000;
|
|
2034
|
+
private static readonly PROCESS_MISS_LIMIT = 2;
|
|
2035
|
+
|
|
1953
2036
|
constructor(
|
|
1954
2037
|
private readonly input: {
|
|
1955
2038
|
sessionId: string;
|
|
@@ -1958,6 +2041,7 @@ export class AgentWorkspaceProxy {
|
|
|
1958
2041
|
command?: string;
|
|
1959
2042
|
send: (envelope: Envelope) => void;
|
|
1960
2043
|
verbose?: boolean;
|
|
2044
|
+
discoverProcesses?: () => AgentProcessSnapshot[];
|
|
1961
2045
|
},
|
|
1962
2046
|
) {}
|
|
1963
2047
|
|
|
@@ -1977,35 +2061,83 @@ export class AgentWorkspaceProxy {
|
|
|
1977
2061
|
const payload = parseTypedPayload("agent.v2.conversation.update", envelope.payload);
|
|
1978
2062
|
const conversation = this.conversations.get(payload.conversationId);
|
|
1979
2063
|
if (!conversation) break;
|
|
2064
|
+
const raw = asRecord(envelope.payload) ?? {};
|
|
2065
|
+
const model = payload.model ?? (typeof raw.model === "string" ? raw.model : undefined);
|
|
2066
|
+
const reasoningEffort = payload.reasoningEffort
|
|
2067
|
+
?? (typeof raw.reasoningEffort === "string" ? raw.reasoningEffort : undefined);
|
|
2068
|
+
const permissionMode = payload.permissionMode
|
|
2069
|
+
?? (raw.permissionMode === "read_only" || raw.permissionMode === "workspace_write" || raw.permissionMode === "full_access"
|
|
2070
|
+
? raw.permissionMode
|
|
2071
|
+
: undefined);
|
|
2072
|
+
const collaborationMode = payload.collaborationMode
|
|
2073
|
+
?? (raw.collaborationMode === "default" || raw.collaborationMode === "plan"
|
|
2074
|
+
? raw.collaborationMode
|
|
2075
|
+
: undefined);
|
|
1980
2076
|
if (payload.title !== undefined) {
|
|
1981
2077
|
const trimmed = payload.title.trim();
|
|
1982
2078
|
conversation.title = trimmed === "" ? undefined : trimmed;
|
|
1983
2079
|
}
|
|
1984
2080
|
if (payload.archived !== undefined) conversation.archived = payload.archived;
|
|
2081
|
+
if (model !== undefined) {
|
|
2082
|
+
const trimmed = model.trim();
|
|
2083
|
+
if (trimmed) conversation.model = trimmed;
|
|
2084
|
+
}
|
|
2085
|
+
if (reasoningEffort !== undefined) conversation.reasoningEffort = reasoningEffort;
|
|
2086
|
+
if (permissionMode !== undefined) conversation.permissionMode = permissionMode;
|
|
2087
|
+
if (collaborationMode !== undefined) conversation.collaborationMode = collaborationMode;
|
|
1985
2088
|
conversation.lastActivityAt = Date.now();
|
|
1986
2089
|
// Echo the updated record so every client refreshes (the web store
|
|
1987
2090
|
// force-replaces by id on any event carrying `conversation`).
|
|
1988
2091
|
this.emitConversation(conversation);
|
|
2092
|
+
if (conversation.provider === "codex" && conversation.agentSessionId) {
|
|
2093
|
+
const client = this.clientForProvider(conversation.provider);
|
|
2094
|
+
const settingsChanged =
|
|
2095
|
+
model !== undefined ||
|
|
2096
|
+
reasoningEffort !== undefined ||
|
|
2097
|
+
permissionMode !== undefined ||
|
|
2098
|
+
collaborationMode !== undefined;
|
|
2099
|
+
try {
|
|
2100
|
+
const acp = client as AcpClient | undefined;
|
|
2101
|
+
if (payload.title !== undefined && typeof acp?.setThreadName === "function") {
|
|
2102
|
+
await acp.setThreadName({
|
|
2103
|
+
sessionId: conversation.agentSessionId,
|
|
2104
|
+
name: conversation.title ?? "",
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
if (payload.archived === true && typeof acp?.archiveThread === "function") {
|
|
2108
|
+
await acp.archiveThread({ sessionId: conversation.agentSessionId });
|
|
2109
|
+
} else if (payload.archived === false && typeof acp?.unarchiveThread === "function") {
|
|
2110
|
+
await acp.unarchiveThread({ sessionId: conversation.agentSessionId });
|
|
2111
|
+
}
|
|
2112
|
+
if (settingsChanged && typeof acp?.updateThreadSettings === "function") {
|
|
2113
|
+
await acp.updateThreadSettings({
|
|
2114
|
+
sessionId: conversation.agentSessionId,
|
|
2115
|
+
model,
|
|
2116
|
+
reasoningEffort,
|
|
2117
|
+
permissionMode,
|
|
2118
|
+
collaborationMode,
|
|
2119
|
+
cwd: conversation.cwd,
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
if (this.input.verbose) {
|
|
2124
|
+
process.stderr.write(`[agent:v2] conversation update provider call failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
1989
2128
|
break;
|
|
1990
2129
|
}
|
|
1991
2130
|
case "agent.v2.conversation.delete": {
|
|
1992
2131
|
const payload = parseTypedPayload("agent.v2.conversation.delete", envelope.payload);
|
|
1993
|
-
|
|
1994
|
-
//
|
|
1995
|
-
//
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
this.
|
|
2002
|
-
this.timelines.delete(payload.conversationId);
|
|
2003
|
-
this.historyCursors.delete(payload.conversationId);
|
|
2004
|
-
this.input.send(createEnvelope({
|
|
2005
|
-
type: "agent.v2.conversation.deleted",
|
|
2006
|
-
sessionId: this.input.sessionId,
|
|
2007
|
-
payload: { conversationId: payload.conversationId },
|
|
2008
|
-
}));
|
|
2132
|
+
// Forget from LinkShell only. Never call Codex thread/delete here —
|
|
2133
|
+
// that hard-deletes the on-disk rollout. Archive is the destructive-ish
|
|
2134
|
+
// user action; this menu item is "hide from the tree".
|
|
2135
|
+
this.forgetConversationLocally(payload.conversationId);
|
|
2136
|
+
break;
|
|
2137
|
+
}
|
|
2138
|
+
case "agent.v2.mcp.login": {
|
|
2139
|
+
const payload = parseTypedPayload("agent.v2.mcp.login", envelope.payload);
|
|
2140
|
+
await this.startMcpLogin(payload);
|
|
2009
2141
|
break;
|
|
2010
2142
|
}
|
|
2011
2143
|
case "agent.v2.conversation.list": {
|
|
@@ -2084,6 +2216,10 @@ export class AgentWorkspaceProxy {
|
|
|
2084
2216
|
}
|
|
2085
2217
|
|
|
2086
2218
|
stop(): void {
|
|
2219
|
+
if (this.processPollTimer) {
|
|
2220
|
+
clearInterval(this.processPollTimer);
|
|
2221
|
+
this.processPollTimer = undefined;
|
|
2222
|
+
}
|
|
2087
2223
|
for (const watcher of this.fsWatchers) {
|
|
2088
2224
|
try { watcher.close(); } catch { /* already closed */ }
|
|
2089
2225
|
}
|
|
@@ -2112,7 +2248,9 @@ export class AgentWorkspaceProxy {
|
|
|
2112
2248
|
private fsSyncTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2113
2249
|
private fsSyncRunning = false;
|
|
2114
2250
|
private lastPushedConversationSignature: string | undefined;
|
|
2251
|
+
private lastHydrateAt = new Map<string, number>();
|
|
2115
2252
|
private static readonly FS_SYNC_THROTTLE_MS = 1500;
|
|
2253
|
+
private static readonly EXTERNAL_HYDRATE_THROTTLE_MS = 2000;
|
|
2116
2254
|
|
|
2117
2255
|
private startSessionWatchers(): void {
|
|
2118
2256
|
const roots = [
|
|
@@ -2164,16 +2302,36 @@ export class AgentWorkspaceProxy {
|
|
|
2164
2302
|
// and client re-renders. A cheap signature over the render-relevant fields
|
|
2165
2303
|
// gates the push.
|
|
2166
2304
|
const signature = this.conversationListSignature(conversations);
|
|
2167
|
-
if (signature
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2305
|
+
if (signature !== this.lastPushedConversationSignature) {
|
|
2306
|
+
this.lastPushedConversationSignature = signature;
|
|
2307
|
+
// Push the conversation list (statuses) — lighter than a full snapshot and
|
|
2308
|
+
// exactly what refreshes the tree. The web handles an unsolicited
|
|
2309
|
+
// conversation.list.result the same as a requested one.
|
|
2310
|
+
this.input.send(createEnvelope({
|
|
2311
|
+
type: "agent.v2.conversation.list.result",
|
|
2312
|
+
sessionId: this.input.sessionId,
|
|
2313
|
+
payload: { conversations },
|
|
2314
|
+
}));
|
|
2315
|
+
}
|
|
2316
|
+
// External sessions (status running / waiting_permission but LinkShell is
|
|
2317
|
+
// not driving the turn) have no live event stream on this app-server.
|
|
2318
|
+
// Re-hydrate on a short throttle so the Web timeline still moves.
|
|
2319
|
+
const now = Date.now();
|
|
2320
|
+
for (const conversation of conversations) {
|
|
2321
|
+
if (conversation.status !== "running" && conversation.status !== "waiting_permission") continue;
|
|
2322
|
+
if (this.currentTurnIds.has(conversation.id)) continue;
|
|
2323
|
+
const last = this.lastHydrateAt.get(conversation.id) ?? 0;
|
|
2324
|
+
if (now - last < AgentWorkspaceProxy.EXTERNAL_HYDRATE_THROTTLE_MS) continue;
|
|
2325
|
+
this.lastHydrateAt.set(conversation.id, now);
|
|
2326
|
+
try {
|
|
2327
|
+
await this.hydrateConversationFromProvider(conversation, undefined, { rememberActiveTurn: false });
|
|
2328
|
+
this.sendSnapshot(conversation.id);
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
if (this.input.verbose) {
|
|
2331
|
+
process.stderr.write(`[agent:v2] external hydrate failed for ${conversation.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2177
2335
|
} catch (error) {
|
|
2178
2336
|
if (this.input.verbose) {
|
|
2179
2337
|
process.stderr.write(`[agent:v2] fs-watch sync failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -2190,7 +2348,7 @@ export class AgentWorkspaceProxy {
|
|
|
2190
2348
|
* membership are what matter. */
|
|
2191
2349
|
private conversationListSignature(conversations: AgentConversation[]): string {
|
|
2192
2350
|
return conversations
|
|
2193
|
-
.map((c) => `${c.id}${c.status}${c.title ?? ""}${c.model ?? ""}${c.provider ?? ""}`)
|
|
2351
|
+
.map((c) => `${c.id}${c.status}${c.title ?? ""}${c.model ?? ""}${c.provider ?? ""}${c.lastMessagePreview ?? ""}`)
|
|
2194
2352
|
.sort()
|
|
2195
2353
|
.join("");
|
|
2196
2354
|
}
|
|
@@ -2244,6 +2402,156 @@ export class AgentWorkspaceProxy {
|
|
|
2244
2402
|
// Start watching the on-disk session stores so externally-started sessions
|
|
2245
2403
|
// and status changes surface live, not just at connect time.
|
|
2246
2404
|
this.startSessionWatchers();
|
|
2405
|
+
this.startProcessDiscovery();
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
private startProcessDiscovery(): void {
|
|
2409
|
+
void this.pollDiscoveredProcesses();
|
|
2410
|
+
if (this.processPollTimer) return;
|
|
2411
|
+
this.processPollTimer = setInterval(() => {
|
|
2412
|
+
void this.pollDiscoveredProcesses();
|
|
2413
|
+
}, AgentWorkspaceProxy.PROCESS_POLL_MS);
|
|
2414
|
+
this.processPollTimer.unref?.();
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
private async pollDiscoveredProcesses(): Promise<void> {
|
|
2418
|
+
if (this.processPollInFlight) return;
|
|
2419
|
+
this.processPollInFlight = true;
|
|
2420
|
+
try {
|
|
2421
|
+
const snapshots = this.input.discoverProcesses
|
|
2422
|
+
? this.input.discoverProcesses()
|
|
2423
|
+
: await discoverAgentProcessesAsync();
|
|
2424
|
+
this.applyDiscoveredProcesses(snapshots);
|
|
2425
|
+
} catch {
|
|
2426
|
+
// A failed ps scan must not tear down the workspace or hitch the PTY.
|
|
2427
|
+
} finally {
|
|
2428
|
+
this.processPollInFlight = false;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/** Open Island rule: a visible agent session is a live process. Two missed
|
|
2433
|
+
* polls (~6s) drop the running signal so a killed CLI does not linger. */
|
|
2434
|
+
reconcileDiscoveredProcesses(): void {
|
|
2435
|
+
let snapshots: AgentProcessSnapshot[] = [];
|
|
2436
|
+
try {
|
|
2437
|
+
snapshots = this.input.discoverProcesses?.() ?? [];
|
|
2438
|
+
} catch {
|
|
2439
|
+
snapshots = [];
|
|
2440
|
+
}
|
|
2441
|
+
this.applyDiscoveredProcesses(snapshots);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
private applyDiscoveredProcesses(snapshots: AgentProcessSnapshot[]): void {
|
|
2445
|
+
|
|
2446
|
+
const seenKeys = new Set<string>();
|
|
2447
|
+
const discoveredProviders = new Set<string>();
|
|
2448
|
+
let listChanged = false;
|
|
2449
|
+
|
|
2450
|
+
for (const snapshot of snapshots) {
|
|
2451
|
+
discoveredProviders.add(snapshot.provider);
|
|
2452
|
+
const key = snapshot.sessionId
|
|
2453
|
+
? `${snapshot.provider}:${snapshot.sessionId}`
|
|
2454
|
+
: `${snapshot.provider}:${snapshot.pid}`;
|
|
2455
|
+
seenKeys.add(key);
|
|
2456
|
+
this.processMissCounts.set(key, 0);
|
|
2457
|
+
|
|
2458
|
+
const existingId = snapshot.sessionId
|
|
2459
|
+
? this.conversationByAgentSessionId.get(snapshot.sessionId)
|
|
2460
|
+
: undefined;
|
|
2461
|
+
const liveId = liveConversationId(snapshot);
|
|
2462
|
+
const conversationId = existingId ?? liveId;
|
|
2463
|
+
const existing = this.conversations.get(conversationId);
|
|
2464
|
+
const cwd = snapshot.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
2465
|
+
const driving = this.currentTurnIds.has(conversationId);
|
|
2466
|
+
const status: AgentStatus = driving
|
|
2467
|
+
? existing?.status ?? "running"
|
|
2468
|
+
: existing?.status === "waiting_permission"
|
|
2469
|
+
? "waiting_permission"
|
|
2470
|
+
: "running";
|
|
2471
|
+
|
|
2472
|
+
if (existing) {
|
|
2473
|
+
const changed = existing.status !== status || existing.cwd !== cwd || !existing.title;
|
|
2474
|
+
existing.status = status;
|
|
2475
|
+
existing.cwd = cwd;
|
|
2476
|
+
if (!existing.title) existing.title = agentProviderLabel(snapshot.provider);
|
|
2477
|
+
if (snapshot.sessionId && !existing.agentSessionId) {
|
|
2478
|
+
existing.agentSessionId = snapshot.sessionId;
|
|
2479
|
+
this.conversationByAgentSessionId.set(snapshot.sessionId, existing.id);
|
|
2480
|
+
}
|
|
2481
|
+
if (changed) {
|
|
2482
|
+
listChanged = true;
|
|
2483
|
+
this.emitConversation(existing);
|
|
2484
|
+
}
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
const now = Date.now();
|
|
2489
|
+
const conversation: AgentConversation = {
|
|
2490
|
+
id: conversationId,
|
|
2491
|
+
agentSessionId: snapshot.sessionId,
|
|
2492
|
+
provider: snapshot.provider,
|
|
2493
|
+
cwd,
|
|
2494
|
+
title: agentProviderLabel(snapshot.provider),
|
|
2495
|
+
status,
|
|
2496
|
+
archived: false,
|
|
2497
|
+
lastMessagePreview: "本机终端中的 Agent 进程",
|
|
2498
|
+
lastActivityAt: now,
|
|
2499
|
+
createdAt: now,
|
|
2500
|
+
};
|
|
2501
|
+
this.conversations.set(conversation.id, conversation);
|
|
2502
|
+
if (snapshot.sessionId) this.conversationByAgentSessionId.set(snapshot.sessionId, conversation.id);
|
|
2503
|
+
this.emitConversation(conversation);
|
|
2504
|
+
listChanged = true;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
for (const [key, misses] of this.processMissCounts) {
|
|
2508
|
+
if (seenKeys.has(key)) continue;
|
|
2509
|
+
const next = misses + 1;
|
|
2510
|
+
this.processMissCounts.set(key, next);
|
|
2511
|
+
if (next < AgentWorkspaceProxy.PROCESS_MISS_LIMIT) continue;
|
|
2512
|
+
this.processMissCounts.delete(key);
|
|
2513
|
+
const [provider, idPart] = key.split(":");
|
|
2514
|
+
if (!provider || !idPart) continue;
|
|
2515
|
+
const conversation = [...this.conversations.values()].find((item) => {
|
|
2516
|
+
if (item.provider !== provider) return false;
|
|
2517
|
+
if (item.agentSessionId === idPart) return true;
|
|
2518
|
+
return item.id === `agent-live-${provider}-${idPart}`;
|
|
2519
|
+
});
|
|
2520
|
+
if (!conversation) continue;
|
|
2521
|
+
if (this.currentTurnIds.has(conversation.id)) continue;
|
|
2522
|
+
if (conversation.status === "waiting_permission") continue;
|
|
2523
|
+
if (conversation.status === "running") {
|
|
2524
|
+
conversation.status = "idle";
|
|
2525
|
+
conversation.lastActivityAt = Date.now();
|
|
2526
|
+
this.emitConversation(conversation);
|
|
2527
|
+
listChanged = true;
|
|
2528
|
+
}
|
|
2529
|
+
if (isLiveConversationId(conversation.id) && !conversation.agentSessionId) {
|
|
2530
|
+
this.conversations.delete(conversation.id);
|
|
2531
|
+
this.timelines.delete(conversation.id);
|
|
2532
|
+
this.input.send(createEnvelope({
|
|
2533
|
+
type: "agent.v2.conversation.deleted",
|
|
2534
|
+
sessionId: this.input.sessionId,
|
|
2535
|
+
payload: { conversationId: conversation.id },
|
|
2536
|
+
}));
|
|
2537
|
+
listChanged = true;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
const catalogChanged =
|
|
2542
|
+
discoveredProviders.size !== this.lastDiscoveredProviders.size ||
|
|
2543
|
+
[...discoveredProviders].some((provider) => !this.lastDiscoveredProviders.has(provider));
|
|
2544
|
+
this.lastDiscoveredProviders = discoveredProviders;
|
|
2545
|
+
if (catalogChanged) this.sendCapabilities();
|
|
2546
|
+
if (listChanged) {
|
|
2547
|
+
const conversations = [...this.conversations.values()].filter((item) => !item.archived);
|
|
2548
|
+
this.lastPushedConversationSignature = this.conversationListSignature(conversations);
|
|
2549
|
+
this.input.send(createEnvelope({
|
|
2550
|
+
type: "agent.v2.conversation.list.result",
|
|
2551
|
+
sessionId: this.input.sessionId,
|
|
2552
|
+
payload: { conversations },
|
|
2553
|
+
}));
|
|
2554
|
+
}
|
|
2247
2555
|
}
|
|
2248
2556
|
|
|
2249
2557
|
private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined> {
|
|
@@ -2398,9 +2706,11 @@ export class AgentWorkspaceProxy {
|
|
|
2398
2706
|
}
|
|
2399
2707
|
}
|
|
2400
2708
|
}
|
|
2709
|
+
const listedMcp = await this.listProviderMcpServers(client);
|
|
2401
2710
|
if (provider === "codex") {
|
|
2402
2711
|
// Codex `model/list` is unreliable across versions — make sure mobile always
|
|
2403
2712
|
// sees a usable model picker by merging in a static fallback list.
|
|
2713
|
+
const skillCommands = await this.loadProviderSkillCommands(provider, client);
|
|
2404
2714
|
const merged: ProviderRuntimeCapabilities = {
|
|
2405
2715
|
models: runtimeCapabilities?.models?.length
|
|
2406
2716
|
? runtimeCapabilities.models
|
|
@@ -2409,13 +2719,14 @@ export class AgentWorkspaceProxy {
|
|
|
2409
2719
|
reasoningEfforts: runtimeCapabilities?.reasoningEfforts?.length
|
|
2410
2720
|
? runtimeCapabilities.reasoningEfforts
|
|
2411
2721
|
: [...ALL_REASONING_EFFORTS],
|
|
2412
|
-
commands: runtimeCapabilities?.commands,
|
|
2722
|
+
commands: mergeCommands(runtimeCapabilities?.commands, skillCommands),
|
|
2413
2723
|
modes: runtimeCapabilities?.modes,
|
|
2414
2724
|
currentMode: runtimeCapabilities?.currentMode,
|
|
2415
2725
|
// Preserve any MCP status already accumulated from startupStatus events.
|
|
2416
2726
|
mcpServers: runtimeCapabilities?.mcpServers ?? this.providerCapabilities.get(provider)?.mcpServers,
|
|
2417
2727
|
};
|
|
2418
2728
|
this.providerCapabilities.set(provider, merged);
|
|
2729
|
+
if (listedMcp.length > 0) this.mergeMcpServers(provider, listedMcp);
|
|
2419
2730
|
return;
|
|
2420
2731
|
}
|
|
2421
2732
|
if (runtimeCapabilities) {
|
|
@@ -2427,8 +2738,48 @@ export class AgentWorkspaceProxy {
|
|
|
2427
2738
|
for (const [name, desc] of runtimeCapabilities.mcpServers ?? []) map.set(name, desc);
|
|
2428
2739
|
runtimeCapabilities = { ...runtimeCapabilities, mcpServers: map };
|
|
2429
2740
|
}
|
|
2741
|
+
const skillCommands = await this.loadProviderSkillCommands(provider, client);
|
|
2742
|
+
if (skillCommands.length > 0) {
|
|
2743
|
+
runtimeCapabilities = {
|
|
2744
|
+
...runtimeCapabilities,
|
|
2745
|
+
commands: mergeCommands(runtimeCapabilities.commands, skillCommands),
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2430
2748
|
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
2431
2749
|
}
|
|
2750
|
+
if (listedMcp.length > 0) this.mergeMcpServers(provider, listedMcp);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
private async listProviderMcpServers(
|
|
2754
|
+
client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient,
|
|
2755
|
+
): Promise<AgentMcpServerDescriptor[]> {
|
|
2756
|
+
const listMcpServers = (client as { listMcpServers?: () => Promise<unknown> }).listMcpServers;
|
|
2757
|
+
if (typeof listMcpServers !== "function") return [];
|
|
2758
|
+
try {
|
|
2759
|
+
return mcpListFromProviderResult(await listMcpServers.call(client));
|
|
2760
|
+
} catch (error) {
|
|
2761
|
+
if (this.input.verbose) {
|
|
2762
|
+
process.stderr.write(`[agent:v2] mcpServerStatus/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2763
|
+
}
|
|
2764
|
+
return [];
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
private async loadProviderSkillCommands(
|
|
2769
|
+
provider: AgentProvider,
|
|
2770
|
+
client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient,
|
|
2771
|
+
): Promise<AgentCommandDescriptor[]> {
|
|
2772
|
+
const listSkills = (client as { listSkills?: (input: { cwd: string }) => Promise<unknown> }).listSkills;
|
|
2773
|
+
if (typeof listSkills !== "function") return [];
|
|
2774
|
+
try {
|
|
2775
|
+
const result = await listSkills.call(client, { cwd: this.input.cwd });
|
|
2776
|
+
return parseSkillCommands(provider, result);
|
|
2777
|
+
} catch (error) {
|
|
2778
|
+
if (this.input.verbose) {
|
|
2779
|
+
process.stderr.write(`[agent:v2] skills/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2780
|
+
}
|
|
2781
|
+
return [];
|
|
2782
|
+
}
|
|
2432
2783
|
}
|
|
2433
2784
|
|
|
2434
2785
|
private async syncProviderSessions(): Promise<void> {
|
|
@@ -2485,9 +2836,11 @@ export class AgentWorkspaceProxy {
|
|
|
2485
2836
|
? existing?.status ?? "running"
|
|
2486
2837
|
: remoteRunning
|
|
2487
2838
|
? "running"
|
|
2488
|
-
:
|
|
2489
|
-
?
|
|
2490
|
-
: "
|
|
2839
|
+
: remote.status === "error"
|
|
2840
|
+
? "error"
|
|
2841
|
+
: existing?.status === "error" || existing?.status === "waiting_permission"
|
|
2842
|
+
? existing.status
|
|
2843
|
+
: "idle";
|
|
2491
2844
|
const conversation: AgentConversation = {
|
|
2492
2845
|
id: conversationId,
|
|
2493
2846
|
agentSessionId,
|
|
@@ -2520,6 +2873,7 @@ export class AgentWorkspaceProxy {
|
|
|
2520
2873
|
private async hydrateConversationFromProvider(
|
|
2521
2874
|
conversation: AgentConversation,
|
|
2522
2875
|
seedResult?: unknown,
|
|
2876
|
+
options?: { rememberActiveTurn?: boolean },
|
|
2523
2877
|
): Promise<void> {
|
|
2524
2878
|
let client = this.clientForProvider(conversation.provider);
|
|
2525
2879
|
if (!client || !conversation.agentSessionId) return;
|
|
@@ -2589,7 +2943,9 @@ export class AgentWorkspaceProxy {
|
|
|
2589
2943
|
|
|
2590
2944
|
const thread = threadFromProviderResult(source);
|
|
2591
2945
|
const activeTurnId = activeTurnIdFromThread(thread);
|
|
2592
|
-
if (activeTurnId
|
|
2946
|
+
if (activeTurnId && options?.rememberActiveTurn !== false) {
|
|
2947
|
+
this.rememberTurnConversationId(conversation.id, activeTurnId);
|
|
2948
|
+
}
|
|
2593
2949
|
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
2594
2950
|
if (model && !conversation.model) conversation.model = model;
|
|
2595
2951
|
|
|
@@ -2662,7 +3018,11 @@ export class AgentWorkspaceProxy {
|
|
|
2662
3018
|
}
|
|
2663
3019
|
|
|
2664
3020
|
private sendCapabilities(): void {
|
|
2665
|
-
const
|
|
3021
|
+
const catalog = [...new Set([
|
|
3022
|
+
...this.input.availableProviders,
|
|
3023
|
+
...this.lastDiscoveredProviders,
|
|
3024
|
+
])];
|
|
3025
|
+
const providers = catalog.map((provider) => {
|
|
2666
3026
|
const client = this.clients.get(provider);
|
|
2667
3027
|
const protocol = this.agentProtocols.get(provider);
|
|
2668
3028
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
@@ -2671,6 +3031,7 @@ export class AgentWorkspaceProxy {
|
|
|
2671
3031
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
2672
3032
|
const supportsPermission = enabled && !isClaudeFallback;
|
|
2673
3033
|
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
3034
|
+
const protocolBacked = isProtocolAgentProvider(provider);
|
|
2674
3035
|
const commands = mergeCommands(
|
|
2675
3036
|
defaultProviderCommands(provider, this.input.cwd, enabled),
|
|
2676
3037
|
runtimeCapabilities?.commands,
|
|
@@ -2680,7 +3041,11 @@ export class AgentWorkspaceProxy {
|
|
|
2680
3041
|
id: provider,
|
|
2681
3042
|
label: providerLabel(provider),
|
|
2682
3043
|
enabled,
|
|
2683
|
-
reason: enabled
|
|
3044
|
+
reason: enabled
|
|
3045
|
+
? undefined
|
|
3046
|
+
: protocolBacked
|
|
3047
|
+
? this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`
|
|
3048
|
+
: `${providerLabel(provider)} 已在本机检测;远程对话请在终端继续`,
|
|
2684
3049
|
supportsImages,
|
|
2685
3050
|
supportsPermission,
|
|
2686
3051
|
supportsPlan: enabled,
|
|
@@ -2717,11 +3082,11 @@ export class AgentWorkspaceProxy {
|
|
|
2717
3082
|
sessionId: this.input.sessionId,
|
|
2718
3083
|
payload: {
|
|
2719
3084
|
enabled: anyEnabled,
|
|
2720
|
-
provider: this.input.availableProviders[0] ?? "codex",
|
|
3085
|
+
provider: this.input.availableProviders[0] ?? catalog[0] ?? "codex",
|
|
2721
3086
|
providers,
|
|
2722
3087
|
protocolVersion: 1,
|
|
2723
3088
|
workspaceProtocolVersion: 2,
|
|
2724
|
-
error: anyEnabled ? undefined : "
|
|
3089
|
+
error: anyEnabled ? undefined : "没有可远程驱动的 Agent。请安装 Claude Code 或 Codex CLI;其它 Agent 只要在本机终端运行就会出现在列表里。",
|
|
2725
3090
|
supportsSessionList: anyEnabled,
|
|
2726
3091
|
supportsSessionLoad: anyEnabled,
|
|
2727
3092
|
supportsImages: providers.some((p) => p.supportsImages),
|
|
@@ -2750,18 +3115,44 @@ export class AgentWorkspaceProxy {
|
|
|
2750
3115
|
if (!provider) {
|
|
2751
3116
|
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
2752
3117
|
}
|
|
2753
|
-
if (!this.input.availableProviders.includes(provider)) {
|
|
2754
|
-
return this.openFailure(
|
|
2755
|
-
payload,
|
|
2756
|
-
`${providerLabel(provider)} 未安装或不可用。`,
|
|
2757
|
-
);
|
|
2758
|
-
}
|
|
2759
3118
|
|
|
2760
3119
|
let cwd = payload.cwd ?? this.input.cwd;
|
|
2761
3120
|
let agentSessionId = payload.agentSessionId;
|
|
2762
3121
|
let existingConversation =
|
|
2763
3122
|
(payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
2764
3123
|
(agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
|
|
3124
|
+
if (
|
|
3125
|
+
existingConversation &&
|
|
3126
|
+
!this.clientForProvider(existingConversation.provider) &&
|
|
3127
|
+
!isProtocolAgentProvider(existingConversation.provider)
|
|
3128
|
+
) {
|
|
3129
|
+
this.activeConversationId = existingConversation.id;
|
|
3130
|
+
this.input.send(createEnvelope({
|
|
3131
|
+
type: "agent.v2.conversation.opened",
|
|
3132
|
+
sessionId: this.input.sessionId,
|
|
3133
|
+
payload: {
|
|
3134
|
+
conversation: existingConversation,
|
|
3135
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
3136
|
+
requestedConversationId: payload.conversationId,
|
|
3137
|
+
},
|
|
3138
|
+
}));
|
|
3139
|
+
return existingConversation;
|
|
3140
|
+
}
|
|
3141
|
+
if (!isProtocolAgentProvider(provider) && !resolveAgentCommand({
|
|
3142
|
+
provider,
|
|
3143
|
+
command: this.input.command,
|
|
3144
|
+
})) {
|
|
3145
|
+
return this.openFailure(
|
|
3146
|
+
payload,
|
|
3147
|
+
`${providerLabel(provider)} 还不能从远程新建对话。在本机终端启动后,会话会出现在列表里。`,
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
if (!this.input.availableProviders.includes(provider) && !existingConversation) {
|
|
3151
|
+
return this.openFailure(
|
|
3152
|
+
payload,
|
|
3153
|
+
`${providerLabel(provider)} 未安装或不可用。`,
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
2765
3156
|
if (!payload.cwd && existingConversation?.cwd) cwd = existingConversation.cwd;
|
|
2766
3157
|
|
|
2767
3158
|
let client = await this.ensureProviderClient(provider);
|
|
@@ -2789,14 +3180,15 @@ export class AgentWorkspaceProxy {
|
|
|
2789
3180
|
}
|
|
2790
3181
|
|
|
2791
3182
|
// Fork: seed a NEW conversation from an existing one's transcript, truncated
|
|
2792
|
-
// at forkFromTurnId. Claude
|
|
2793
|
-
//
|
|
2794
|
-
//
|
|
2795
|
-
//
|
|
3183
|
+
// at forkFromTurnId. Claude uses the SDK forkSession(); Codex uses
|
|
3184
|
+
// thread/fork. Other providers notify and fall through to a plain new
|
|
3185
|
+
// conversation. On success we set agentSessionId to the new thread id and
|
|
3186
|
+
// let the normal load path hydrate it.
|
|
2796
3187
|
if (payload.forkFromConversationId) {
|
|
2797
3188
|
const source = this.conversations.get(payload.forkFromConversationId);
|
|
2798
3189
|
const sourceSessionId = source?.agentSessionId;
|
|
2799
3190
|
const forkable = (client as { forkSession?: (i: { sourceSessionId: string; uptoTurnId?: string }) => { sessionId: string } | undefined }).forkSession;
|
|
3191
|
+
const forkThread = (client as { forkThread?: (i: { sessionId: string; lastTurnId?: string }) => Promise<unknown> }).forkThread;
|
|
2800
3192
|
if (provider === "claude" && typeof forkable === "function" && sourceSessionId) {
|
|
2801
3193
|
if (source?.cwd && !payload.cwd) cwd = source.cwd;
|
|
2802
3194
|
const forked = forkable.call(client, { sourceSessionId, uptoTurnId: payload.forkFromTurnId });
|
|
@@ -2815,6 +3207,35 @@ export class AgentWorkspaceProxy {
|
|
|
2815
3207
|
} else {
|
|
2816
3208
|
this.emitNotice({ kind: "warning", title: "分叉失败", detail: "无法复制该会话的历史记录,已新建空会话。" });
|
|
2817
3209
|
}
|
|
3210
|
+
} else if (provider === "codex" && typeof forkThread === "function" && sourceSessionId) {
|
|
3211
|
+
if (source?.cwd && !payload.cwd) cwd = source.cwd;
|
|
3212
|
+
try {
|
|
3213
|
+
const forked = await forkThread.call(client, {
|
|
3214
|
+
sessionId: sourceSessionId,
|
|
3215
|
+
lastTurnId: payload.forkFromTurnId,
|
|
3216
|
+
});
|
|
3217
|
+
const forkedId = this.extractSessionId(forked);
|
|
3218
|
+
if (forkedId) {
|
|
3219
|
+
agentSessionId = forkedId;
|
|
3220
|
+
payload = {
|
|
3221
|
+
...payload,
|
|
3222
|
+
model: payload.model ?? source?.model,
|
|
3223
|
+
reasoningEffort: payload.reasoningEffort ?? source?.reasoningEffort,
|
|
3224
|
+
permissionMode: payload.permissionMode ?? source?.permissionMode,
|
|
3225
|
+
collaborationMode: payload.collaborationMode ?? source?.collaborationMode,
|
|
3226
|
+
title: payload.title ?? (source?.title ? `${source.title} (分叉)` : undefined),
|
|
3227
|
+
};
|
|
3228
|
+
existingConversation = undefined;
|
|
3229
|
+
} else {
|
|
3230
|
+
this.emitNotice({ kind: "warning", title: "分叉失败", detail: "无法复制该会话的历史记录,已新建空会话。" });
|
|
3231
|
+
}
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
this.emitNotice({
|
|
3234
|
+
kind: "warning",
|
|
3235
|
+
title: "分叉失败",
|
|
3236
|
+
detail: error instanceof Error ? error.message : "无法复制该会话的历史记录,已新建空会话。",
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
2818
3239
|
} else {
|
|
2819
3240
|
this.emitNotice({
|
|
2820
3241
|
kind: "native_unsupported",
|
|
@@ -3057,7 +3478,9 @@ export class AgentWorkspaceProxy {
|
|
|
3057
3478
|
if (!conversation.agentSessionId) {
|
|
3058
3479
|
this.rejectAgentAction(
|
|
3059
3480
|
conversation,
|
|
3060
|
-
|
|
3481
|
+
isLiveConversationId(conversation.id) || !isProtocolAgentProvider(conversation.provider)
|
|
3482
|
+
? `${providerLabel(conversation.provider)} 正在本机终端中运行。远程对话协议尚未接入,请打开终端面板继续。`
|
|
3483
|
+
: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
3061
3484
|
);
|
|
3062
3485
|
return;
|
|
3063
3486
|
}
|
|
@@ -3065,7 +3488,9 @@ export class AgentWorkspaceProxy {
|
|
|
3065
3488
|
if (!client) {
|
|
3066
3489
|
this.rejectAgentAction(
|
|
3067
3490
|
conversation,
|
|
3068
|
-
|
|
3491
|
+
isLiveConversationId(conversation.id) || !isProtocolAgentProvider(conversation.provider)
|
|
3492
|
+
? `${providerLabel(conversation.provider)} 正在本机终端中运行。远程对话协议尚未接入,请打开终端面板继续。`
|
|
3493
|
+
: this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
|
|
3069
3494
|
"error",
|
|
3070
3495
|
);
|
|
3071
3496
|
return;
|
|
@@ -3411,14 +3836,37 @@ export class AgentWorkspaceProxy {
|
|
|
3411
3836
|
return;
|
|
3412
3837
|
}
|
|
3413
3838
|
|
|
3414
|
-
if (command.name === "review"
|
|
3415
|
-
const
|
|
3416
|
-
|
|
3417
|
-
|
|
3839
|
+
if (command.name === "review") {
|
|
3840
|
+
const startReview = (client as { startReview?: (input: { sessionId: string; prompt?: string }) => Promise<unknown> })?.startReview;
|
|
3841
|
+
if (typeof startReview === "function" && conversation.agentSessionId) {
|
|
3842
|
+
conversation.status = "running";
|
|
3843
|
+
conversation.lastActivityAt = now;
|
|
3844
|
+
this.emitConversation(conversation);
|
|
3845
|
+
const result = await startReview.call(client, {
|
|
3846
|
+
sessionId: conversation.agentSessionId,
|
|
3847
|
+
prompt: args || undefined,
|
|
3848
|
+
});
|
|
3849
|
+
const turnId = this.extractTurnId(result);
|
|
3850
|
+
if (turnId) this.rememberTurnConversationId(conversation.id, turnId);
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3418
3853
|
await this.sendPrompt({
|
|
3419
3854
|
conversationId: conversation.id,
|
|
3420
3855
|
clientMessageId: id(command.name),
|
|
3421
|
-
contentBlocks: [{ type: "text", text:
|
|
3856
|
+
contentBlocks: [{ type: "text", text: args || "Review the current local changes." }],
|
|
3857
|
+
model: conversation.model,
|
|
3858
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
3859
|
+
permissionMode: conversation.permissionMode,
|
|
3860
|
+
collaborationMode: conversation.collaborationMode,
|
|
3861
|
+
});
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
if (command.name === "subagents") {
|
|
3866
|
+
await this.sendPrompt({
|
|
3867
|
+
conversationId: conversation.id,
|
|
3868
|
+
clientMessageId: id(command.name),
|
|
3869
|
+
contentBlocks: [{ type: "text", text: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results." }],
|
|
3422
3870
|
model: conversation.model,
|
|
3423
3871
|
reasoningEffort: conversation.reasoningEffort,
|
|
3424
3872
|
permissionMode: conversation.permissionMode,
|
|
@@ -3460,8 +3908,72 @@ export class AgentWorkspaceProxy {
|
|
|
3460
3908
|
return {};
|
|
3461
3909
|
}
|
|
3462
3910
|
|
|
3463
|
-
|
|
3464
|
-
|
|
3911
|
+
private async startMcpLogin(payload: { provider?: AgentProvider; serverName: string }): Promise<void> {
|
|
3912
|
+
const provider = payload.provider
|
|
3913
|
+
?? this.input.availableProviders.find((item) => this.clients.has(item))
|
|
3914
|
+
?? this.input.availableProviders[0];
|
|
3915
|
+
const sendResult = (input: { authorizationUrl?: string; error?: string }) => {
|
|
3916
|
+
this.input.send(createEnvelope({
|
|
3917
|
+
type: "agent.v2.mcp.login.result",
|
|
3918
|
+
sessionId: this.input.sessionId,
|
|
3919
|
+
payload: {
|
|
3920
|
+
...(provider ? { provider } : {}),
|
|
3921
|
+
serverName: payload.serverName,
|
|
3922
|
+
authorizationUrl: input.authorizationUrl,
|
|
3923
|
+
error: input.error,
|
|
3924
|
+
},
|
|
3925
|
+
}));
|
|
3926
|
+
};
|
|
3927
|
+
if (!provider) {
|
|
3928
|
+
sendResult({ error: "没有可用的 Agent 提供方。" });
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
const client = this.clientForProvider(provider);
|
|
3932
|
+
const startMcpOAuth = (client as { startMcpOAuth?: (input: { serverName: string }) => Promise<unknown> } | undefined)?.startMcpOAuth;
|
|
3933
|
+
if (typeof startMcpOAuth !== "function") {
|
|
3934
|
+
sendResult({ error: "请在主机上的 Claude / Codex 完成该 MCP 的授权。" });
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
try {
|
|
3938
|
+
const result = await startMcpOAuth.call(client, { serverName: payload.serverName });
|
|
3939
|
+
const raw = asRecord(result);
|
|
3940
|
+
const authorizationUrl = firstString(raw, [
|
|
3941
|
+
"authorizationUrl",
|
|
3942
|
+
"authorization_url",
|
|
3943
|
+
"url",
|
|
3944
|
+
"loginUrl",
|
|
3945
|
+
]);
|
|
3946
|
+
if (!authorizationUrl) {
|
|
3947
|
+
sendResult({ error: "主机没有返回授权地址。" });
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
this.mergeMcpServers(provider, [{
|
|
3951
|
+
name: payload.serverName,
|
|
3952
|
+
status: "needs_auth",
|
|
3953
|
+
}]);
|
|
3954
|
+
this.sendCapabilities();
|
|
3955
|
+
sendResult({ authorizationUrl });
|
|
3956
|
+
} catch (error) {
|
|
3957
|
+
sendResult({ error: error instanceof Error ? error.message : String(error) });
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
private handleMcpOAuthCompleted(params: unknown): void {
|
|
3962
|
+
const raw = asRecord(params) ?? {};
|
|
3963
|
+
const name = firstString(raw, ["name", "server", "serverName"]);
|
|
3964
|
+
if (!name) return;
|
|
3965
|
+
const success = raw.success !== false && !raw.error;
|
|
3966
|
+
const provider = this.input.availableProviders.find((item) => this.clients.has(item))
|
|
3967
|
+
?? this.input.availableProviders[0];
|
|
3968
|
+
if (!provider) return;
|
|
3969
|
+
this.mergeMcpServers(provider, [{
|
|
3970
|
+
name,
|
|
3971
|
+
status: success ? "connected" : "needs_auth",
|
|
3972
|
+
error: success ? undefined : firstString(raw, ["error", "message"]),
|
|
3973
|
+
}]);
|
|
3974
|
+
this.sendCapabilities();
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3465
3977
|
private mergeMcpServers(provider: AgentProvider, servers: AgentMcpServerDescriptor[]): void {
|
|
3466
3978
|
if (servers.length === 0) return;
|
|
3467
3979
|
const existing = this.providerCapabilities.get(provider) ?? {};
|
|
@@ -3486,7 +3998,7 @@ export class AgentWorkspaceProxy {
|
|
|
3486
3998
|
if (!name) return;
|
|
3487
3999
|
this.mergeMcpServers(provider, [{
|
|
3488
4000
|
name,
|
|
3489
|
-
status:
|
|
4001
|
+
status: mcpStatusFromRecord(raw),
|
|
3490
4002
|
error: firstString(raw, ["error", "message", "reason"]),
|
|
3491
4003
|
toolCount: firstNumber(raw, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(raw.tools) ? raw.tools.length : undefined),
|
|
3492
4004
|
}]);
|
|
@@ -3538,11 +4050,11 @@ export class AgentWorkspaceProxy {
|
|
|
3538
4050
|
this.handleMcpStartupStatus(method, params);
|
|
3539
4051
|
return;
|
|
3540
4052
|
}
|
|
3541
|
-
if (
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
) {
|
|
4053
|
+
if (method === "mcpServer/oauthLogin/completed") {
|
|
4054
|
+
this.handleMcpOAuthCompleted(params);
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
if (method.startsWith("account/") || method === "serverRequest/resolved") {
|
|
3546
4058
|
return;
|
|
3547
4059
|
}
|
|
3548
4060
|
|
|
@@ -3577,6 +4089,45 @@ export class AgentWorkspaceProxy {
|
|
|
3577
4089
|
}
|
|
3578
4090
|
return;
|
|
3579
4091
|
}
|
|
4092
|
+
if (method === "thread/name/updated") {
|
|
4093
|
+
if (conversationId) {
|
|
4094
|
+
const raw = asRecord(params);
|
|
4095
|
+
const title = firstString(raw, ["name", "title"]) ?? firstString(asRecord(raw?.thread), ["name", "title"]);
|
|
4096
|
+
const conversation = this.conversations.get(conversationId);
|
|
4097
|
+
if (conversation && title !== undefined) {
|
|
4098
|
+
conversation.title = title;
|
|
4099
|
+
conversation.lastActivityAt = Date.now();
|
|
4100
|
+
this.emitConversation(conversation);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
if (method === "thread/settings/updated") {
|
|
4106
|
+
if (conversationId) {
|
|
4107
|
+
const conversation = this.conversations.get(conversationId);
|
|
4108
|
+
if (conversation) {
|
|
4109
|
+
applyThreadSettingsToConversation(conversation, params);
|
|
4110
|
+
conversation.lastActivityAt = Date.now();
|
|
4111
|
+
this.emitConversation(conversation);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
if (method === "thread/archived" || method === "thread/unarchived") {
|
|
4117
|
+
if (conversationId) {
|
|
4118
|
+
const conversation = this.conversations.get(conversationId);
|
|
4119
|
+
if (conversation) {
|
|
4120
|
+
conversation.archived = method === "thread/archived";
|
|
4121
|
+
conversation.lastActivityAt = Date.now();
|
|
4122
|
+
this.emitConversation(conversation);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
return;
|
|
4126
|
+
}
|
|
4127
|
+
if (method === "thread/deleted") {
|
|
4128
|
+
if (conversationId) this.forgetConversationLocally(conversationId);
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
3580
4131
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
3581
4132
|
this.handleStructuredInput(params);
|
|
3582
4133
|
return;
|
|
@@ -3637,6 +4188,13 @@ export class AgentWorkspaceProxy {
|
|
|
3637
4188
|
case "item/agentMessage/delta":
|
|
3638
4189
|
this.handleAgentMessageDelta(params);
|
|
3639
4190
|
return;
|
|
4191
|
+
case "item/reasoning/summaryTextDelta":
|
|
4192
|
+
case "item/reasoning/textDelta":
|
|
4193
|
+
this.handleReasoningDelta(params);
|
|
4194
|
+
return;
|
|
4195
|
+
case "item/reasoning/summaryPartAdded":
|
|
4196
|
+
this.handleReasoningDelta(params, { partBoundary: true });
|
|
4197
|
+
return;
|
|
3640
4198
|
case "turn/plan/updated":
|
|
3641
4199
|
this.handlePlanUpdated(params);
|
|
3642
4200
|
return;
|
|
@@ -3700,10 +4258,68 @@ export class AgentWorkspaceProxy {
|
|
|
3700
4258
|
updatedAt: Date.now(),
|
|
3701
4259
|
isStreaming: true,
|
|
3702
4260
|
};
|
|
3703
|
-
this.
|
|
4261
|
+
this.applyStreamingTextDelta(conversationId, item, existing, delta);
|
|
3704
4262
|
this.updateConversationPreview(conversationId, text, "running");
|
|
3705
4263
|
}
|
|
3706
4264
|
|
|
4265
|
+
private handleReasoningDelta(params: unknown, options?: { partBoundary?: boolean }): void {
|
|
4266
|
+
const raw = asRecord(params);
|
|
4267
|
+
if (!raw) return;
|
|
4268
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
4269
|
+
if (!conversationId) return;
|
|
4270
|
+
const itemId = firstString(raw, ["itemId", "id", "messageId"]) ?? id("thinking");
|
|
4271
|
+
const existing = this.findItem(conversationId, itemId);
|
|
4272
|
+
const delta = firstString(raw, ["delta", "text", "content", "summary"])
|
|
4273
|
+
?? (options?.partBoundary ? "\n" : undefined);
|
|
4274
|
+
if (!delta) return;
|
|
4275
|
+
const text = `${existing?.text ?? ""}${delta}`;
|
|
4276
|
+
const item: AgentTimelineItem = {
|
|
4277
|
+
id: itemId,
|
|
4278
|
+
conversationId,
|
|
4279
|
+
type: "status",
|
|
4280
|
+
kind: "thinking",
|
|
4281
|
+
role: "system",
|
|
4282
|
+
turnId: this.extractTurnId(raw) ?? this.currentTurnIds.get(conversationId),
|
|
4283
|
+
itemId,
|
|
4284
|
+
content: [{ type: "text", text }],
|
|
4285
|
+
text,
|
|
4286
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
4287
|
+
updatedAt: Date.now(),
|
|
4288
|
+
isStreaming: true,
|
|
4289
|
+
};
|
|
4290
|
+
this.applyStreamingTextDelta(conversationId, item, existing, delta);
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
private applyStreamingTextDelta(
|
|
4294
|
+
conversationId: string,
|
|
4295
|
+
item: AgentTimelineItem,
|
|
4296
|
+
existing: AgentTimelineItem | undefined,
|
|
4297
|
+
delta: string,
|
|
4298
|
+
): void {
|
|
4299
|
+
if (!existing) {
|
|
4300
|
+
this.upsertItem(conversationId, item);
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
this.rememberItemConversationId(conversationId, item);
|
|
4304
|
+
const timeline = this.timelines.get(conversationId) ?? [];
|
|
4305
|
+
const index = timeline.findIndex((entry) => entry.id === item.id);
|
|
4306
|
+
if (index >= 0) timeline[index] = item;
|
|
4307
|
+
else timeline.push(item);
|
|
4308
|
+
this.timelines.set(conversationId, timeline);
|
|
4309
|
+
this.input.send(createEnvelope({
|
|
4310
|
+
type: "agent.v2.event",
|
|
4311
|
+
sessionId: this.input.sessionId,
|
|
4312
|
+
payload: {
|
|
4313
|
+
conversationId,
|
|
4314
|
+
patch: {
|
|
4315
|
+
itemId: item.id,
|
|
4316
|
+
textDelta: delta,
|
|
4317
|
+
updatedAt: item.updatedAt,
|
|
4318
|
+
},
|
|
4319
|
+
},
|
|
4320
|
+
}));
|
|
4321
|
+
}
|
|
4322
|
+
|
|
3707
4323
|
private handlePlanUpdated(params: unknown): void {
|
|
3708
4324
|
const raw = asRecord(params);
|
|
3709
4325
|
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
@@ -4174,20 +4790,14 @@ export class AgentWorkspaceProxy {
|
|
|
4174
4790
|
const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
|
|
4175
4791
|
const rawToolCall = asRecord(raw.toolCall) ?? raw;
|
|
4176
4792
|
// Codex app-server command/file approvals carry `command`/`reason`/`cwd`
|
|
4177
|
-
// (not toolCall/context).
|
|
4178
|
-
//
|
|
4179
|
-
const codexCommand = firstString(raw, ["command"]);
|
|
4180
|
-
const codexCwd = firstString(raw, ["cwd"]);
|
|
4793
|
+
// (not toolCall/context). Keep those fields as JSON so the web card can
|
|
4794
|
+
// render `$ command` / a path instead of a raw dump.
|
|
4181
4795
|
const permission: AgentPermission = {
|
|
4182
4796
|
requestId,
|
|
4183
|
-
toolName:
|
|
4184
|
-
|
|
4185
|
-
(codexCommand ? "shell" : undefined),
|
|
4186
|
-
toolInput: codexCommand
|
|
4187
|
-
? (codexCwd ? `$ ${codexCommand}\n# cwd: ${codexCwd}` : `$ ${codexCommand}`)
|
|
4188
|
-
: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
|
|
4797
|
+
toolName: permissionToolName(raw, rawToolCall),
|
|
4798
|
+
toolInput: permissionToolInput(raw, rawToolCall),
|
|
4189
4799
|
context: firstString(raw, ["context", "description", "message", "title", "reason"]),
|
|
4190
|
-
options:
|
|
4800
|
+
options: permissionOptionsFromRequest(raw, source),
|
|
4191
4801
|
};
|
|
4192
4802
|
this.pendingPermissions.set(requestId, permission);
|
|
4193
4803
|
if (source) this.permissionSources.set(requestId, source);
|
|
@@ -4499,6 +5109,23 @@ export class AgentWorkspaceProxy {
|
|
|
4499
5109
|
}));
|
|
4500
5110
|
}
|
|
4501
5111
|
|
|
5112
|
+
private forgetConversationLocally(conversationId: string): void {
|
|
5113
|
+
const conversation = this.conversations.get(conversationId);
|
|
5114
|
+
if (conversation?.agentSessionId) {
|
|
5115
|
+
this.deletedAgentSessionIds.add(conversation.agentSessionId);
|
|
5116
|
+
this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
5117
|
+
}
|
|
5118
|
+
this.conversations.delete(conversationId);
|
|
5119
|
+
this.timelines.delete(conversationId);
|
|
5120
|
+
this.historyCursors.delete(conversationId);
|
|
5121
|
+
this.lastHydrateAt.delete(conversationId);
|
|
5122
|
+
this.input.send(createEnvelope({
|
|
5123
|
+
type: "agent.v2.conversation.deleted",
|
|
5124
|
+
sessionId: this.input.sessionId,
|
|
5125
|
+
payload: { conversationId },
|
|
5126
|
+
}));
|
|
5127
|
+
}
|
|
5128
|
+
|
|
4502
5129
|
private emitConversation(conversation: AgentConversation): void {
|
|
4503
5130
|
this.input.send(createEnvelope({
|
|
4504
5131
|
type: "agent.v2.event",
|
|
@@ -4886,6 +5513,84 @@ export class AgentWorkspaceProxy {
|
|
|
4886
5513
|
}
|
|
4887
5514
|
}
|
|
4888
5515
|
|
|
5516
|
+
function permissionToolName(
|
|
5517
|
+
raw: Record<string, unknown>,
|
|
5518
|
+
rawToolCall: Record<string, unknown>,
|
|
5519
|
+
): string | undefined {
|
|
5520
|
+
const named = firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]);
|
|
5521
|
+
if (named) return named;
|
|
5522
|
+
if (firstString(raw, ["command"])) return "shell";
|
|
5523
|
+
if (permissionFilePath(raw)) return "Edit";
|
|
5524
|
+
return undefined;
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
function permissionFilePath(raw: Record<string, unknown>): string | undefined {
|
|
5528
|
+
const direct = firstString(raw, ["path", "filePath", "file_path"]);
|
|
5529
|
+
if (direct) return direct;
|
|
5530
|
+
const changes = Array.isArray(raw.changes) ? raw.changes : undefined;
|
|
5531
|
+
const first = changes ? asRecord(changes[0]) : undefined;
|
|
5532
|
+
return firstString(first, ["path", "filePath", "file_path"]);
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
function permissionToolInput(
|
|
5536
|
+
raw: Record<string, unknown>,
|
|
5537
|
+
rawToolCall: Record<string, unknown>,
|
|
5538
|
+
): string {
|
|
5539
|
+
const command = firstString(raw, ["command"]);
|
|
5540
|
+
const cwd = firstString(raw, ["cwd"]);
|
|
5541
|
+
const path = permissionFilePath(raw);
|
|
5542
|
+
if (command || path) {
|
|
5543
|
+
return JSON.stringify({
|
|
5544
|
+
...(command ? { command } : {}),
|
|
5545
|
+
...(cwd ? { cwd } : {}),
|
|
5546
|
+
...(path ? { path } : {}),
|
|
5547
|
+
});
|
|
5548
|
+
}
|
|
5549
|
+
return stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall);
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
function officialDecisionKey(value: unknown): string | undefined {
|
|
5553
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
5554
|
+
const raw = asRecord(value);
|
|
5555
|
+
return firstString(raw, ["decision", "id", "kind", "type", "optionId"]);
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
function officialDecisionOption(key: string): AgentPermission["options"][number] | undefined {
|
|
5559
|
+
const normalized = key.replace(/[\s_-]/g, "").toLowerCase();
|
|
5560
|
+
if (normalized === "accept" || normalized === "allow" || normalized === "allowonce") {
|
|
5561
|
+
return { id: key === "accept" ? "accept" : key, label: "允许一次", kind: "allow" };
|
|
5562
|
+
}
|
|
5563
|
+
if (
|
|
5564
|
+
normalized === "acceptforsession" ||
|
|
5565
|
+
normalized === "allowsession" ||
|
|
5566
|
+
normalized === "allowforsession" ||
|
|
5567
|
+
normalized === "session"
|
|
5568
|
+
) {
|
|
5569
|
+
return { id: "acceptForSession", label: "本会话允许", kind: "allow" };
|
|
5570
|
+
}
|
|
5571
|
+
if (normalized === "decline" || normalized === "deny" || normalized === "reject") {
|
|
5572
|
+
return { id: normalized === "decline" ? "decline" : key, label: "拒绝", kind: "deny" };
|
|
5573
|
+
}
|
|
5574
|
+
if (normalized === "cancel") {
|
|
5575
|
+
return { id: "cancel", label: "取消", kind: "deny" };
|
|
5576
|
+
}
|
|
5577
|
+
return undefined;
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
function parseOfficialDecisions(value: unknown[]): AgentPermission["options"] {
|
|
5581
|
+
const options: AgentPermission["options"] = [];
|
|
5582
|
+
const seen = new Set<string>();
|
|
5583
|
+
for (const entry of value) {
|
|
5584
|
+
const key = officialDecisionKey(entry);
|
|
5585
|
+
if (!key) continue;
|
|
5586
|
+
const mapped = officialDecisionOption(key);
|
|
5587
|
+
if (!mapped || seen.has(mapped.id)) continue;
|
|
5588
|
+
seen.add(mapped.id);
|
|
5589
|
+
options.push(mapped);
|
|
5590
|
+
}
|
|
5591
|
+
return options;
|
|
5592
|
+
}
|
|
5593
|
+
|
|
4889
5594
|
function parsePermissionOptions(value: unknown): AgentPermission["options"] {
|
|
4890
5595
|
if (!Array.isArray(value)) {
|
|
4891
5596
|
return [
|
|
@@ -4894,19 +5599,27 @@ function parsePermissionOptions(value: unknown): AgentPermission["options"] {
|
|
|
4894
5599
|
];
|
|
4895
5600
|
}
|
|
4896
5601
|
|
|
5602
|
+
const official = parseOfficialDecisions(value);
|
|
5603
|
+
if (official.length > 0 && value.every((entry) => officialDecisionOption(officialDecisionKey(entry) ?? ""))) {
|
|
5604
|
+
return official;
|
|
5605
|
+
}
|
|
5606
|
+
|
|
4897
5607
|
const options = value
|
|
4898
5608
|
.map((entry, index) => {
|
|
4899
5609
|
const raw = asRecord(entry) ?? {};
|
|
4900
|
-
const idValue = raw.optionId ?? raw.id ?? raw.kind ?? `option-${index + 1}`;
|
|
4901
|
-
const labelValue = raw.name ?? raw.label ?? raw.kind ?? String(idValue);
|
|
5610
|
+
const idValue = raw.optionId ?? raw.id ?? raw.kind ?? raw.decision ?? `option-${index + 1}`;
|
|
5611
|
+
const labelValue = raw.name ?? raw.label ?? raw.kind ?? raw.decision ?? String(idValue);
|
|
4902
5612
|
const id = String(idValue);
|
|
4903
5613
|
const label = String(labelValue);
|
|
5614
|
+
const official = officialDecisionOption(id);
|
|
5615
|
+
if (official) return { ...official, id, label: official.label === "允许一次" && label !== id ? label : official.label };
|
|
4904
5616
|
const normalized = `${id} ${label}`.toLowerCase();
|
|
4905
|
-
const kind: AgentPermission["options"][number]["kind"] =
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
5617
|
+
const kind: AgentPermission["options"][number]["kind"] =
|
|
5618
|
+
/reject|deny|decline|cancel/.test(normalized)
|
|
5619
|
+
? "deny"
|
|
5620
|
+
: /allow|accept/.test(normalized)
|
|
5621
|
+
? "allow"
|
|
5622
|
+
: "other";
|
|
4910
5623
|
return { id, label, kind };
|
|
4911
5624
|
})
|
|
4912
5625
|
.filter((option) => option.id.length > 0 && option.label.length > 0);
|
|
@@ -4919,6 +5632,38 @@ function parsePermissionOptions(value: unknown): AgentPermission["options"] {
|
|
|
4919
5632
|
];
|
|
4920
5633
|
}
|
|
4921
5634
|
|
|
5635
|
+
function permissionOptionsFromRequest(
|
|
5636
|
+
raw: Record<string, unknown>,
|
|
5637
|
+
source?: string,
|
|
5638
|
+
): AgentPermission["options"] {
|
|
5639
|
+
const decisions = raw.availableDecisions;
|
|
5640
|
+
if (Array.isArray(decisions) && decisions.length > 0) {
|
|
5641
|
+
const official = parseOfficialDecisions(decisions);
|
|
5642
|
+
if (official.length > 0) return official;
|
|
5643
|
+
}
|
|
5644
|
+
if (Array.isArray(raw.options)) {
|
|
5645
|
+
return parsePermissionOptions(raw.options);
|
|
5646
|
+
}
|
|
5647
|
+
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
5648
|
+
return [
|
|
5649
|
+
{ id: "accept", label: "允许一次", kind: "allow" },
|
|
5650
|
+
{ id: "acceptForSession", label: "本会话允许", kind: "allow" },
|
|
5651
|
+
{ id: "decline", label: "拒绝", kind: "deny" },
|
|
5652
|
+
];
|
|
5653
|
+
}
|
|
5654
|
+
if (source === "item/permissions/requestApproval") {
|
|
5655
|
+
return [
|
|
5656
|
+
{ id: "turn", label: "本次允许", kind: "allow" },
|
|
5657
|
+
{ id: "session", label: "本会话允许", kind: "allow" },
|
|
5658
|
+
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
5659
|
+
];
|
|
5660
|
+
}
|
|
5661
|
+
return [
|
|
5662
|
+
{ id: "allow", label: "允许", kind: "allow" },
|
|
5663
|
+
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
5664
|
+
];
|
|
5665
|
+
}
|
|
5666
|
+
|
|
4922
5667
|
function selectPermissionOption(
|
|
4923
5668
|
permission: AgentPermission | undefined,
|
|
4924
5669
|
outcome: "allow" | "deny" | "cancelled",
|
|
@@ -5013,7 +5758,10 @@ function formatPermissionResponse(
|
|
|
5013
5758
|
optionId: string,
|
|
5014
5759
|
): unknown {
|
|
5015
5760
|
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
5016
|
-
|
|
5761
|
+
if (outcome === "cancelled" || optionId === "cancel") return { decision: "cancel" };
|
|
5762
|
+
if (outcome === "deny") return { decision: "decline" };
|
|
5763
|
+
if (optionId === "acceptForSession") return { decision: "acceptForSession" };
|
|
5764
|
+
return { decision: "accept" };
|
|
5017
5765
|
}
|
|
5018
5766
|
if (source === "item/permissions/requestApproval") {
|
|
5019
5767
|
if (outcome === "allow") {
|