linkshell-cli 0.5.34 → 0.5.36
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 +49 -18
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/hook-command.d.ts +19 -0
- package/dist/cli/src/runtime/hook-command.js +31 -0
- package/dist/cli/src/runtime/hook-command.js.map +1 -0
- 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 +46 -18
- package/src/runtime/hook-command.ts +35 -0
- package/web/assets/{FileBrowser-9wRS6a0E.js → FileBrowser-Coy7Szjx.js} +1 -1
- package/web/assets/{PortPreview-yvXMn63g.js → PortPreview-D-XovbV1.js} +1 -1
- package/web/assets/{TerminalPanel-xtGQI_fv.js → TerminalPanel-Cah56AtC.js} +1 -1
- package/web/assets/{UsageDashboard-ByQ6mehF.js → UsageDashboard-DfkjJagC.js} +1 -1
- package/web/assets/index-BsnDaeCt.css +33 -0
- package/web/assets/index-ClZzQ4ba.js +35 -0
- package/web/assets/{vendor-highlight-Cus7zBI4.js → vendor-highlight-NXWAAyIv.js} +1 -1
- package/web/assets/{vendor-react-0zU5oKmd.js → vendor-react-M7xG3qiw.js} +8 -8
- package/web/index.html +4 -4
- package/web/assets/index-Vtyd7s5T.css +0 -33
- package/web/assets/index-pqslvKcS.js +0 -35
|
@@ -7,6 +7,7 @@ import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
|
7
7
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
8
8
|
import { aggregateUsage } from "../usage-report.js";
|
|
9
9
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
10
|
+
import { agentProviderLabel, discoverAgentProcessesAsync, isLiveConversationId, isProtocolAgentProvider, liveConversationId, } from "./agent-process-discovery.js";
|
|
10
11
|
// Normalize the many status spellings Claude/Codex emit into our enum. Unknown
|
|
11
12
|
// values fall back to "pending" (known-but-not-yet-up) rather than dropping the
|
|
12
13
|
// server. Kept liberal because the real event vocabulary isn't fully pinned.
|
|
@@ -24,6 +25,19 @@ function normalizeMcpStatus(raw) {
|
|
|
24
25
|
return "disabled";
|
|
25
26
|
return "pending";
|
|
26
27
|
}
|
|
28
|
+
function mcpStatusFromRecord(rec) {
|
|
29
|
+
if (normalizeMcpStatus(rec.authStatus ?? rec.auth) === "needs_auth")
|
|
30
|
+
return "needs_auth";
|
|
31
|
+
return normalizeMcpStatus(rec.status ?? rec.state ?? rec.authStatus);
|
|
32
|
+
}
|
|
33
|
+
function mcpListFromProviderResult(value) {
|
|
34
|
+
const raw = asRecord(value);
|
|
35
|
+
const list = Array.isArray(value) ? value :
|
|
36
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
37
|
+
Array.isArray(raw?.servers) ? raw.servers :
|
|
38
|
+
value;
|
|
39
|
+
return parseMcpServerList(list);
|
|
40
|
+
}
|
|
27
41
|
// Parse an MCP server list from either an array of {name,status,...} objects or
|
|
28
42
|
// a {name: status} record — Claude's init `mcp_servers` shape isn't fixed.
|
|
29
43
|
function parseMcpServerList(value) {
|
|
@@ -37,7 +51,7 @@ function parseMcpServerList(value) {
|
|
|
37
51
|
continue;
|
|
38
52
|
out.push({
|
|
39
53
|
name,
|
|
40
|
-
status:
|
|
54
|
+
status: mcpStatusFromRecord(rec),
|
|
41
55
|
error: firstString(rec, ["error", "message", "reason"]),
|
|
42
56
|
toolCount: firstNumber(rec, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(rec.tools) ? rec.tools.length : undefined),
|
|
43
57
|
});
|
|
@@ -56,7 +70,7 @@ function parseMcpServerList(value) {
|
|
|
56
70
|
const inner = asRecord(raw);
|
|
57
71
|
out.push({
|
|
58
72
|
name,
|
|
59
|
-
status:
|
|
73
|
+
status: inner ? mcpStatusFromRecord(inner) : normalizeMcpStatus(raw),
|
|
60
74
|
error: inner ? firstString(inner, ["error", "message", "reason"]) : undefined,
|
|
61
75
|
toolCount: inner ? (firstNumber(inner, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(inner.tools) ? inner.tools.length : undefined)) : undefined,
|
|
62
76
|
});
|
|
@@ -232,6 +246,47 @@ function normalizePlanStatus(value) {
|
|
|
232
246
|
return "in_progress";
|
|
233
247
|
return "pending";
|
|
234
248
|
}
|
|
249
|
+
function applyThreadSettingsToConversation(conversation, value) {
|
|
250
|
+
const raw = asRecord(value);
|
|
251
|
+
const settings = asRecord(raw?.threadSettings) ?? asRecord(raw?.settings) ?? raw;
|
|
252
|
+
if (!settings)
|
|
253
|
+
return;
|
|
254
|
+
const model = firstString(settings, ["model", "modelId", "currentModel"]);
|
|
255
|
+
if (model)
|
|
256
|
+
conversation.model = model;
|
|
257
|
+
const effort = firstString(settings, ["effort", "reasoningEffort", "reasoning_effort"])
|
|
258
|
+
?? firstString(asRecord(asRecord(settings.collaborationMode ?? settings.collaboration_mode)?.settings), ["reasoning_effort", "reasoningEffort"]);
|
|
259
|
+
if (effort)
|
|
260
|
+
conversation.reasoningEffort = effort;
|
|
261
|
+
const collab = asRecord(settings.collaborationMode ?? settings.collaboration_mode);
|
|
262
|
+
const mode = typeof settings.collaborationMode === "string"
|
|
263
|
+
? settings.collaborationMode
|
|
264
|
+
: typeof settings.collaboration_mode === "string"
|
|
265
|
+
? settings.collaboration_mode
|
|
266
|
+
: firstString(collab, ["mode"]);
|
|
267
|
+
if (mode === "plan")
|
|
268
|
+
conversation.collaborationMode = "plan";
|
|
269
|
+
else if (mode === "default")
|
|
270
|
+
conversation.collaborationMode = "default";
|
|
271
|
+
const permission = firstString(settings, ["permissionMode", "permission_mode"]);
|
|
272
|
+
if (permission === "read_only" || permission === "workspace_write" || permission === "full_access") {
|
|
273
|
+
conversation.permissionMode = permission;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const sandboxValue = settings.sandboxPolicy ?? settings.sandbox_policy;
|
|
277
|
+
const sandbox = asRecord(sandboxValue);
|
|
278
|
+
const sandboxType = typeof sandboxValue === "string" ? sandboxValue : firstString(sandbox, ["type"]);
|
|
279
|
+
if (!sandboxType)
|
|
280
|
+
return;
|
|
281
|
+
const norm = sandboxType.replace(/-/g, "").toLowerCase();
|
|
282
|
+
conversation.permissionMode = norm.includes("readonly")
|
|
283
|
+
? "read_only"
|
|
284
|
+
: norm.includes("workspace")
|
|
285
|
+
? "workspace_write"
|
|
286
|
+
: norm.includes("danger") || norm.includes("full")
|
|
287
|
+
? "full_access"
|
|
288
|
+
: conversation.permissionMode;
|
|
289
|
+
}
|
|
235
290
|
function agentStatusFromThreadStatus(value) {
|
|
236
291
|
const raw = asRecord(value);
|
|
237
292
|
const status = typeof value === "string"
|
|
@@ -697,11 +752,7 @@ function previewText(text) {
|
|
|
697
752
|
return text.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
698
753
|
}
|
|
699
754
|
function providerLabel(provider) {
|
|
700
|
-
|
|
701
|
-
return "Codex";
|
|
702
|
-
if (provider === "claude")
|
|
703
|
-
return "Claude";
|
|
704
|
-
return "Custom";
|
|
755
|
+
return agentProviderLabel(provider);
|
|
705
756
|
}
|
|
706
757
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
707
758
|
const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
|
|
@@ -1058,6 +1109,28 @@ function mergeCommands(...groups) {
|
|
|
1058
1109
|
}
|
|
1059
1110
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1060
1111
|
}
|
|
1112
|
+
function parseSkillCommands(provider, value) {
|
|
1113
|
+
const raw = asRecord(value);
|
|
1114
|
+
const groups = Array.isArray(value) ? value :
|
|
1115
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1116
|
+
Array.isArray(raw?.skills) ? [raw] :
|
|
1117
|
+
[];
|
|
1118
|
+
const skills = [];
|
|
1119
|
+
for (const group of groups) {
|
|
1120
|
+
if (typeof group === "string" && group) {
|
|
1121
|
+
skills.push(group);
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const rec = asRecord(group);
|
|
1125
|
+
if (!rec)
|
|
1126
|
+
continue;
|
|
1127
|
+
if (Array.isArray(rec.skills))
|
|
1128
|
+
skills.push(...rec.skills);
|
|
1129
|
+
else if (firstString(rec, ["name", "command", "id"]))
|
|
1130
|
+
skills.push(group);
|
|
1131
|
+
}
|
|
1132
|
+
return runtimeCommands(provider, skills);
|
|
1133
|
+
}
|
|
1061
1134
|
function runtimeCommands(provider, value) {
|
|
1062
1135
|
const raw = asRecord(value);
|
|
1063
1136
|
const commandsValue = Array.isArray(value) ? value :
|
|
@@ -1390,11 +1463,11 @@ function parseRemoteSessions(value) {
|
|
|
1390
1463
|
const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
|
|
1391
1464
|
if (!id)
|
|
1392
1465
|
continue;
|
|
1393
|
-
//
|
|
1394
|
-
// (
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
const status =
|
|
1466
|
+
// Claude reports "running"/"idle" strings; Codex thread/list uses a tagged
|
|
1467
|
+
// object ({ type: "active"|"idle"|"notLoaded"|"systemError" }). Both map
|
|
1468
|
+
// through the same helper. Unknown / notLoaded stay undefined so the sync
|
|
1469
|
+
// site keeps its own status.
|
|
1470
|
+
const status = agentStatusFromThreadStatus(source.status);
|
|
1398
1471
|
result.push({
|
|
1399
1472
|
id,
|
|
1400
1473
|
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
@@ -1679,10 +1752,9 @@ export class AgentWorkspaceProxy {
|
|
|
1679
1752
|
conversations = new Map();
|
|
1680
1753
|
conversationByAgentSessionId = new Map();
|
|
1681
1754
|
timelines = new Map();
|
|
1682
|
-
// Conversations the user
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
// forgotten conversation stays gone for the life of this workspace process.
|
|
1755
|
+
// Conversations the user forgot from the LinkShell list. Tombstone by
|
|
1756
|
+
// agentSessionId so syncProviderSessions cannot resurrect them. This is
|
|
1757
|
+
// NOT Codex thread/delete (that hard-deletes the on-disk rollout).
|
|
1686
1758
|
deletedAgentSessionIds = new Set();
|
|
1687
1759
|
// Opaque codex turns cursor per conversation, pointing at OLDER history to
|
|
1688
1760
|
// page through (captured at hydration, advanced on each history.request).
|
|
@@ -1695,6 +1767,12 @@ export class AgentWorkspaceProxy {
|
|
|
1695
1767
|
structuredInputWaiters = new Map();
|
|
1696
1768
|
itemConversationIds = new Map();
|
|
1697
1769
|
toolConversationIds = new Map();
|
|
1770
|
+
processPollTimer;
|
|
1771
|
+
processPollInFlight = false;
|
|
1772
|
+
processMissCounts = new Map();
|
|
1773
|
+
lastDiscoveredProviders = new Set();
|
|
1774
|
+
static PROCESS_POLL_MS = 3_000;
|
|
1775
|
+
static PROCESS_MISS_LIMIT = 2;
|
|
1698
1776
|
constructor(input) {
|
|
1699
1777
|
this.input = input;
|
|
1700
1778
|
}
|
|
@@ -1715,36 +1793,89 @@ export class AgentWorkspaceProxy {
|
|
|
1715
1793
|
const conversation = this.conversations.get(payload.conversationId);
|
|
1716
1794
|
if (!conversation)
|
|
1717
1795
|
break;
|
|
1796
|
+
const raw = asRecord(envelope.payload) ?? {};
|
|
1797
|
+
const model = payload.model ?? (typeof raw.model === "string" ? raw.model : undefined);
|
|
1798
|
+
const reasoningEffort = payload.reasoningEffort
|
|
1799
|
+
?? (typeof raw.reasoningEffort === "string" ? raw.reasoningEffort : undefined);
|
|
1800
|
+
const permissionMode = payload.permissionMode
|
|
1801
|
+
?? (raw.permissionMode === "read_only" || raw.permissionMode === "workspace_write" || raw.permissionMode === "full_access"
|
|
1802
|
+
? raw.permissionMode
|
|
1803
|
+
: undefined);
|
|
1804
|
+
const collaborationMode = payload.collaborationMode
|
|
1805
|
+
?? (raw.collaborationMode === "default" || raw.collaborationMode === "plan"
|
|
1806
|
+
? raw.collaborationMode
|
|
1807
|
+
: undefined);
|
|
1718
1808
|
if (payload.title !== undefined) {
|
|
1719
1809
|
const trimmed = payload.title.trim();
|
|
1720
1810
|
conversation.title = trimmed === "" ? undefined : trimmed;
|
|
1721
1811
|
}
|
|
1722
1812
|
if (payload.archived !== undefined)
|
|
1723
1813
|
conversation.archived = payload.archived;
|
|
1814
|
+
if (model !== undefined) {
|
|
1815
|
+
const trimmed = model.trim();
|
|
1816
|
+
if (trimmed)
|
|
1817
|
+
conversation.model = trimmed;
|
|
1818
|
+
}
|
|
1819
|
+
if (reasoningEffort !== undefined)
|
|
1820
|
+
conversation.reasoningEffort = reasoningEffort;
|
|
1821
|
+
if (permissionMode !== undefined)
|
|
1822
|
+
conversation.permissionMode = permissionMode;
|
|
1823
|
+
if (collaborationMode !== undefined)
|
|
1824
|
+
conversation.collaborationMode = collaborationMode;
|
|
1724
1825
|
conversation.lastActivityAt = Date.now();
|
|
1725
1826
|
// Echo the updated record so every client refreshes (the web store
|
|
1726
1827
|
// force-replaces by id on any event carrying `conversation`).
|
|
1727
1828
|
this.emitConversation(conversation);
|
|
1829
|
+
if (conversation.provider === "codex" && conversation.agentSessionId) {
|
|
1830
|
+
const client = this.clientForProvider(conversation.provider);
|
|
1831
|
+
const settingsChanged = model !== undefined ||
|
|
1832
|
+
reasoningEffort !== undefined ||
|
|
1833
|
+
permissionMode !== undefined ||
|
|
1834
|
+
collaborationMode !== undefined;
|
|
1835
|
+
try {
|
|
1836
|
+
const acp = client;
|
|
1837
|
+
if (payload.title !== undefined && typeof acp?.setThreadName === "function") {
|
|
1838
|
+
await acp.setThreadName({
|
|
1839
|
+
sessionId: conversation.agentSessionId,
|
|
1840
|
+
name: conversation.title ?? "",
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
if (payload.archived === true && typeof acp?.archiveThread === "function") {
|
|
1844
|
+
await acp.archiveThread({ sessionId: conversation.agentSessionId });
|
|
1845
|
+
}
|
|
1846
|
+
else if (payload.archived === false && typeof acp?.unarchiveThread === "function") {
|
|
1847
|
+
await acp.unarchiveThread({ sessionId: conversation.agentSessionId });
|
|
1848
|
+
}
|
|
1849
|
+
if (settingsChanged && typeof acp?.updateThreadSettings === "function") {
|
|
1850
|
+
await acp.updateThreadSettings({
|
|
1851
|
+
sessionId: conversation.agentSessionId,
|
|
1852
|
+
model,
|
|
1853
|
+
reasoningEffort,
|
|
1854
|
+
permissionMode,
|
|
1855
|
+
collaborationMode,
|
|
1856
|
+
cwd: conversation.cwd,
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
catch (error) {
|
|
1861
|
+
if (this.input.verbose) {
|
|
1862
|
+
process.stderr.write(`[agent:v2] conversation update provider call failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1728
1866
|
break;
|
|
1729
1867
|
}
|
|
1730
1868
|
case "agent.v2.conversation.delete": {
|
|
1731
1869
|
const payload = parseTypedPayload("agent.v2.conversation.delete", envelope.payload);
|
|
1732
|
-
|
|
1733
|
-
//
|
|
1734
|
-
//
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
this.
|
|
1741
|
-
this.timelines.delete(payload.conversationId);
|
|
1742
|
-
this.historyCursors.delete(payload.conversationId);
|
|
1743
|
-
this.input.send(createEnvelope({
|
|
1744
|
-
type: "agent.v2.conversation.deleted",
|
|
1745
|
-
sessionId: this.input.sessionId,
|
|
1746
|
-
payload: { conversationId: payload.conversationId },
|
|
1747
|
-
}));
|
|
1870
|
+
// Forget from LinkShell only. Never call Codex thread/delete here —
|
|
1871
|
+
// that hard-deletes the on-disk rollout. Archive is the destructive-ish
|
|
1872
|
+
// user action; this menu item is "hide from the tree".
|
|
1873
|
+
this.forgetConversationLocally(payload.conversationId);
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1876
|
+
case "agent.v2.mcp.login": {
|
|
1877
|
+
const payload = parseTypedPayload("agent.v2.mcp.login", envelope.payload);
|
|
1878
|
+
await this.startMcpLogin(payload);
|
|
1748
1879
|
break;
|
|
1749
1880
|
}
|
|
1750
1881
|
case "agent.v2.conversation.list": {
|
|
@@ -1817,6 +1948,10 @@ export class AgentWorkspaceProxy {
|
|
|
1817
1948
|
}
|
|
1818
1949
|
}
|
|
1819
1950
|
stop() {
|
|
1951
|
+
if (this.processPollTimer) {
|
|
1952
|
+
clearInterval(this.processPollTimer);
|
|
1953
|
+
this.processPollTimer = undefined;
|
|
1954
|
+
}
|
|
1820
1955
|
for (const watcher of this.fsWatchers) {
|
|
1821
1956
|
try {
|
|
1822
1957
|
watcher.close();
|
|
@@ -1847,7 +1982,9 @@ export class AgentWorkspaceProxy {
|
|
|
1847
1982
|
fsSyncTimer;
|
|
1848
1983
|
fsSyncRunning = false;
|
|
1849
1984
|
lastPushedConversationSignature;
|
|
1985
|
+
lastHydrateAt = new Map();
|
|
1850
1986
|
static FS_SYNC_THROTTLE_MS = 1500;
|
|
1987
|
+
static EXTERNAL_HYDRATE_THROTTLE_MS = 2000;
|
|
1851
1988
|
startSessionWatchers() {
|
|
1852
1989
|
const roots = [
|
|
1853
1990
|
join(homedir(), ".claude", "projects"),
|
|
@@ -1901,17 +2038,40 @@ export class AgentWorkspaceProxy {
|
|
|
1901
2038
|
// and client re-renders. A cheap signature over the render-relevant fields
|
|
1902
2039
|
// gates the push.
|
|
1903
2040
|
const signature = this.conversationListSignature(conversations);
|
|
1904
|
-
if (signature
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
}
|
|
2041
|
+
if (signature !== this.lastPushedConversationSignature) {
|
|
2042
|
+
this.lastPushedConversationSignature = signature;
|
|
2043
|
+
// Push the conversation list (statuses) — lighter than a full snapshot and
|
|
2044
|
+
// exactly what refreshes the tree. The web handles an unsolicited
|
|
2045
|
+
// conversation.list.result the same as a requested one.
|
|
2046
|
+
this.input.send(createEnvelope({
|
|
2047
|
+
type: "agent.v2.conversation.list.result",
|
|
2048
|
+
sessionId: this.input.sessionId,
|
|
2049
|
+
payload: { conversations },
|
|
2050
|
+
}));
|
|
2051
|
+
}
|
|
2052
|
+
// External sessions (status running / waiting_permission but LinkShell is
|
|
2053
|
+
// not driving the turn) have no live event stream on this app-server.
|
|
2054
|
+
// Re-hydrate on a short throttle so the Web timeline still moves.
|
|
2055
|
+
const now = Date.now();
|
|
2056
|
+
for (const conversation of conversations) {
|
|
2057
|
+
if (conversation.status !== "running" && conversation.status !== "waiting_permission")
|
|
2058
|
+
continue;
|
|
2059
|
+
if (this.currentTurnIds.has(conversation.id))
|
|
2060
|
+
continue;
|
|
2061
|
+
const last = this.lastHydrateAt.get(conversation.id) ?? 0;
|
|
2062
|
+
if (now - last < AgentWorkspaceProxy.EXTERNAL_HYDRATE_THROTTLE_MS)
|
|
2063
|
+
continue;
|
|
2064
|
+
this.lastHydrateAt.set(conversation.id, now);
|
|
2065
|
+
try {
|
|
2066
|
+
await this.hydrateConversationFromProvider(conversation, undefined, { rememberActiveTurn: false });
|
|
2067
|
+
this.sendSnapshot(conversation.id);
|
|
2068
|
+
}
|
|
2069
|
+
catch (error) {
|
|
2070
|
+
if (this.input.verbose) {
|
|
2071
|
+
process.stderr.write(`[agent:v2] external hydrate failed for ${conversation.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
1915
2075
|
}
|
|
1916
2076
|
catch (error) {
|
|
1917
2077
|
if (this.input.verbose) {
|
|
@@ -1929,7 +2089,7 @@ export class AgentWorkspaceProxy {
|
|
|
1929
2089
|
* membership are what matter. */
|
|
1930
2090
|
conversationListSignature(conversations) {
|
|
1931
2091
|
return conversations
|
|
1932
|
-
.map((c) => `${c.id}${c.status}${c.title ?? ""}${c.model ?? ""}${c.provider ?? ""}`)
|
|
2092
|
+
.map((c) => `${c.id}${c.status}${c.title ?? ""}${c.model ?? ""}${c.provider ?? ""}${c.lastMessagePreview ?? ""}`)
|
|
1933
2093
|
.sort()
|
|
1934
2094
|
.join("");
|
|
1935
2095
|
}
|
|
@@ -1981,6 +2141,160 @@ export class AgentWorkspaceProxy {
|
|
|
1981
2141
|
// Start watching the on-disk session stores so externally-started sessions
|
|
1982
2142
|
// and status changes surface live, not just at connect time.
|
|
1983
2143
|
this.startSessionWatchers();
|
|
2144
|
+
this.startProcessDiscovery();
|
|
2145
|
+
}
|
|
2146
|
+
startProcessDiscovery() {
|
|
2147
|
+
void this.pollDiscoveredProcesses();
|
|
2148
|
+
if (this.processPollTimer)
|
|
2149
|
+
return;
|
|
2150
|
+
this.processPollTimer = setInterval(() => {
|
|
2151
|
+
void this.pollDiscoveredProcesses();
|
|
2152
|
+
}, AgentWorkspaceProxy.PROCESS_POLL_MS);
|
|
2153
|
+
this.processPollTimer.unref?.();
|
|
2154
|
+
}
|
|
2155
|
+
async pollDiscoveredProcesses() {
|
|
2156
|
+
if (this.processPollInFlight)
|
|
2157
|
+
return;
|
|
2158
|
+
this.processPollInFlight = true;
|
|
2159
|
+
try {
|
|
2160
|
+
const snapshots = this.input.discoverProcesses
|
|
2161
|
+
? this.input.discoverProcesses()
|
|
2162
|
+
: await discoverAgentProcessesAsync();
|
|
2163
|
+
this.applyDiscoveredProcesses(snapshots);
|
|
2164
|
+
}
|
|
2165
|
+
catch {
|
|
2166
|
+
// A failed ps scan must not tear down the workspace or hitch the PTY.
|
|
2167
|
+
}
|
|
2168
|
+
finally {
|
|
2169
|
+
this.processPollInFlight = false;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
/** Open Island rule: a visible agent session is a live process. Two missed
|
|
2173
|
+
* polls (~6s) drop the running signal so a killed CLI does not linger. */
|
|
2174
|
+
reconcileDiscoveredProcesses() {
|
|
2175
|
+
let snapshots = [];
|
|
2176
|
+
try {
|
|
2177
|
+
snapshots = this.input.discoverProcesses?.() ?? [];
|
|
2178
|
+
}
|
|
2179
|
+
catch {
|
|
2180
|
+
snapshots = [];
|
|
2181
|
+
}
|
|
2182
|
+
this.applyDiscoveredProcesses(snapshots);
|
|
2183
|
+
}
|
|
2184
|
+
applyDiscoveredProcesses(snapshots) {
|
|
2185
|
+
const seenKeys = new Set();
|
|
2186
|
+
const discoveredProviders = new Set();
|
|
2187
|
+
let listChanged = false;
|
|
2188
|
+
for (const snapshot of snapshots) {
|
|
2189
|
+
discoveredProviders.add(snapshot.provider);
|
|
2190
|
+
const key = snapshot.sessionId
|
|
2191
|
+
? `${snapshot.provider}:${snapshot.sessionId}`
|
|
2192
|
+
: `${snapshot.provider}:${snapshot.pid}`;
|
|
2193
|
+
seenKeys.add(key);
|
|
2194
|
+
this.processMissCounts.set(key, 0);
|
|
2195
|
+
const existingId = snapshot.sessionId
|
|
2196
|
+
? this.conversationByAgentSessionId.get(snapshot.sessionId)
|
|
2197
|
+
: undefined;
|
|
2198
|
+
const liveId = liveConversationId(snapshot);
|
|
2199
|
+
const conversationId = existingId ?? liveId;
|
|
2200
|
+
const existing = this.conversations.get(conversationId);
|
|
2201
|
+
const cwd = snapshot.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
2202
|
+
const driving = this.currentTurnIds.has(conversationId);
|
|
2203
|
+
const status = driving
|
|
2204
|
+
? existing?.status ?? "running"
|
|
2205
|
+
: existing?.status === "waiting_permission"
|
|
2206
|
+
? "waiting_permission"
|
|
2207
|
+
: "running";
|
|
2208
|
+
if (existing) {
|
|
2209
|
+
const changed = existing.status !== status || existing.cwd !== cwd || !existing.title;
|
|
2210
|
+
existing.status = status;
|
|
2211
|
+
existing.cwd = cwd;
|
|
2212
|
+
if (!existing.title)
|
|
2213
|
+
existing.title = agentProviderLabel(snapshot.provider);
|
|
2214
|
+
if (snapshot.sessionId && !existing.agentSessionId) {
|
|
2215
|
+
existing.agentSessionId = snapshot.sessionId;
|
|
2216
|
+
this.conversationByAgentSessionId.set(snapshot.sessionId, existing.id);
|
|
2217
|
+
}
|
|
2218
|
+
if (changed) {
|
|
2219
|
+
listChanged = true;
|
|
2220
|
+
this.emitConversation(existing);
|
|
2221
|
+
}
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
const now = Date.now();
|
|
2225
|
+
const conversation = {
|
|
2226
|
+
id: conversationId,
|
|
2227
|
+
agentSessionId: snapshot.sessionId,
|
|
2228
|
+
provider: snapshot.provider,
|
|
2229
|
+
cwd,
|
|
2230
|
+
title: agentProviderLabel(snapshot.provider),
|
|
2231
|
+
status,
|
|
2232
|
+
archived: false,
|
|
2233
|
+
lastMessagePreview: "本机终端中的 Agent 进程",
|
|
2234
|
+
lastActivityAt: now,
|
|
2235
|
+
createdAt: now,
|
|
2236
|
+
};
|
|
2237
|
+
this.conversations.set(conversation.id, conversation);
|
|
2238
|
+
if (snapshot.sessionId)
|
|
2239
|
+
this.conversationByAgentSessionId.set(snapshot.sessionId, conversation.id);
|
|
2240
|
+
this.emitConversation(conversation);
|
|
2241
|
+
listChanged = true;
|
|
2242
|
+
}
|
|
2243
|
+
for (const [key, misses] of this.processMissCounts) {
|
|
2244
|
+
if (seenKeys.has(key))
|
|
2245
|
+
continue;
|
|
2246
|
+
const next = misses + 1;
|
|
2247
|
+
this.processMissCounts.set(key, next);
|
|
2248
|
+
if (next < AgentWorkspaceProxy.PROCESS_MISS_LIMIT)
|
|
2249
|
+
continue;
|
|
2250
|
+
this.processMissCounts.delete(key);
|
|
2251
|
+
const [provider, idPart] = key.split(":");
|
|
2252
|
+
if (!provider || !idPart)
|
|
2253
|
+
continue;
|
|
2254
|
+
const conversation = [...this.conversations.values()].find((item) => {
|
|
2255
|
+
if (item.provider !== provider)
|
|
2256
|
+
return false;
|
|
2257
|
+
if (item.agentSessionId === idPart)
|
|
2258
|
+
return true;
|
|
2259
|
+
return item.id === `agent-live-${provider}-${idPart}`;
|
|
2260
|
+
});
|
|
2261
|
+
if (!conversation)
|
|
2262
|
+
continue;
|
|
2263
|
+
if (this.currentTurnIds.has(conversation.id))
|
|
2264
|
+
continue;
|
|
2265
|
+
if (conversation.status === "waiting_permission")
|
|
2266
|
+
continue;
|
|
2267
|
+
if (conversation.status === "running") {
|
|
2268
|
+
conversation.status = "idle";
|
|
2269
|
+
conversation.lastActivityAt = Date.now();
|
|
2270
|
+
this.emitConversation(conversation);
|
|
2271
|
+
listChanged = true;
|
|
2272
|
+
}
|
|
2273
|
+
if (isLiveConversationId(conversation.id) && !conversation.agentSessionId) {
|
|
2274
|
+
this.conversations.delete(conversation.id);
|
|
2275
|
+
this.timelines.delete(conversation.id);
|
|
2276
|
+
this.input.send(createEnvelope({
|
|
2277
|
+
type: "agent.v2.conversation.deleted",
|
|
2278
|
+
sessionId: this.input.sessionId,
|
|
2279
|
+
payload: { conversationId: conversation.id },
|
|
2280
|
+
}));
|
|
2281
|
+
listChanged = true;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
const catalogChanged = discoveredProviders.size !== this.lastDiscoveredProviders.size ||
|
|
2285
|
+
[...discoveredProviders].some((provider) => !this.lastDiscoveredProviders.has(provider));
|
|
2286
|
+
this.lastDiscoveredProviders = discoveredProviders;
|
|
2287
|
+
if (catalogChanged)
|
|
2288
|
+
this.sendCapabilities();
|
|
2289
|
+
if (listChanged) {
|
|
2290
|
+
const conversations = [...this.conversations.values()].filter((item) => !item.archived);
|
|
2291
|
+
this.lastPushedConversationSignature = this.conversationListSignature(conversations);
|
|
2292
|
+
this.input.send(createEnvelope({
|
|
2293
|
+
type: "agent.v2.conversation.list.result",
|
|
2294
|
+
sessionId: this.input.sessionId,
|
|
2295
|
+
payload: { conversations },
|
|
2296
|
+
}));
|
|
2297
|
+
}
|
|
1984
2298
|
}
|
|
1985
2299
|
async ensureProviderClient(provider) {
|
|
1986
2300
|
const existing = this.clients.get(provider);
|
|
@@ -2127,9 +2441,11 @@ export class AgentWorkspaceProxy {
|
|
|
2127
2441
|
}
|
|
2128
2442
|
}
|
|
2129
2443
|
}
|
|
2444
|
+
const listedMcp = await this.listProviderMcpServers(client);
|
|
2130
2445
|
if (provider === "codex") {
|
|
2131
2446
|
// Codex `model/list` is unreliable across versions — make sure mobile always
|
|
2132
2447
|
// sees a usable model picker by merging in a static fallback list.
|
|
2448
|
+
const skillCommands = await this.loadProviderSkillCommands(provider, client);
|
|
2133
2449
|
const merged = {
|
|
2134
2450
|
models: runtimeCapabilities?.models?.length
|
|
2135
2451
|
? runtimeCapabilities.models
|
|
@@ -2138,13 +2454,15 @@ export class AgentWorkspaceProxy {
|
|
|
2138
2454
|
reasoningEfforts: runtimeCapabilities?.reasoningEfforts?.length
|
|
2139
2455
|
? runtimeCapabilities.reasoningEfforts
|
|
2140
2456
|
: [...ALL_REASONING_EFFORTS],
|
|
2141
|
-
commands: runtimeCapabilities?.commands,
|
|
2457
|
+
commands: mergeCommands(runtimeCapabilities?.commands, skillCommands),
|
|
2142
2458
|
modes: runtimeCapabilities?.modes,
|
|
2143
2459
|
currentMode: runtimeCapabilities?.currentMode,
|
|
2144
2460
|
// Preserve any MCP status already accumulated from startupStatus events.
|
|
2145
2461
|
mcpServers: runtimeCapabilities?.mcpServers ?? this.providerCapabilities.get(provider)?.mcpServers,
|
|
2146
2462
|
};
|
|
2147
2463
|
this.providerCapabilities.set(provider, merged);
|
|
2464
|
+
if (listedMcp.length > 0)
|
|
2465
|
+
this.mergeMcpServers(provider, listedMcp);
|
|
2148
2466
|
return;
|
|
2149
2467
|
}
|
|
2150
2468
|
if (runtimeCapabilities) {
|
|
@@ -2157,8 +2475,46 @@ export class AgentWorkspaceProxy {
|
|
|
2157
2475
|
map.set(name, desc);
|
|
2158
2476
|
runtimeCapabilities = { ...runtimeCapabilities, mcpServers: map };
|
|
2159
2477
|
}
|
|
2478
|
+
const skillCommands = await this.loadProviderSkillCommands(provider, client);
|
|
2479
|
+
if (skillCommands.length > 0) {
|
|
2480
|
+
runtimeCapabilities = {
|
|
2481
|
+
...runtimeCapabilities,
|
|
2482
|
+
commands: mergeCommands(runtimeCapabilities.commands, skillCommands),
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2160
2485
|
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
2161
2486
|
}
|
|
2487
|
+
if (listedMcp.length > 0)
|
|
2488
|
+
this.mergeMcpServers(provider, listedMcp);
|
|
2489
|
+
}
|
|
2490
|
+
async listProviderMcpServers(client) {
|
|
2491
|
+
const listMcpServers = client.listMcpServers;
|
|
2492
|
+
if (typeof listMcpServers !== "function")
|
|
2493
|
+
return [];
|
|
2494
|
+
try {
|
|
2495
|
+
return mcpListFromProviderResult(await listMcpServers.call(client));
|
|
2496
|
+
}
|
|
2497
|
+
catch (error) {
|
|
2498
|
+
if (this.input.verbose) {
|
|
2499
|
+
process.stderr.write(`[agent:v2] mcpServerStatus/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2500
|
+
}
|
|
2501
|
+
return [];
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
async loadProviderSkillCommands(provider, client) {
|
|
2505
|
+
const listSkills = client.listSkills;
|
|
2506
|
+
if (typeof listSkills !== "function")
|
|
2507
|
+
return [];
|
|
2508
|
+
try {
|
|
2509
|
+
const result = await listSkills.call(client, { cwd: this.input.cwd });
|
|
2510
|
+
return parseSkillCommands(provider, result);
|
|
2511
|
+
}
|
|
2512
|
+
catch (error) {
|
|
2513
|
+
if (this.input.verbose) {
|
|
2514
|
+
process.stderr.write(`[agent:v2] skills/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2515
|
+
}
|
|
2516
|
+
return [];
|
|
2517
|
+
}
|
|
2162
2518
|
}
|
|
2163
2519
|
async syncProviderSessions() {
|
|
2164
2520
|
await this.initialize();
|
|
@@ -2217,9 +2573,11 @@ export class AgentWorkspaceProxy {
|
|
|
2217
2573
|
? existing?.status ?? "running"
|
|
2218
2574
|
: remoteRunning
|
|
2219
2575
|
? "running"
|
|
2220
|
-
:
|
|
2221
|
-
?
|
|
2222
|
-
: "
|
|
2576
|
+
: remote.status === "error"
|
|
2577
|
+
? "error"
|
|
2578
|
+
: existing?.status === "error" || existing?.status === "waiting_permission"
|
|
2579
|
+
? existing.status
|
|
2580
|
+
: "idle";
|
|
2223
2581
|
const conversation = {
|
|
2224
2582
|
id: conversationId,
|
|
2225
2583
|
agentSessionId,
|
|
@@ -2249,7 +2607,7 @@ export class AgentWorkspaceProxy {
|
|
|
2249
2607
|
}
|
|
2250
2608
|
}
|
|
2251
2609
|
}
|
|
2252
|
-
async hydrateConversationFromProvider(conversation, seedResult) {
|
|
2610
|
+
async hydrateConversationFromProvider(conversation, seedResult, options) {
|
|
2253
2611
|
let client = this.clientForProvider(conversation.provider);
|
|
2254
2612
|
if (!client || !conversation.agentSessionId)
|
|
2255
2613
|
return;
|
|
@@ -2319,8 +2677,9 @@ export class AgentWorkspaceProxy {
|
|
|
2319
2677
|
}
|
|
2320
2678
|
const thread = threadFromProviderResult(source);
|
|
2321
2679
|
const activeTurnId = activeTurnIdFromThread(thread);
|
|
2322
|
-
if (activeTurnId)
|
|
2680
|
+
if (activeTurnId && options?.rememberActiveTurn !== false) {
|
|
2323
2681
|
this.rememberTurnConversationId(conversation.id, activeTurnId);
|
|
2682
|
+
}
|
|
2324
2683
|
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
2325
2684
|
if (model && !conversation.model)
|
|
2326
2685
|
conversation.model = model;
|
|
@@ -2393,7 +2752,11 @@ export class AgentWorkspaceProxy {
|
|
|
2393
2752
|
}
|
|
2394
2753
|
}
|
|
2395
2754
|
sendCapabilities() {
|
|
2396
|
-
const
|
|
2755
|
+
const catalog = [...new Set([
|
|
2756
|
+
...this.input.availableProviders,
|
|
2757
|
+
...this.lastDiscoveredProviders,
|
|
2758
|
+
])];
|
|
2759
|
+
const providers = catalog.map((provider) => {
|
|
2397
2760
|
const client = this.clients.get(provider);
|
|
2398
2761
|
const protocol = this.agentProtocols.get(provider);
|
|
2399
2762
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
@@ -2402,13 +2765,18 @@ export class AgentWorkspaceProxy {
|
|
|
2402
2765
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
2403
2766
|
const supportsPermission = enabled && !isClaudeFallback;
|
|
2404
2767
|
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
2768
|
+
const protocolBacked = isProtocolAgentProvider(provider);
|
|
2405
2769
|
const commands = mergeCommands(defaultProviderCommands(provider, this.input.cwd, enabled), runtimeCapabilities?.commands);
|
|
2406
2770
|
const currentMode = [...this.conversations.values()].find((conversation) => conversation.provider === provider)?.collaborationMode;
|
|
2407
2771
|
return {
|
|
2408
2772
|
id: provider,
|
|
2409
2773
|
label: providerLabel(provider),
|
|
2410
2774
|
enabled,
|
|
2411
|
-
reason: enabled
|
|
2775
|
+
reason: enabled
|
|
2776
|
+
? undefined
|
|
2777
|
+
: protocolBacked
|
|
2778
|
+
? this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`
|
|
2779
|
+
: `${providerLabel(provider)} 已在本机检测;远程对话请在终端继续`,
|
|
2412
2780
|
supportsImages,
|
|
2413
2781
|
supportsPermission,
|
|
2414
2782
|
supportsPlan: enabled,
|
|
@@ -2445,11 +2813,11 @@ export class AgentWorkspaceProxy {
|
|
|
2445
2813
|
sessionId: this.input.sessionId,
|
|
2446
2814
|
payload: {
|
|
2447
2815
|
enabled: anyEnabled,
|
|
2448
|
-
provider: this.input.availableProviders[0] ?? "codex",
|
|
2816
|
+
provider: this.input.availableProviders[0] ?? catalog[0] ?? "codex",
|
|
2449
2817
|
providers,
|
|
2450
2818
|
protocolVersion: 1,
|
|
2451
2819
|
workspaceProtocolVersion: 2,
|
|
2452
|
-
error: anyEnabled ? undefined : "
|
|
2820
|
+
error: anyEnabled ? undefined : "没有可远程驱动的 Agent。请安装 Claude Code 或 Codex CLI;其它 Agent 只要在本机终端运行就会出现在列表里。",
|
|
2453
2821
|
supportsSessionList: anyEnabled,
|
|
2454
2822
|
supportsSessionLoad: anyEnabled,
|
|
2455
2823
|
supportsImages: providers.some((p) => p.supportsImages),
|
|
@@ -2465,13 +2833,34 @@ export class AgentWorkspaceProxy {
|
|
|
2465
2833
|
if (!provider) {
|
|
2466
2834
|
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
2467
2835
|
}
|
|
2468
|
-
if (!this.input.availableProviders.includes(provider)) {
|
|
2469
|
-
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
2470
|
-
}
|
|
2471
2836
|
let cwd = payload.cwd ?? this.input.cwd;
|
|
2472
2837
|
let agentSessionId = payload.agentSessionId;
|
|
2473
2838
|
let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
2474
2839
|
(agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
|
|
2840
|
+
if (existingConversation &&
|
|
2841
|
+
!this.clientForProvider(existingConversation.provider) &&
|
|
2842
|
+
!isProtocolAgentProvider(existingConversation.provider)) {
|
|
2843
|
+
this.activeConversationId = existingConversation.id;
|
|
2844
|
+
this.input.send(createEnvelope({
|
|
2845
|
+
type: "agent.v2.conversation.opened",
|
|
2846
|
+
sessionId: this.input.sessionId,
|
|
2847
|
+
payload: {
|
|
2848
|
+
conversation: existingConversation,
|
|
2849
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
2850
|
+
requestedConversationId: payload.conversationId,
|
|
2851
|
+
},
|
|
2852
|
+
}));
|
|
2853
|
+
return existingConversation;
|
|
2854
|
+
}
|
|
2855
|
+
if (!isProtocolAgentProvider(provider) && !resolveAgentCommand({
|
|
2856
|
+
provider,
|
|
2857
|
+
command: this.input.command,
|
|
2858
|
+
})) {
|
|
2859
|
+
return this.openFailure(payload, `${providerLabel(provider)} 还不能从远程新建对话。在本机终端启动后,会话会出现在列表里。`);
|
|
2860
|
+
}
|
|
2861
|
+
if (!this.input.availableProviders.includes(provider) && !existingConversation) {
|
|
2862
|
+
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
2863
|
+
}
|
|
2475
2864
|
if (!payload.cwd && existingConversation?.cwd)
|
|
2476
2865
|
cwd = existingConversation.cwd;
|
|
2477
2866
|
let client = await this.ensureProviderClient(provider);
|
|
@@ -2498,14 +2887,15 @@ export class AgentWorkspaceProxy {
|
|
|
2498
2887
|
return this.openFailure(payload, message, cwd);
|
|
2499
2888
|
}
|
|
2500
2889
|
// Fork: seed a NEW conversation from an existing one's transcript, truncated
|
|
2501
|
-
// at forkFromTurnId. Claude
|
|
2502
|
-
//
|
|
2503
|
-
//
|
|
2504
|
-
//
|
|
2890
|
+
// at forkFromTurnId. Claude uses the SDK forkSession(); Codex uses
|
|
2891
|
+
// thread/fork. Other providers notify and fall through to a plain new
|
|
2892
|
+
// conversation. On success we set agentSessionId to the new thread id and
|
|
2893
|
+
// let the normal load path hydrate it.
|
|
2505
2894
|
if (payload.forkFromConversationId) {
|
|
2506
2895
|
const source = this.conversations.get(payload.forkFromConversationId);
|
|
2507
2896
|
const sourceSessionId = source?.agentSessionId;
|
|
2508
2897
|
const forkable = client.forkSession;
|
|
2898
|
+
const forkThread = client.forkThread;
|
|
2509
2899
|
if (provider === "claude" && typeof forkable === "function" && sourceSessionId) {
|
|
2510
2900
|
if (source?.cwd && !payload.cwd)
|
|
2511
2901
|
cwd = source.cwd;
|
|
@@ -2527,6 +2917,39 @@ export class AgentWorkspaceProxy {
|
|
|
2527
2917
|
this.emitNotice({ kind: "warning", title: "分叉失败", detail: "无法复制该会话的历史记录,已新建空会话。" });
|
|
2528
2918
|
}
|
|
2529
2919
|
}
|
|
2920
|
+
else if (provider === "codex" && typeof forkThread === "function" && sourceSessionId) {
|
|
2921
|
+
if (source?.cwd && !payload.cwd)
|
|
2922
|
+
cwd = source.cwd;
|
|
2923
|
+
try {
|
|
2924
|
+
const forked = await forkThread.call(client, {
|
|
2925
|
+
sessionId: sourceSessionId,
|
|
2926
|
+
lastTurnId: payload.forkFromTurnId,
|
|
2927
|
+
});
|
|
2928
|
+
const forkedId = this.extractSessionId(forked);
|
|
2929
|
+
if (forkedId) {
|
|
2930
|
+
agentSessionId = forkedId;
|
|
2931
|
+
payload = {
|
|
2932
|
+
...payload,
|
|
2933
|
+
model: payload.model ?? source?.model,
|
|
2934
|
+
reasoningEffort: payload.reasoningEffort ?? source?.reasoningEffort,
|
|
2935
|
+
permissionMode: payload.permissionMode ?? source?.permissionMode,
|
|
2936
|
+
collaborationMode: payload.collaborationMode ?? source?.collaborationMode,
|
|
2937
|
+
title: payload.title ?? (source?.title ? `${source.title} (分叉)` : undefined),
|
|
2938
|
+
};
|
|
2939
|
+
existingConversation = undefined;
|
|
2940
|
+
}
|
|
2941
|
+
else {
|
|
2942
|
+
this.emitNotice({ kind: "warning", title: "分叉失败", detail: "无法复制该会话的历史记录,已新建空会话。" });
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
catch (error) {
|
|
2946
|
+
this.emitNotice({
|
|
2947
|
+
kind: "warning",
|
|
2948
|
+
title: "分叉失败",
|
|
2949
|
+
detail: error instanceof Error ? error.message : "无法复制该会话的历史记录,已新建空会话。",
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2530
2953
|
else {
|
|
2531
2954
|
this.emitNotice({
|
|
2532
2955
|
kind: "native_unsupported",
|
|
@@ -2727,12 +3150,16 @@ export class AgentWorkspaceProxy {
|
|
|
2727
3150
|
if (!conversation)
|
|
2728
3151
|
return;
|
|
2729
3152
|
if (!conversation.agentSessionId) {
|
|
2730
|
-
this.rejectAgentAction(conversation,
|
|
3153
|
+
this.rejectAgentAction(conversation, isLiveConversationId(conversation.id) || !isProtocolAgentProvider(conversation.provider)
|
|
3154
|
+
? `${providerLabel(conversation.provider)} 正在本机终端中运行。远程对话协议尚未接入,请打开终端面板继续。`
|
|
3155
|
+
: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。");
|
|
2731
3156
|
return;
|
|
2732
3157
|
}
|
|
2733
3158
|
const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
|
|
2734
3159
|
if (!client) {
|
|
2735
|
-
this.rejectAgentAction(conversation,
|
|
3160
|
+
this.rejectAgentAction(conversation, isLiveConversationId(conversation.id) || !isProtocolAgentProvider(conversation.provider)
|
|
3161
|
+
? `${providerLabel(conversation.provider)} 正在本机终端中运行。远程对话协议尚未接入,请打开终端面板继续。`
|
|
3162
|
+
: this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`, "error");
|
|
2736
3163
|
return;
|
|
2737
3164
|
}
|
|
2738
3165
|
const protocol = this.protocolForProvider(conversation.provider);
|
|
@@ -3036,14 +3463,37 @@ export class AgentWorkspaceProxy {
|
|
|
3036
3463
|
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
3037
3464
|
return;
|
|
3038
3465
|
}
|
|
3039
|
-
if (command.name === "review"
|
|
3040
|
-
const
|
|
3041
|
-
|
|
3042
|
-
|
|
3466
|
+
if (command.name === "review") {
|
|
3467
|
+
const startReview = client?.startReview;
|
|
3468
|
+
if (typeof startReview === "function" && conversation.agentSessionId) {
|
|
3469
|
+
conversation.status = "running";
|
|
3470
|
+
conversation.lastActivityAt = now;
|
|
3471
|
+
this.emitConversation(conversation);
|
|
3472
|
+
const result = await startReview.call(client, {
|
|
3473
|
+
sessionId: conversation.agentSessionId,
|
|
3474
|
+
prompt: args || undefined,
|
|
3475
|
+
});
|
|
3476
|
+
const turnId = this.extractTurnId(result);
|
|
3477
|
+
if (turnId)
|
|
3478
|
+
this.rememberTurnConversationId(conversation.id, turnId);
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3043
3481
|
await this.sendPrompt({
|
|
3044
3482
|
conversationId: conversation.id,
|
|
3045
3483
|
clientMessageId: id(command.name),
|
|
3046
|
-
contentBlocks: [{ type: "text", text:
|
|
3484
|
+
contentBlocks: [{ type: "text", text: args || "Review the current local changes." }],
|
|
3485
|
+
model: conversation.model,
|
|
3486
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
3487
|
+
permissionMode: conversation.permissionMode,
|
|
3488
|
+
collaborationMode: conversation.collaborationMode,
|
|
3489
|
+
});
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
if (command.name === "subagents") {
|
|
3493
|
+
await this.sendPrompt({
|
|
3494
|
+
conversationId: conversation.id,
|
|
3495
|
+
clientMessageId: id(command.name),
|
|
3496
|
+
contentBlocks: [{ type: "text", text: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results." }],
|
|
3047
3497
|
model: conversation.model,
|
|
3048
3498
|
reasoningEffort: conversation.reasoningEffort,
|
|
3049
3499
|
permissionMode: conversation.permissionMode,
|
|
@@ -3083,8 +3533,73 @@ export class AgentWorkspaceProxy {
|
|
|
3083
3533
|
}
|
|
3084
3534
|
return {};
|
|
3085
3535
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3536
|
+
async startMcpLogin(payload) {
|
|
3537
|
+
const provider = payload.provider
|
|
3538
|
+
?? this.input.availableProviders.find((item) => this.clients.has(item))
|
|
3539
|
+
?? this.input.availableProviders[0];
|
|
3540
|
+
const sendResult = (input) => {
|
|
3541
|
+
this.input.send(createEnvelope({
|
|
3542
|
+
type: "agent.v2.mcp.login.result",
|
|
3543
|
+
sessionId: this.input.sessionId,
|
|
3544
|
+
payload: {
|
|
3545
|
+
...(provider ? { provider } : {}),
|
|
3546
|
+
serverName: payload.serverName,
|
|
3547
|
+
authorizationUrl: input.authorizationUrl,
|
|
3548
|
+
error: input.error,
|
|
3549
|
+
},
|
|
3550
|
+
}));
|
|
3551
|
+
};
|
|
3552
|
+
if (!provider) {
|
|
3553
|
+
sendResult({ error: "没有可用的 Agent 提供方。" });
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
const client = this.clientForProvider(provider);
|
|
3557
|
+
const startMcpOAuth = client?.startMcpOAuth;
|
|
3558
|
+
if (typeof startMcpOAuth !== "function") {
|
|
3559
|
+
sendResult({ error: "请在主机上的 Claude / Codex 完成该 MCP 的授权。" });
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
try {
|
|
3563
|
+
const result = await startMcpOAuth.call(client, { serverName: payload.serverName });
|
|
3564
|
+
const raw = asRecord(result);
|
|
3565
|
+
const authorizationUrl = firstString(raw, [
|
|
3566
|
+
"authorizationUrl",
|
|
3567
|
+
"authorization_url",
|
|
3568
|
+
"url",
|
|
3569
|
+
"loginUrl",
|
|
3570
|
+
]);
|
|
3571
|
+
if (!authorizationUrl) {
|
|
3572
|
+
sendResult({ error: "主机没有返回授权地址。" });
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
this.mergeMcpServers(provider, [{
|
|
3576
|
+
name: payload.serverName,
|
|
3577
|
+
status: "needs_auth",
|
|
3578
|
+
}]);
|
|
3579
|
+
this.sendCapabilities();
|
|
3580
|
+
sendResult({ authorizationUrl });
|
|
3581
|
+
}
|
|
3582
|
+
catch (error) {
|
|
3583
|
+
sendResult({ error: error instanceof Error ? error.message : String(error) });
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
handleMcpOAuthCompleted(params) {
|
|
3587
|
+
const raw = asRecord(params) ?? {};
|
|
3588
|
+
const name = firstString(raw, ["name", "server", "serverName"]);
|
|
3589
|
+
if (!name)
|
|
3590
|
+
return;
|
|
3591
|
+
const success = raw.success !== false && !raw.error;
|
|
3592
|
+
const provider = this.input.availableProviders.find((item) => this.clients.has(item))
|
|
3593
|
+
?? this.input.availableProviders[0];
|
|
3594
|
+
if (!provider)
|
|
3595
|
+
return;
|
|
3596
|
+
this.mergeMcpServers(provider, [{
|
|
3597
|
+
name,
|
|
3598
|
+
status: success ? "connected" : "needs_auth",
|
|
3599
|
+
error: success ? undefined : firstString(raw, ["error", "message"]),
|
|
3600
|
+
}]);
|
|
3601
|
+
this.sendCapabilities();
|
|
3602
|
+
}
|
|
3088
3603
|
mergeMcpServers(provider, servers) {
|
|
3089
3604
|
if (servers.length === 0)
|
|
3090
3605
|
return;
|
|
@@ -3112,7 +3627,7 @@ export class AgentWorkspaceProxy {
|
|
|
3112
3627
|
return;
|
|
3113
3628
|
this.mergeMcpServers(provider, [{
|
|
3114
3629
|
name,
|
|
3115
|
-
status:
|
|
3630
|
+
status: mcpStatusFromRecord(raw),
|
|
3116
3631
|
error: firstString(raw, ["error", "message", "reason"]),
|
|
3117
3632
|
toolCount: firstNumber(raw, ["toolCount", "tools", "toolsCount"]) ?? (Array.isArray(raw.tools) ? raw.tools.length : undefined),
|
|
3118
3633
|
}]);
|
|
@@ -3162,9 +3677,11 @@ export class AgentWorkspaceProxy {
|
|
|
3162
3677
|
this.handleMcpStartupStatus(method, params);
|
|
3163
3678
|
return;
|
|
3164
3679
|
}
|
|
3165
|
-
if (method
|
|
3166
|
-
|
|
3167
|
-
|
|
3680
|
+
if (method === "mcpServer/oauthLogin/completed") {
|
|
3681
|
+
this.handleMcpOAuthCompleted(params);
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
if (method.startsWith("account/") || method === "serverRequest/resolved") {
|
|
3168
3685
|
return;
|
|
3169
3686
|
}
|
|
3170
3687
|
const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
|
|
@@ -3197,6 +3714,46 @@ export class AgentWorkspaceProxy {
|
|
|
3197
3714
|
}
|
|
3198
3715
|
return;
|
|
3199
3716
|
}
|
|
3717
|
+
if (method === "thread/name/updated") {
|
|
3718
|
+
if (conversationId) {
|
|
3719
|
+
const raw = asRecord(params);
|
|
3720
|
+
const title = firstString(raw, ["name", "title"]) ?? firstString(asRecord(raw?.thread), ["name", "title"]);
|
|
3721
|
+
const conversation = this.conversations.get(conversationId);
|
|
3722
|
+
if (conversation && title !== undefined) {
|
|
3723
|
+
conversation.title = title;
|
|
3724
|
+
conversation.lastActivityAt = Date.now();
|
|
3725
|
+
this.emitConversation(conversation);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
if (method === "thread/settings/updated") {
|
|
3731
|
+
if (conversationId) {
|
|
3732
|
+
const conversation = this.conversations.get(conversationId);
|
|
3733
|
+
if (conversation) {
|
|
3734
|
+
applyThreadSettingsToConversation(conversation, params);
|
|
3735
|
+
conversation.lastActivityAt = Date.now();
|
|
3736
|
+
this.emitConversation(conversation);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
if (method === "thread/archived" || method === "thread/unarchived") {
|
|
3742
|
+
if (conversationId) {
|
|
3743
|
+
const conversation = this.conversations.get(conversationId);
|
|
3744
|
+
if (conversation) {
|
|
3745
|
+
conversation.archived = method === "thread/archived";
|
|
3746
|
+
conversation.lastActivityAt = Date.now();
|
|
3747
|
+
this.emitConversation(conversation);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
if (method === "thread/deleted") {
|
|
3753
|
+
if (conversationId)
|
|
3754
|
+
this.forgetConversationLocally(conversationId);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3200
3757
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
3201
3758
|
this.handleStructuredInput(params);
|
|
3202
3759
|
return;
|
|
@@ -3257,6 +3814,13 @@ export class AgentWorkspaceProxy {
|
|
|
3257
3814
|
case "item/agentMessage/delta":
|
|
3258
3815
|
this.handleAgentMessageDelta(params);
|
|
3259
3816
|
return;
|
|
3817
|
+
case "item/reasoning/summaryTextDelta":
|
|
3818
|
+
case "item/reasoning/textDelta":
|
|
3819
|
+
this.handleReasoningDelta(params);
|
|
3820
|
+
return;
|
|
3821
|
+
case "item/reasoning/summaryPartAdded":
|
|
3822
|
+
this.handleReasoningDelta(params, { partBoundary: true });
|
|
3823
|
+
return;
|
|
3260
3824
|
case "turn/plan/updated":
|
|
3261
3825
|
this.handlePlanUpdated(params);
|
|
3262
3826
|
return;
|
|
@@ -3321,9 +3885,65 @@ export class AgentWorkspaceProxy {
|
|
|
3321
3885
|
updatedAt: Date.now(),
|
|
3322
3886
|
isStreaming: true,
|
|
3323
3887
|
};
|
|
3324
|
-
this.
|
|
3888
|
+
this.applyStreamingTextDelta(conversationId, item, existing, delta);
|
|
3325
3889
|
this.updateConversationPreview(conversationId, text, "running");
|
|
3326
3890
|
}
|
|
3891
|
+
handleReasoningDelta(params, options) {
|
|
3892
|
+
const raw = asRecord(params);
|
|
3893
|
+
if (!raw)
|
|
3894
|
+
return;
|
|
3895
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
3896
|
+
if (!conversationId)
|
|
3897
|
+
return;
|
|
3898
|
+
const itemId = firstString(raw, ["itemId", "id", "messageId"]) ?? id("thinking");
|
|
3899
|
+
const existing = this.findItem(conversationId, itemId);
|
|
3900
|
+
const delta = firstString(raw, ["delta", "text", "content", "summary"])
|
|
3901
|
+
?? (options?.partBoundary ? "\n" : undefined);
|
|
3902
|
+
if (!delta)
|
|
3903
|
+
return;
|
|
3904
|
+
const text = `${existing?.text ?? ""}${delta}`;
|
|
3905
|
+
const item = {
|
|
3906
|
+
id: itemId,
|
|
3907
|
+
conversationId,
|
|
3908
|
+
type: "status",
|
|
3909
|
+
kind: "thinking",
|
|
3910
|
+
role: "system",
|
|
3911
|
+
turnId: this.extractTurnId(raw) ?? this.currentTurnIds.get(conversationId),
|
|
3912
|
+
itemId,
|
|
3913
|
+
content: [{ type: "text", text }],
|
|
3914
|
+
text,
|
|
3915
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
3916
|
+
updatedAt: Date.now(),
|
|
3917
|
+
isStreaming: true,
|
|
3918
|
+
};
|
|
3919
|
+
this.applyStreamingTextDelta(conversationId, item, existing, delta);
|
|
3920
|
+
}
|
|
3921
|
+
applyStreamingTextDelta(conversationId, item, existing, delta) {
|
|
3922
|
+
if (!existing) {
|
|
3923
|
+
this.upsertItem(conversationId, item);
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
this.rememberItemConversationId(conversationId, item);
|
|
3927
|
+
const timeline = this.timelines.get(conversationId) ?? [];
|
|
3928
|
+
const index = timeline.findIndex((entry) => entry.id === item.id);
|
|
3929
|
+
if (index >= 0)
|
|
3930
|
+
timeline[index] = item;
|
|
3931
|
+
else
|
|
3932
|
+
timeline.push(item);
|
|
3933
|
+
this.timelines.set(conversationId, timeline);
|
|
3934
|
+
this.input.send(createEnvelope({
|
|
3935
|
+
type: "agent.v2.event",
|
|
3936
|
+
sessionId: this.input.sessionId,
|
|
3937
|
+
payload: {
|
|
3938
|
+
conversationId,
|
|
3939
|
+
patch: {
|
|
3940
|
+
itemId: item.id,
|
|
3941
|
+
textDelta: delta,
|
|
3942
|
+
updatedAt: item.updatedAt,
|
|
3943
|
+
},
|
|
3944
|
+
},
|
|
3945
|
+
}));
|
|
3946
|
+
}
|
|
3327
3947
|
handlePlanUpdated(params) {
|
|
3328
3948
|
const raw = asRecord(params);
|
|
3329
3949
|
const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
|
|
@@ -3789,19 +4409,14 @@ export class AgentWorkspaceProxy {
|
|
|
3789
4409
|
const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
|
|
3790
4410
|
const rawToolCall = asRecord(raw.toolCall) ?? raw;
|
|
3791
4411
|
// Codex app-server command/file approvals carry `command`/`reason`/`cwd`
|
|
3792
|
-
// (not toolCall/context).
|
|
3793
|
-
//
|
|
3794
|
-
const codexCommand = firstString(raw, ["command"]);
|
|
3795
|
-
const codexCwd = firstString(raw, ["cwd"]);
|
|
4412
|
+
// (not toolCall/context). Keep those fields as JSON so the web card can
|
|
4413
|
+
// render `$ command` / a path instead of a raw dump.
|
|
3796
4414
|
const permission = {
|
|
3797
4415
|
requestId,
|
|
3798
|
-
toolName:
|
|
3799
|
-
|
|
3800
|
-
toolInput: codexCommand
|
|
3801
|
-
? (codexCwd ? `$ ${codexCommand}\n# cwd: ${codexCwd}` : `$ ${codexCommand}`)
|
|
3802
|
-
: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
|
|
4416
|
+
toolName: permissionToolName(raw, rawToolCall),
|
|
4417
|
+
toolInput: permissionToolInput(raw, rawToolCall),
|
|
3803
4418
|
context: firstString(raw, ["context", "description", "message", "title", "reason"]),
|
|
3804
|
-
options:
|
|
4419
|
+
options: permissionOptionsFromRequest(raw, source),
|
|
3805
4420
|
};
|
|
3806
4421
|
this.pendingPermissions.set(requestId, permission);
|
|
3807
4422
|
if (source)
|
|
@@ -4075,6 +4690,22 @@ export class AgentWorkspaceProxy {
|
|
|
4075
4690
|
payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
|
|
4076
4691
|
}));
|
|
4077
4692
|
}
|
|
4693
|
+
forgetConversationLocally(conversationId) {
|
|
4694
|
+
const conversation = this.conversations.get(conversationId);
|
|
4695
|
+
if (conversation?.agentSessionId) {
|
|
4696
|
+
this.deletedAgentSessionIds.add(conversation.agentSessionId);
|
|
4697
|
+
this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
4698
|
+
}
|
|
4699
|
+
this.conversations.delete(conversationId);
|
|
4700
|
+
this.timelines.delete(conversationId);
|
|
4701
|
+
this.historyCursors.delete(conversationId);
|
|
4702
|
+
this.lastHydrateAt.delete(conversationId);
|
|
4703
|
+
this.input.send(createEnvelope({
|
|
4704
|
+
type: "agent.v2.conversation.deleted",
|
|
4705
|
+
sessionId: this.input.sessionId,
|
|
4706
|
+
payload: { conversationId },
|
|
4707
|
+
}));
|
|
4708
|
+
}
|
|
4078
4709
|
emitConversation(conversation) {
|
|
4079
4710
|
this.input.send(createEnvelope({
|
|
4080
4711
|
type: "agent.v2.event",
|
|
@@ -4418,6 +5049,77 @@ export class AgentWorkspaceProxy {
|
|
|
4418
5049
|
}, 0);
|
|
4419
5050
|
}
|
|
4420
5051
|
}
|
|
5052
|
+
function permissionToolName(raw, rawToolCall) {
|
|
5053
|
+
const named = firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]);
|
|
5054
|
+
if (named)
|
|
5055
|
+
return named;
|
|
5056
|
+
if (firstString(raw, ["command"]))
|
|
5057
|
+
return "shell";
|
|
5058
|
+
if (permissionFilePath(raw))
|
|
5059
|
+
return "Edit";
|
|
5060
|
+
return undefined;
|
|
5061
|
+
}
|
|
5062
|
+
function permissionFilePath(raw) {
|
|
5063
|
+
const direct = firstString(raw, ["path", "filePath", "file_path"]);
|
|
5064
|
+
if (direct)
|
|
5065
|
+
return direct;
|
|
5066
|
+
const changes = Array.isArray(raw.changes) ? raw.changes : undefined;
|
|
5067
|
+
const first = changes ? asRecord(changes[0]) : undefined;
|
|
5068
|
+
return firstString(first, ["path", "filePath", "file_path"]);
|
|
5069
|
+
}
|
|
5070
|
+
function permissionToolInput(raw, rawToolCall) {
|
|
5071
|
+
const command = firstString(raw, ["command"]);
|
|
5072
|
+
const cwd = firstString(raw, ["cwd"]);
|
|
5073
|
+
const path = permissionFilePath(raw);
|
|
5074
|
+
if (command || path) {
|
|
5075
|
+
return JSON.stringify({
|
|
5076
|
+
...(command ? { command } : {}),
|
|
5077
|
+
...(cwd ? { cwd } : {}),
|
|
5078
|
+
...(path ? { path } : {}),
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
5081
|
+
return stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall);
|
|
5082
|
+
}
|
|
5083
|
+
function officialDecisionKey(value) {
|
|
5084
|
+
if (typeof value === "string" && value.trim())
|
|
5085
|
+
return value.trim();
|
|
5086
|
+
const raw = asRecord(value);
|
|
5087
|
+
return firstString(raw, ["decision", "id", "kind", "type", "optionId"]);
|
|
5088
|
+
}
|
|
5089
|
+
function officialDecisionOption(key) {
|
|
5090
|
+
const normalized = key.replace(/[\s_-]/g, "").toLowerCase();
|
|
5091
|
+
if (normalized === "accept" || normalized === "allow" || normalized === "allowonce") {
|
|
5092
|
+
return { id: key === "accept" ? "accept" : key, label: "允许一次", kind: "allow" };
|
|
5093
|
+
}
|
|
5094
|
+
if (normalized === "acceptforsession" ||
|
|
5095
|
+
normalized === "allowsession" ||
|
|
5096
|
+
normalized === "allowforsession" ||
|
|
5097
|
+
normalized === "session") {
|
|
5098
|
+
return { id: "acceptForSession", label: "本会话允许", kind: "allow" };
|
|
5099
|
+
}
|
|
5100
|
+
if (normalized === "decline" || normalized === "deny" || normalized === "reject") {
|
|
5101
|
+
return { id: normalized === "decline" ? "decline" : key, label: "拒绝", kind: "deny" };
|
|
5102
|
+
}
|
|
5103
|
+
if (normalized === "cancel") {
|
|
5104
|
+
return { id: "cancel", label: "取消", kind: "deny" };
|
|
5105
|
+
}
|
|
5106
|
+
return undefined;
|
|
5107
|
+
}
|
|
5108
|
+
function parseOfficialDecisions(value) {
|
|
5109
|
+
const options = [];
|
|
5110
|
+
const seen = new Set();
|
|
5111
|
+
for (const entry of value) {
|
|
5112
|
+
const key = officialDecisionKey(entry);
|
|
5113
|
+
if (!key)
|
|
5114
|
+
continue;
|
|
5115
|
+
const mapped = officialDecisionOption(key);
|
|
5116
|
+
if (!mapped || seen.has(mapped.id))
|
|
5117
|
+
continue;
|
|
5118
|
+
seen.add(mapped.id);
|
|
5119
|
+
options.push(mapped);
|
|
5120
|
+
}
|
|
5121
|
+
return options;
|
|
5122
|
+
}
|
|
4421
5123
|
function parsePermissionOptions(value) {
|
|
4422
5124
|
if (!Array.isArray(value)) {
|
|
4423
5125
|
return [
|
|
@@ -4425,17 +5127,24 @@ function parsePermissionOptions(value) {
|
|
|
4425
5127
|
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
4426
5128
|
];
|
|
4427
5129
|
}
|
|
5130
|
+
const official = parseOfficialDecisions(value);
|
|
5131
|
+
if (official.length > 0 && value.every((entry) => officialDecisionOption(officialDecisionKey(entry) ?? ""))) {
|
|
5132
|
+
return official;
|
|
5133
|
+
}
|
|
4428
5134
|
const options = value
|
|
4429
5135
|
.map((entry, index) => {
|
|
4430
5136
|
const raw = asRecord(entry) ?? {};
|
|
4431
|
-
const idValue = raw.optionId ?? raw.id ?? raw.kind ?? `option-${index + 1}`;
|
|
4432
|
-
const labelValue = raw.name ?? raw.label ?? raw.kind ?? String(idValue);
|
|
5137
|
+
const idValue = raw.optionId ?? raw.id ?? raw.kind ?? raw.decision ?? `option-${index + 1}`;
|
|
5138
|
+
const labelValue = raw.name ?? raw.label ?? raw.kind ?? raw.decision ?? String(idValue);
|
|
4433
5139
|
const id = String(idValue);
|
|
4434
5140
|
const label = String(labelValue);
|
|
5141
|
+
const official = officialDecisionOption(id);
|
|
5142
|
+
if (official)
|
|
5143
|
+
return { ...official, id, label: official.label === "允许一次" && label !== id ? label : official.label };
|
|
4435
5144
|
const normalized = `${id} ${label}`.toLowerCase();
|
|
4436
|
-
const kind =
|
|
5145
|
+
const kind = /reject|deny|decline|cancel/.test(normalized)
|
|
4437
5146
|
? "deny"
|
|
4438
|
-
: normalized
|
|
5147
|
+
: /allow|accept/.test(normalized)
|
|
4439
5148
|
? "allow"
|
|
4440
5149
|
: "other";
|
|
4441
5150
|
return { id, label, kind };
|
|
@@ -4448,6 +5157,35 @@ function parsePermissionOptions(value) {
|
|
|
4448
5157
|
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
4449
5158
|
];
|
|
4450
5159
|
}
|
|
5160
|
+
function permissionOptionsFromRequest(raw, source) {
|
|
5161
|
+
const decisions = raw.availableDecisions;
|
|
5162
|
+
if (Array.isArray(decisions) && decisions.length > 0) {
|
|
5163
|
+
const official = parseOfficialDecisions(decisions);
|
|
5164
|
+
if (official.length > 0)
|
|
5165
|
+
return official;
|
|
5166
|
+
}
|
|
5167
|
+
if (Array.isArray(raw.options)) {
|
|
5168
|
+
return parsePermissionOptions(raw.options);
|
|
5169
|
+
}
|
|
5170
|
+
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
5171
|
+
return [
|
|
5172
|
+
{ id: "accept", label: "允许一次", kind: "allow" },
|
|
5173
|
+
{ id: "acceptForSession", label: "本会话允许", kind: "allow" },
|
|
5174
|
+
{ id: "decline", label: "拒绝", kind: "deny" },
|
|
5175
|
+
];
|
|
5176
|
+
}
|
|
5177
|
+
if (source === "item/permissions/requestApproval") {
|
|
5178
|
+
return [
|
|
5179
|
+
{ id: "turn", label: "本次允许", kind: "allow" },
|
|
5180
|
+
{ id: "session", label: "本会话允许", kind: "allow" },
|
|
5181
|
+
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
5182
|
+
];
|
|
5183
|
+
}
|
|
5184
|
+
return [
|
|
5185
|
+
{ id: "allow", label: "允许", kind: "allow" },
|
|
5186
|
+
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
5187
|
+
];
|
|
5188
|
+
}
|
|
4451
5189
|
function selectPermissionOption(permission, outcome) {
|
|
4452
5190
|
if (outcome === "cancelled")
|
|
4453
5191
|
return "cancelled";
|
|
@@ -4518,7 +5256,13 @@ function formatStructuredInputResponseForSource(source, answers, input) {
|
|
|
4518
5256
|
}
|
|
4519
5257
|
function formatPermissionResponse(source, outcome, optionId) {
|
|
4520
5258
|
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
4521
|
-
|
|
5259
|
+
if (outcome === "cancelled" || optionId === "cancel")
|
|
5260
|
+
return { decision: "cancel" };
|
|
5261
|
+
if (outcome === "deny")
|
|
5262
|
+
return { decision: "decline" };
|
|
5263
|
+
if (optionId === "acceptForSession")
|
|
5264
|
+
return { decision: "acceptForSession" };
|
|
5265
|
+
return { decision: "accept" };
|
|
4522
5266
|
}
|
|
4523
5267
|
if (source === "item/permissions/requestApproval") {
|
|
4524
5268
|
if (outcome === "allow") {
|