linkshell-cli 0.5.37 → 0.5.39
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 +21 -0
- package/dist/cli/src/runtime/acp/acp-client.js +101 -5
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +1 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +12 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +137 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +607 -97
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.js +28 -2
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +0 -48
- package/dist/cli/src/runtime/bridge-session.js +10 -1039
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/hook-command.d.ts +5 -30
- package/dist/cli/src/runtime/hook-command.js +54 -41
- package/dist/cli/src/runtime/hook-command.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +164 -8
- package/dist/shared-protocol/src/index.js +6 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/acp-client.ts +116 -5
- package/src/runtime/acp/agent-process-discovery.ts +8 -1
- package/src/runtime/acp/agent-workspace.ts +557 -50
- package/src/runtime/acp/provider-resolver.ts +31 -2
- package/src/runtime/bridge-session.ts +9 -1117
- package/src/runtime/hook-command.ts +47 -47
- package/web/assets/{FileBrowser-Coy7Szjx.js → FileBrowser-DtVanRUn.js} +1 -1
- package/web/assets/{PortPreview-D-XovbV1.js → PortPreview-DC2-UmHo.js} +1 -1
- package/web/assets/{TerminalPanel-Cah56AtC.js → TerminalPanel-D4gHM4Ta.js} +1 -1
- package/web/assets/{UsageDashboard-DfkjJagC.js → UsageDashboard-ZnvuvqWc.js} +1 -1
- package/web/assets/index-DKdJgZ9D.js +36 -0
- package/web/index.html +1 -1
- package/web/assets/index-ClZzQ4ba.js +0 -35
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync, watch } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, join, relative } from "node:path";
|
|
4
4
|
import {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
parseTypedPayload,
|
|
7
7
|
type Envelope,
|
|
8
8
|
} from "@linkshell/protocol";
|
|
9
|
-
import { AcpClient } from "./acp-client.js";
|
|
9
|
+
import { AcpClient, type AdvertisedAcpCapabilities } from "./acp-client.js";
|
|
10
10
|
import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
11
11
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
12
12
|
import { aggregateUsage } from "../usage-report.js";
|
|
@@ -249,11 +249,14 @@ interface AgentConversation {
|
|
|
249
249
|
agentSessionId?: string;
|
|
250
250
|
provider: AgentProvider;
|
|
251
251
|
cwd: string;
|
|
252
|
+
workspaceRoots?: string[];
|
|
253
|
+
group?: string;
|
|
252
254
|
title?: string;
|
|
253
255
|
model?: string;
|
|
254
256
|
reasoningEffort?: string;
|
|
255
257
|
permissionMode?: AgentPermissionMode;
|
|
256
258
|
collaborationMode?: AgentCollaborationMode;
|
|
259
|
+
control?: "owned" | "attached";
|
|
257
260
|
status: AgentStatus;
|
|
258
261
|
archived: boolean;
|
|
259
262
|
lastMessagePreview?: string;
|
|
@@ -475,7 +478,9 @@ function normalizeToolStatus(value: unknown, completedFallback = false): AgentTo
|
|
|
475
478
|
|
|
476
479
|
function normalizePlanStatus(value: unknown): AgentPlanStep["status"] {
|
|
477
480
|
if (value === "completed" || value === "done") return "completed";
|
|
478
|
-
if (value === "inProgress" || value === "running" || value === "active")
|
|
481
|
+
if (value === "inProgress" || value === "in_progress" || value === "running" || value === "active") {
|
|
482
|
+
return "in_progress";
|
|
483
|
+
}
|
|
479
484
|
return "pending";
|
|
480
485
|
}
|
|
481
486
|
|
|
@@ -808,7 +813,11 @@ function contentBlocksFromItem(item: Record<string, unknown>): AgentContentBlock
|
|
|
808
813
|
return text ? [{ type: "text", text }] : [];
|
|
809
814
|
}
|
|
810
815
|
|
|
811
|
-
function protocolSupportsImages(
|
|
816
|
+
function protocolSupportsImages(
|
|
817
|
+
protocol: AgentProtocol | undefined,
|
|
818
|
+
advertised?: AdvertisedAcpCapabilities,
|
|
819
|
+
): boolean {
|
|
820
|
+
if (protocol === "acp") return Boolean(advertised?.images);
|
|
812
821
|
return protocol === "codex-app-server" ||
|
|
813
822
|
protocol === "claude-agent-sdk" ||
|
|
814
823
|
protocol === "claude-stream-json";
|
|
@@ -997,6 +1006,7 @@ interface ProviderRuntimeCapabilities {
|
|
|
997
1006
|
commands?: AgentCommandDescriptor[];
|
|
998
1007
|
modes?: AgentModeDescriptor[];
|
|
999
1008
|
currentMode?: string;
|
|
1009
|
+
advertised?: AdvertisedAcpCapabilities;
|
|
1000
1010
|
// MCP server connection status, keyed by server name (last-writer-wins across
|
|
1001
1011
|
// the init seed and live startupStatus events).
|
|
1002
1012
|
mcpServers?: Map<string, AgentMcpServerDescriptor>;
|
|
@@ -1668,6 +1678,226 @@ function codexSessionConfig(sessionId: string): {
|
|
|
1668
1678
|
return cfg;
|
|
1669
1679
|
}
|
|
1670
1680
|
|
|
1681
|
+
const CODEX_ROLLOUT_NAME =
|
|
1682
|
+
/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/;
|
|
1683
|
+
|
|
1684
|
+
function walkCodexRollouts(
|
|
1685
|
+
root: string,
|
|
1686
|
+
onFile: (path: string, filenameId: string, mtimeMs: number) => void,
|
|
1687
|
+
): void {
|
|
1688
|
+
if (!existsSync(root)) return;
|
|
1689
|
+
const walk = (dir: string): void => {
|
|
1690
|
+
let entries: string[];
|
|
1691
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
1692
|
+
for (const name of entries) {
|
|
1693
|
+
const path = join(dir, name);
|
|
1694
|
+
let stat;
|
|
1695
|
+
try { stat = statSync(path); } catch { continue; }
|
|
1696
|
+
if (stat.isDirectory()) { walk(path); continue; }
|
|
1697
|
+
const match = CODEX_ROLLOUT_NAME.exec(name);
|
|
1698
|
+
if (match?.[1]) onFile(path, match[1], stat.mtimeMs);
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
walk(root);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function findCodexRolloutPath(sessionId: string, root = join(homedir(), ".codex", "sessions")): string | undefined {
|
|
1705
|
+
const byName: { path: string; mt: number }[] = [];
|
|
1706
|
+
const recent: { path: string; mt: number }[] = [];
|
|
1707
|
+
walkCodexRollouts(root, (path, filenameId, mt) => {
|
|
1708
|
+
recent.push({ path, mt });
|
|
1709
|
+
if (filenameId === sessionId) byName.push({ path, mt });
|
|
1710
|
+
});
|
|
1711
|
+
if (byName.length > 0) {
|
|
1712
|
+
byName.sort((a, b) => b.mt - a.mt);
|
|
1713
|
+
return byName[0]?.path;
|
|
1714
|
+
}
|
|
1715
|
+
recent.sort((a, b) => b.mt - a.mt);
|
|
1716
|
+
for (const candidate of recent.slice(0, 80)) {
|
|
1717
|
+
try {
|
|
1718
|
+
const fdText = readFileSync(candidate.path, "utf8");
|
|
1719
|
+
const first = fdText.slice(0, fdText.indexOf("\n") >= 0 ? fdText.indexOf("\n") : fdText.length);
|
|
1720
|
+
const metaId = firstString(asRecord(asRecord(JSON.parse(first))?.payload), ["session_id", "id"]);
|
|
1721
|
+
if (metaId === sessionId) return candidate.path;
|
|
1722
|
+
} catch { /* skip */ }
|
|
1723
|
+
}
|
|
1724
|
+
return undefined;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function findLatestCodexRolloutForCwd(cwd: string, root = join(homedir(), ".codex", "sessions")): { path: string; sessionId: string } | undefined {
|
|
1728
|
+
const wanted = cwd.replace(/\/+$/, "");
|
|
1729
|
+
const candidates: { path: string; sessionId: string; mt: number }[] = [];
|
|
1730
|
+
walkCodexRollouts(root, (path, filenameId, mt) => {
|
|
1731
|
+
candidates.push({ path, sessionId: filenameId, mt });
|
|
1732
|
+
});
|
|
1733
|
+
candidates.sort((a, b) => b.mt - a.mt);
|
|
1734
|
+
for (const candidate of candidates.slice(0, 40)) {
|
|
1735
|
+
try {
|
|
1736
|
+
const text = readFileSync(candidate.path, "utf8");
|
|
1737
|
+
const first = text.slice(0, text.indexOf("\n") >= 0 ? text.indexOf("\n") : text.length);
|
|
1738
|
+
const payload = asRecord(asRecord(JSON.parse(first))?.payload);
|
|
1739
|
+
const fileCwd = firstString(payload, ["cwd"])?.replace(/\/+$/, "");
|
|
1740
|
+
const metaId = firstString(payload, ["session_id", "id"]) ?? candidate.sessionId;
|
|
1741
|
+
if (fileCwd === wanted) return { path: candidate.path, sessionId: metaId };
|
|
1742
|
+
} catch { continue; }
|
|
1743
|
+
}
|
|
1744
|
+
return undefined;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function publicWorkspaceRoots(roots: unknown): string[] | undefined {
|
|
1748
|
+
if (!Array.isArray(roots)) return undefined;
|
|
1749
|
+
const cleaned = roots
|
|
1750
|
+
.filter((root): root is string => typeof root === "string" && root.length > 0)
|
|
1751
|
+
.filter((root) => !root.includes("/.codex/visualizations/"));
|
|
1752
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
function isInjectedCodexUserText(text: string): boolean {
|
|
1756
|
+
const trimmed = text.trim();
|
|
1757
|
+
return (
|
|
1758
|
+
trimmed.startsWith("<app-context>") ||
|
|
1759
|
+
trimmed.startsWith("<recommended_plugins>") ||
|
|
1760
|
+
trimmed.startsWith("<skills_instructions>") ||
|
|
1761
|
+
trimmed.startsWith("# AGENTS.md") ||
|
|
1762
|
+
trimmed.startsWith("You are `/root`")
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function textsFromCodexContent(content: unknown): string[] {
|
|
1767
|
+
if (typeof content === "string" && content.trim()) return [content];
|
|
1768
|
+
if (!Array.isArray(content)) return [];
|
|
1769
|
+
const texts: string[] = [];
|
|
1770
|
+
for (const block of content) {
|
|
1771
|
+
const rec = asRecord(block);
|
|
1772
|
+
const text = typeof rec?.text === "string" ? rec.text : undefined;
|
|
1773
|
+
if (text?.trim()) texts.push(text);
|
|
1774
|
+
}
|
|
1775
|
+
return texts;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/** Incremental Codex jsonl parser so live TUI/desktop sessions can tail the file. */
|
|
1779
|
+
export class CodexRolloutParser {
|
|
1780
|
+
private toolNames = new Map<string, string>();
|
|
1781
|
+
private index = 0;
|
|
1782
|
+
workspaceRoots: string[] | undefined;
|
|
1783
|
+
sessionCwd: string | undefined;
|
|
1784
|
+
|
|
1785
|
+
consume(text: string, conversationId: string): AgentTimelineItem[] {
|
|
1786
|
+
const items: AgentTimelineItem[] = [];
|
|
1787
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1788
|
+
if (!line.trim()) continue;
|
|
1789
|
+
const next = this.consumeLine(line, conversationId);
|
|
1790
|
+
if (next) items.push(next);
|
|
1791
|
+
}
|
|
1792
|
+
return items;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
private consumeLine(line: string, conversationId: string): AgentTimelineItem | undefined {
|
|
1796
|
+
let record: Record<string, unknown> | undefined;
|
|
1797
|
+
try { record = asRecord(JSON.parse(line)); } catch { return undefined; }
|
|
1798
|
+
if (!record) return undefined;
|
|
1799
|
+
const payload = asRecord(record.payload) ?? record;
|
|
1800
|
+
const recordType = firstString(record, ["type"]);
|
|
1801
|
+
const payloadType = firstString(payload, ["type"]);
|
|
1802
|
+
const roots = publicWorkspaceRoots(payload.workspace_roots ?? payload.workspaceRoots);
|
|
1803
|
+
if (roots) this.workspaceRoots = roots;
|
|
1804
|
+
const cwd = firstString(payload, ["cwd"]);
|
|
1805
|
+
if (cwd) this.sessionCwd = cwd;
|
|
1806
|
+
const createdAt = parseTimestamp(record.timestamp ?? payload.timestamp) ?? Date.now();
|
|
1807
|
+
if (recordType === "response_item" && payloadType === "message") {
|
|
1808
|
+
const roleRaw = firstString(payload, ["role"]);
|
|
1809
|
+
if (roleRaw !== "user" && roleRaw !== "assistant") return undefined;
|
|
1810
|
+
const texts = textsFromCodexContent(payload.content).filter((text) =>
|
|
1811
|
+
roleRaw === "assistant" || !isInjectedCodexUserText(text),
|
|
1812
|
+
);
|
|
1813
|
+
if (texts.length === 0) return undefined;
|
|
1814
|
+
const text = texts.join("\n\n");
|
|
1815
|
+
this.index += 1;
|
|
1816
|
+
return {
|
|
1817
|
+
id: firstString(payload, ["id"]) ?? id(`codex-msg-${this.index}`),
|
|
1818
|
+
conversationId,
|
|
1819
|
+
type: "message",
|
|
1820
|
+
kind: "chat",
|
|
1821
|
+
role: roleRaw,
|
|
1822
|
+
text,
|
|
1823
|
+
content: [{ type: "text", text }],
|
|
1824
|
+
createdAt,
|
|
1825
|
+
updatedAt: createdAt,
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
if (recordType === "response_item" && payloadType === "reasoning") {
|
|
1829
|
+
const texts = textsFromCodexContent(payload.content ?? payload.summary);
|
|
1830
|
+
if (texts.length === 0) return undefined;
|
|
1831
|
+
this.index += 1;
|
|
1832
|
+
return {
|
|
1833
|
+
id: firstString(payload, ["id"]) ?? id(`codex-think-${this.index}`),
|
|
1834
|
+
conversationId,
|
|
1835
|
+
type: "status",
|
|
1836
|
+
kind: "thinking",
|
|
1837
|
+
text: texts.join("\n"),
|
|
1838
|
+
createdAt,
|
|
1839
|
+
updatedAt: createdAt,
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
if (recordType === "response_item" && (payloadType === "custom_tool_call" || payloadType === "function_call")) {
|
|
1843
|
+
const callId = firstString(payload, ["call_id", "id"]) ?? id(`codex-tool-${this.index}`);
|
|
1844
|
+
const name = firstString(payload, ["name"]) ?? "tool";
|
|
1845
|
+
this.toolNames.set(callId, name);
|
|
1846
|
+
const input = firstString(payload, ["input", "arguments"]) ?? stringify(payload.input ?? payload.arguments ?? "");
|
|
1847
|
+
this.index += 1;
|
|
1848
|
+
return {
|
|
1849
|
+
id: `tool:${callId}`,
|
|
1850
|
+
conversationId,
|
|
1851
|
+
type: "tool_call",
|
|
1852
|
+
kind: name === "exec" || name.includes("command") ? "command_execution" : "tool_activity",
|
|
1853
|
+
itemId: callId,
|
|
1854
|
+
toolCall: {
|
|
1855
|
+
id: callId,
|
|
1856
|
+
name,
|
|
1857
|
+
input,
|
|
1858
|
+
createdAt,
|
|
1859
|
+
status: "running",
|
|
1860
|
+
},
|
|
1861
|
+
createdAt,
|
|
1862
|
+
updatedAt: createdAt,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
if (recordType === "response_item" && (payloadType === "custom_tool_call_output" || payloadType === "function_call_output")) {
|
|
1866
|
+
const callId = firstString(payload, ["call_id", "id"]);
|
|
1867
|
+
if (!callId) return undefined;
|
|
1868
|
+
const outputTexts = textsFromCodexContent(payload.output);
|
|
1869
|
+
const output = outputTexts.join("\n") || stringify(payload.output ?? "");
|
|
1870
|
+
this.index += 1;
|
|
1871
|
+
return {
|
|
1872
|
+
id: `tool:${callId}`,
|
|
1873
|
+
conversationId,
|
|
1874
|
+
type: "tool_call",
|
|
1875
|
+
kind: "tool_activity",
|
|
1876
|
+
itemId: callId,
|
|
1877
|
+
toolCall: {
|
|
1878
|
+
id: callId,
|
|
1879
|
+
name: this.toolNames.get(callId) ?? "tool",
|
|
1880
|
+
output,
|
|
1881
|
+
createdAt,
|
|
1882
|
+
status: "completed",
|
|
1883
|
+
},
|
|
1884
|
+
createdAt,
|
|
1885
|
+
updatedAt: createdAt,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
return undefined;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/** Parse a Codex on-disk rollout into timeline items. App-server thread/read
|
|
1893
|
+
* only sees threads it owns; desktop/TUI sessions live in jsonl. */
|
|
1894
|
+
export function timelineItemsFromCodexRolloutText(
|
|
1895
|
+
text: string,
|
|
1896
|
+
conversationId: string,
|
|
1897
|
+
): AgentTimelineItem[] {
|
|
1898
|
+
return new CodexRolloutParser().consume(text, conversationId).slice(-MAX_TIMELINE_ITEMS);
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1671
1901
|
function parseRemoteSessions(value: unknown): Array<{
|
|
1672
1902
|
id: string;
|
|
1673
1903
|
cwd?: string;
|
|
@@ -2089,6 +2319,21 @@ export class AgentWorkspaceProxy {
|
|
|
2089
2319
|
// Echo the updated record so every client refreshes (the web store
|
|
2090
2320
|
// force-replaces by id on any event carrying `conversation`).
|
|
2091
2321
|
this.emitConversation(conversation);
|
|
2322
|
+
if (conversation.agentSessionId) {
|
|
2323
|
+
const client = this.clientForProvider(conversation.provider);
|
|
2324
|
+
if (client instanceof AcpClient && model?.trim() && client.advertised.setModel && conversation.provider !== "codex") {
|
|
2325
|
+
try {
|
|
2326
|
+
await client.setSessionModel({
|
|
2327
|
+
sessionId: conversation.agentSessionId,
|
|
2328
|
+
modelId: model.trim(),
|
|
2329
|
+
});
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
if (this.input.verbose) {
|
|
2332
|
+
process.stderr.write(`[agent:v2] session/set_model failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2092
2337
|
if (conversation.provider === "codex" && conversation.agentSessionId) {
|
|
2093
2338
|
const client = this.clientForProvider(conversation.provider);
|
|
2094
2339
|
const settingsChanged =
|
|
@@ -2182,6 +2427,14 @@ export class AgentWorkspaceProxy {
|
|
|
2182
2427
|
const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
|
|
2183
2428
|
const conversation = this.conversations.get(payload.conversationId);
|
|
2184
2429
|
if (!conversation) break;
|
|
2430
|
+
if (conversation.control === "attached" || this.attachedExternalCodex.has(conversation.id)) {
|
|
2431
|
+
this.rejectAgentAction(
|
|
2432
|
+
conversation,
|
|
2433
|
+
`${providerLabel(conversation.provider)} 这场会话在本机终端/桌面里运行。Web 无法停止它,请在那边操作。`,
|
|
2434
|
+
conversation.status,
|
|
2435
|
+
);
|
|
2436
|
+
break;
|
|
2437
|
+
}
|
|
2185
2438
|
const turnId = this.currentTurnIds.get(payload.conversationId);
|
|
2186
2439
|
if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
|
|
2187
2440
|
this.rejectAgentAction(
|
|
@@ -2249,8 +2502,18 @@ export class AgentWorkspaceProxy {
|
|
|
2249
2502
|
private fsSyncRunning = false;
|
|
2250
2503
|
private lastPushedConversationSignature: string | undefined;
|
|
2251
2504
|
private lastHydrateAt = new Map<string, number>();
|
|
2252
|
-
|
|
2253
|
-
private
|
|
2505
|
+
/** Session ids this LinkShell process created via app-server `newSession`. */
|
|
2506
|
+
private ownedCodexSessionIds = new Set<string>();
|
|
2507
|
+
/** Conversations attached to an external Codex TUI/desktop rollout (same file, not a copy). */
|
|
2508
|
+
private attachedExternalCodex = new Set<string>();
|
|
2509
|
+
private codexFollow = new Map<string, {
|
|
2510
|
+
path: string;
|
|
2511
|
+
offset: number;
|
|
2512
|
+
carry: string;
|
|
2513
|
+
parser: CodexRolloutParser;
|
|
2514
|
+
}>();
|
|
2515
|
+
private static readonly FS_SYNC_THROTTLE_MS = 400;
|
|
2516
|
+
private static readonly EXTERNAL_HYDRATE_THROTTLE_MS = 400;
|
|
2254
2517
|
|
|
2255
2518
|
private startSessionWatchers(): void {
|
|
2256
2519
|
const roots = [
|
|
@@ -2325,7 +2588,9 @@ export class AgentWorkspaceProxy {
|
|
|
2325
2588
|
this.lastHydrateAt.set(conversation.id, now);
|
|
2326
2589
|
try {
|
|
2327
2590
|
await this.hydrateConversationFromProvider(conversation, undefined, { rememberActiveTurn: false });
|
|
2328
|
-
this.
|
|
2591
|
+
if (this.followCodexRolloutDisk(conversation, true) > 0) {
|
|
2592
|
+
this.sendSnapshot(conversation.id);
|
|
2593
|
+
}
|
|
2329
2594
|
} catch (error) {
|
|
2330
2595
|
if (this.input.verbose) {
|
|
2331
2596
|
process.stderr.write(`[agent:v2] external hydrate failed for ${conversation.id}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -2455,11 +2720,17 @@ export class AgentWorkspaceProxy {
|
|
|
2455
2720
|
seenKeys.add(key);
|
|
2456
2721
|
this.processMissCounts.set(key, 0);
|
|
2457
2722
|
|
|
2458
|
-
|
|
2459
|
-
|
|
2723
|
+
let sessionId = snapshot.sessionId;
|
|
2724
|
+
if (!sessionId && snapshot.provider === "codex") {
|
|
2725
|
+
const matched = findLatestCodexRolloutForCwd(snapshot.cwd ?? this.input.cwd);
|
|
2726
|
+
if (matched) sessionId = matched.sessionId;
|
|
2727
|
+
}
|
|
2728
|
+
const existingId = sessionId
|
|
2729
|
+
? this.conversationByAgentSessionId.get(sessionId)
|
|
2460
2730
|
: undefined;
|
|
2461
|
-
const liveId = liveConversationId(snapshot);
|
|
2462
|
-
const conversationId = existingId
|
|
2731
|
+
const liveId = liveConversationId({ ...snapshot, sessionId });
|
|
2732
|
+
const conversationId = existingId
|
|
2733
|
+
?? (sessionId ? makeAgentV2RemoteConversationId(snapshot.provider, sessionId) : liveId);
|
|
2463
2734
|
const existing = this.conversations.get(conversationId);
|
|
2464
2735
|
const cwd = snapshot.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
2465
2736
|
const driving = this.currentTurnIds.has(conversationId);
|
|
@@ -2474,10 +2745,12 @@ export class AgentWorkspaceProxy {
|
|
|
2474
2745
|
existing.status = status;
|
|
2475
2746
|
existing.cwd = cwd;
|
|
2476
2747
|
if (!existing.title) existing.title = agentProviderLabel(snapshot.provider);
|
|
2477
|
-
if (
|
|
2478
|
-
existing.agentSessionId =
|
|
2479
|
-
this.conversationByAgentSessionId.set(
|
|
2748
|
+
if (sessionId && !existing.agentSessionId) {
|
|
2749
|
+
existing.agentSessionId = sessionId;
|
|
2750
|
+
this.conversationByAgentSessionId.set(sessionId, existing.id);
|
|
2480
2751
|
}
|
|
2752
|
+
this.attachedExternalCodex.add(existing.id);
|
|
2753
|
+
existing.control = "attached";
|
|
2481
2754
|
if (changed) {
|
|
2482
2755
|
listChanged = true;
|
|
2483
2756
|
this.emitConversation(existing);
|
|
@@ -2488,7 +2761,7 @@ export class AgentWorkspaceProxy {
|
|
|
2488
2761
|
const now = Date.now();
|
|
2489
2762
|
const conversation: AgentConversation = {
|
|
2490
2763
|
id: conversationId,
|
|
2491
|
-
agentSessionId:
|
|
2764
|
+
agentSessionId: sessionId,
|
|
2492
2765
|
provider: snapshot.provider,
|
|
2493
2766
|
cwd,
|
|
2494
2767
|
title: agentProviderLabel(snapshot.provider),
|
|
@@ -2497,9 +2770,11 @@ export class AgentWorkspaceProxy {
|
|
|
2497
2770
|
lastMessagePreview: "本机终端中的 Agent 进程",
|
|
2498
2771
|
lastActivityAt: now,
|
|
2499
2772
|
createdAt: now,
|
|
2773
|
+
control: "attached",
|
|
2500
2774
|
};
|
|
2501
2775
|
this.conversations.set(conversation.id, conversation);
|
|
2502
|
-
if (
|
|
2776
|
+
if (sessionId) this.conversationByAgentSessionId.set(sessionId, conversation.id);
|
|
2777
|
+
this.attachedExternalCodex.add(conversation.id);
|
|
2503
2778
|
this.emitConversation(conversation);
|
|
2504
2779
|
listChanged = true;
|
|
2505
2780
|
}
|
|
@@ -2693,7 +2968,14 @@ export class AgentWorkspaceProxy {
|
|
|
2693
2968
|
client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient,
|
|
2694
2969
|
protocol: AgentProtocol,
|
|
2695
2970
|
): Promise<void> {
|
|
2696
|
-
if (client instanceof AcpClient
|
|
2971
|
+
if (client instanceof AcpClient) {
|
|
2972
|
+
const prior = this.providerCapabilities.get(provider);
|
|
2973
|
+
this.providerCapabilities.set(provider, {
|
|
2974
|
+
...prior,
|
|
2975
|
+
advertised: client.advertised,
|
|
2976
|
+
});
|
|
2977
|
+
if (protocol !== "codex-app-server") return;
|
|
2978
|
+
}
|
|
2697
2979
|
const listModels = (client as { listModels?: () => Promise<unknown> }).listModels;
|
|
2698
2980
|
let runtimeCapabilities: ProviderRuntimeCapabilities | undefined;
|
|
2699
2981
|
if (typeof listModels === "function") {
|
|
@@ -2722,6 +3004,8 @@ export class AgentWorkspaceProxy {
|
|
|
2722
3004
|
commands: mergeCommands(runtimeCapabilities?.commands, skillCommands),
|
|
2723
3005
|
modes: runtimeCapabilities?.modes,
|
|
2724
3006
|
currentMode: runtimeCapabilities?.currentMode,
|
|
3007
|
+
advertised: this.providerCapabilities.get(provider)?.advertised
|
|
3008
|
+
?? (client instanceof AcpClient ? client.advertised : undefined),
|
|
2725
3009
|
// Preserve any MCP status already accumulated from startupStatus events.
|
|
2726
3010
|
mcpServers: runtimeCapabilities?.mcpServers ?? this.providerCapabilities.get(provider)?.mcpServers,
|
|
2727
3011
|
};
|
|
@@ -2745,7 +3029,12 @@ export class AgentWorkspaceProxy {
|
|
|
2745
3029
|
commands: mergeCommands(runtimeCapabilities.commands, skillCommands),
|
|
2746
3030
|
};
|
|
2747
3031
|
}
|
|
2748
|
-
this.providerCapabilities.set(provider,
|
|
3032
|
+
this.providerCapabilities.set(provider, {
|
|
3033
|
+
...runtimeCapabilities,
|
|
3034
|
+
advertised: runtimeCapabilities.advertised
|
|
3035
|
+
?? this.providerCapabilities.get(provider)?.advertised
|
|
3036
|
+
?? (client instanceof AcpClient ? client.advertised : undefined),
|
|
3037
|
+
});
|
|
2749
3038
|
}
|
|
2750
3039
|
if (listedMcp.length > 0) this.mergeMcpServers(provider, listedMcp);
|
|
2751
3040
|
}
|
|
@@ -2857,6 +3146,8 @@ export class AgentWorkspaceProxy {
|
|
|
2857
3146
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
2858
3147
|
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
2859
3148
|
usage,
|
|
3149
|
+
group: existing?.group ?? (provider === "claude" ? titleFromCwd(cwd) : existing?.group),
|
|
3150
|
+
workspaceRoots: existing?.workspaceRoots,
|
|
2860
3151
|
};
|
|
2861
3152
|
this.conversations.set(conversation.id, conversation);
|
|
2862
3153
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
@@ -2995,7 +3286,13 @@ export class AgentWorkspaceProxy {
|
|
|
2995
3286
|
if (turnCount > 0) this.historyCursors.set(conversation.id, String(turnCount));
|
|
2996
3287
|
}
|
|
2997
3288
|
|
|
2998
|
-
|
|
3289
|
+
let hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
|
|
3290
|
+
if (conversation.provider === "codex") {
|
|
3291
|
+
this.followCodexRolloutDisk(conversation, false);
|
|
3292
|
+
}
|
|
3293
|
+
if (hydratedItems.length === 0) {
|
|
3294
|
+
hydratedItems = this.timelines.get(conversation.id) ?? [];
|
|
3295
|
+
}
|
|
2999
3296
|
if (hydratedItems.length === 0) return;
|
|
3000
3297
|
const existing = this.timelines.get(conversation.id) ?? [];
|
|
3001
3298
|
const merged = new Map<string, AgentTimelineItem>();
|
|
@@ -3012,9 +3309,84 @@ export class AgentWorkspaceProxy {
|
|
|
3012
3309
|
const lastPreview = [...nextItems].reverse()
|
|
3013
3310
|
.map((item) => previewText(previewFromTimelineItem(item) ?? ""))
|
|
3014
3311
|
.find(Boolean);
|
|
3015
|
-
if (lastPreview
|
|
3016
|
-
|
|
3312
|
+
if (lastPreview) conversation.lastMessagePreview = lastPreview;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
/** Tail a Codex desktop/TUI rollout. `emit` sends agent.v2.event for new lines
|
|
3316
|
+
* so a phone/web client sees tokens as the jsonl grows, not only on open. */
|
|
3317
|
+
private followCodexRolloutDisk(conversation: AgentConversation, emit: boolean): number {
|
|
3318
|
+
if (conversation.provider !== "codex") return 0;
|
|
3319
|
+
let path = conversation.agentSessionId
|
|
3320
|
+
? findCodexRolloutPath(conversation.agentSessionId)
|
|
3321
|
+
: undefined;
|
|
3322
|
+
if (!path && conversation.cwd) {
|
|
3323
|
+
const matched = findLatestCodexRolloutForCwd(conversation.cwd);
|
|
3324
|
+
if (matched) {
|
|
3325
|
+
path = matched.path;
|
|
3326
|
+
if (!conversation.agentSessionId) {
|
|
3327
|
+
conversation.agentSessionId = matched.sessionId;
|
|
3328
|
+
this.conversationByAgentSessionId.set(matched.sessionId, conversation.id);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (!path) return 0;
|
|
3333
|
+
let follow = this.codexFollow.get(conversation.id);
|
|
3334
|
+
if (!follow || follow.path !== path) {
|
|
3335
|
+
follow = { path, offset: 0, carry: "", parser: new CodexRolloutParser() };
|
|
3336
|
+
this.codexFollow.set(conversation.id, follow);
|
|
3337
|
+
}
|
|
3338
|
+
let stat;
|
|
3339
|
+
try {
|
|
3340
|
+
stat = statSync(path);
|
|
3341
|
+
} catch {
|
|
3342
|
+
return 0;
|
|
3343
|
+
}
|
|
3344
|
+
if (stat.size < follow.offset) {
|
|
3345
|
+
follow.offset = 0;
|
|
3346
|
+
follow.carry = "";
|
|
3347
|
+
follow.parser = new CodexRolloutParser();
|
|
3348
|
+
}
|
|
3349
|
+
if (stat.size === follow.offset && !follow.carry) return 0;
|
|
3350
|
+
const length = stat.size - follow.offset;
|
|
3351
|
+
const buf = Buffer.alloc(length);
|
|
3352
|
+
const fd = openSync(path, "r");
|
|
3353
|
+
try {
|
|
3354
|
+
readSync(fd, buf, 0, length, follow.offset);
|
|
3355
|
+
} finally {
|
|
3356
|
+
closeSync(fd);
|
|
3357
|
+
}
|
|
3358
|
+
follow.offset = stat.size;
|
|
3359
|
+
const combined = follow.carry + buf.toString("utf8");
|
|
3360
|
+
const lastNl = combined.lastIndexOf("\n");
|
|
3361
|
+
if (lastNl < 0) {
|
|
3362
|
+
follow.carry = combined;
|
|
3363
|
+
return 0;
|
|
3364
|
+
}
|
|
3365
|
+
const complete = combined.slice(0, lastNl + 1);
|
|
3366
|
+
follow.carry = combined.slice(lastNl + 1);
|
|
3367
|
+
const items = follow.parser.consume(complete, conversation.id);
|
|
3368
|
+
if (items.length === 0) return 0;
|
|
3369
|
+
if (emit) {
|
|
3370
|
+
for (const item of items) this.upsertItem(conversation.id, item);
|
|
3371
|
+
} else {
|
|
3372
|
+
const existing = this.timelines.get(conversation.id) ?? [];
|
|
3373
|
+
const merged = new Map(existing.map((item) => [item.id, item] as const));
|
|
3374
|
+
for (const item of items) merged.set(item.id, item);
|
|
3375
|
+
const nextItems = [...merged.values()]
|
|
3376
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3377
|
+
.slice(-MAX_TIMELINE_ITEMS);
|
|
3378
|
+
this.timelines.set(conversation.id, nextItems);
|
|
3379
|
+
for (const item of nextItems) this.rememberItemConversationId(conversation.id, item);
|
|
3017
3380
|
}
|
|
3381
|
+
const timeline = this.timelines.get(conversation.id) ?? [];
|
|
3382
|
+
const lastPreview = [...timeline].reverse()
|
|
3383
|
+
.map((item) => previewText(previewFromTimelineItem(item) ?? ""))
|
|
3384
|
+
.find(Boolean);
|
|
3385
|
+
if (lastPreview) conversation.lastMessagePreview = lastPreview;
|
|
3386
|
+
conversation.lastActivityAt = Date.now();
|
|
3387
|
+
if (follow.parser.workspaceRoots?.length) conversation.workspaceRoots = follow.parser.workspaceRoots;
|
|
3388
|
+
if (follow.parser.sessionCwd && !conversation.cwd) conversation.cwd = follow.parser.sessionCwd;
|
|
3389
|
+
return items.length;
|
|
3018
3390
|
}
|
|
3019
3391
|
|
|
3020
3392
|
private sendCapabilities(): void {
|
|
@@ -3026,11 +3398,23 @@ export class AgentWorkspaceProxy {
|
|
|
3026
3398
|
const client = this.clients.get(provider);
|
|
3027
3399
|
const protocol = this.agentProtocols.get(provider);
|
|
3028
3400
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
3401
|
+
const advertised = runtimeCapabilities?.advertised
|
|
3402
|
+
?? (client instanceof AcpClient ? client.advertised : undefined);
|
|
3029
3403
|
const enabled = Boolean(client);
|
|
3030
|
-
const
|
|
3404
|
+
const isAcp = protocol === "acp";
|
|
3405
|
+
const supportsImages = enabled && protocolSupportsImages(protocol, advertised);
|
|
3031
3406
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
3032
3407
|
const supportsPermission = enabled && !isClaudeFallback;
|
|
3033
3408
|
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
3409
|
+
const supportsSessionList = enabled && (isAcp ? Boolean(advertised?.listSession) : !isClaudeFallback);
|
|
3410
|
+
const supportsSessionLoad = enabled && (isAcp ? Boolean(advertised?.loadSession) : !isClaudeFallback);
|
|
3411
|
+
const supportsCancel = enabled && (isAcp ? Boolean(advertised?.cancel) : true);
|
|
3412
|
+
const supportsFork = enabled && (isAcp
|
|
3413
|
+
? Boolean(advertised?.forkSession)
|
|
3414
|
+
: protocol === "codex-app-server" || protocol === "claude-agent-sdk");
|
|
3415
|
+
const supportsSetModel = enabled && (isAcp
|
|
3416
|
+
? Boolean(advertised?.setModel)
|
|
3417
|
+
: protocol === "codex-app-server");
|
|
3034
3418
|
const protocolBacked = isProtocolAgentProvider(provider);
|
|
3035
3419
|
const commands = mergeCommands(
|
|
3036
3420
|
defaultProviderCommands(provider, this.input.cwd, enabled),
|
|
@@ -3048,8 +3432,8 @@ export class AgentWorkspaceProxy {
|
|
|
3048
3432
|
: `${providerLabel(provider)} 已在本机检测;远程对话请在终端继续`,
|
|
3049
3433
|
supportsImages,
|
|
3050
3434
|
supportsPermission,
|
|
3051
|
-
supportsPlan: enabled,
|
|
3052
|
-
supportsCancel
|
|
3435
|
+
supportsPlan: enabled && (isAcp ? Boolean(advertised?.embeddedContext) : true),
|
|
3436
|
+
supportsCancel,
|
|
3053
3437
|
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
3054
3438
|
defaultModel: runtimeCapabilities?.defaultModel ?? "default",
|
|
3055
3439
|
reasoningEfforts: supportsReasoningEffort
|
|
@@ -3064,11 +3448,17 @@ export class AgentWorkspaceProxy {
|
|
|
3064
3448
|
currentMode,
|
|
3065
3449
|
features: {
|
|
3066
3450
|
images: supportsImages,
|
|
3451
|
+
audio: Boolean(advertised?.audio),
|
|
3067
3452
|
permissions: supportsPermission,
|
|
3068
|
-
plan: enabled,
|
|
3069
|
-
cancel:
|
|
3453
|
+
plan: enabled && (protocol === "acp" ? Boolean(advertised?.embeddedContext) : true),
|
|
3454
|
+
cancel: supportsCancel,
|
|
3070
3455
|
reasoningEffort: supportsReasoningEffort,
|
|
3071
3456
|
streamJsonFallback: isClaudeFallback,
|
|
3457
|
+
loadSession: supportsSessionLoad,
|
|
3458
|
+
sessionList: supportsSessionList,
|
|
3459
|
+
sessionFork: supportsFork,
|
|
3460
|
+
setModel: supportsSetModel,
|
|
3461
|
+
mcp: Boolean(advertised?.mcp) || Boolean(runtimeCapabilities?.mcpServers && runtimeCapabilities.mcpServers.size > 0),
|
|
3072
3462
|
},
|
|
3073
3463
|
mcpServers: runtimeCapabilities?.mcpServers && runtimeCapabilities.mcpServers.size > 0
|
|
3074
3464
|
? [...runtimeCapabilities.mcpServers.values()]
|
|
@@ -3087,13 +3477,13 @@ export class AgentWorkspaceProxy {
|
|
|
3087
3477
|
protocolVersion: 1,
|
|
3088
3478
|
workspaceProtocolVersion: 2,
|
|
3089
3479
|
error: anyEnabled ? undefined : "没有可远程驱动的 Agent。请安装 Claude Code 或 Codex CLI;其它 Agent 只要在本机终端运行就会出现在列表里。",
|
|
3090
|
-
supportsSessionList:
|
|
3091
|
-
supportsSessionLoad:
|
|
3480
|
+
supportsSessionList: providers.some((p) => p.features.sessionList),
|
|
3481
|
+
supportsSessionLoad: providers.some((p) => p.features.loadSession),
|
|
3092
3482
|
supportsImages: providers.some((p) => p.supportsImages),
|
|
3093
|
-
supportsAudio:
|
|
3483
|
+
supportsAudio: providers.some((p) => p.features.audio),
|
|
3094
3484
|
supportsPermission: anyPermission,
|
|
3095
|
-
supportsPlan:
|
|
3096
|
-
supportsCancel:
|
|
3485
|
+
supportsPlan: providers.some((p) => p.supportsPlan),
|
|
3486
|
+
supportsCancel: providers.some((p) => p.supportsCancel),
|
|
3097
3487
|
},
|
|
3098
3488
|
}));
|
|
3099
3489
|
}
|
|
@@ -3207,7 +3597,7 @@ export class AgentWorkspaceProxy {
|
|
|
3207
3597
|
} else {
|
|
3208
3598
|
this.emitNotice({ kind: "warning", title: "分叉失败", detail: "无法复制该会话的历史记录,已新建空会话。" });
|
|
3209
3599
|
}
|
|
3210
|
-
} else if (
|
|
3600
|
+
} else if (typeof forkThread === "function" && sourceSessionId) {
|
|
3211
3601
|
if (source?.cwd && !payload.cwd) cwd = source.cwd;
|
|
3212
3602
|
try {
|
|
3213
3603
|
const forked = await forkThread.call(client, {
|
|
@@ -3245,7 +3635,46 @@ export class AgentWorkspaceProxy {
|
|
|
3245
3635
|
}
|
|
3246
3636
|
}
|
|
3247
3637
|
|
|
3248
|
-
if (
|
|
3638
|
+
if (
|
|
3639
|
+
existingConversation &&
|
|
3640
|
+
!existingConversation.agentSessionId &&
|
|
3641
|
+
provider === "codex"
|
|
3642
|
+
) {
|
|
3643
|
+
const matched = findLatestCodexRolloutForCwd(existingConversation.cwd ?? cwd);
|
|
3644
|
+
if (matched) {
|
|
3645
|
+
existingConversation.agentSessionId = matched.sessionId;
|
|
3646
|
+
this.conversationByAgentSessionId.set(matched.sessionId, existingConversation.id);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
if (existingConversation && existingConversation.status !== "error") {
|
|
3651
|
+
const owned = Boolean(
|
|
3652
|
+
existingConversation.agentSessionId &&
|
|
3653
|
+
this.ownedCodexSessionIds.has(existingConversation.agentSessionId),
|
|
3654
|
+
);
|
|
3655
|
+
// External Codex (desktop/TUI) is the same session on disk. Attach by
|
|
3656
|
+
// tailing that rollout — never loadSession/newSession (those mint another thread).
|
|
3657
|
+
if (
|
|
3658
|
+
provider === "codex" &&
|
|
3659
|
+
!owned &&
|
|
3660
|
+
this.attachedExternalCodex.has(existingConversation.id) &&
|
|
3661
|
+
!this.currentTurnIds.has(existingConversation.id)
|
|
3662
|
+
) {
|
|
3663
|
+
existingConversation.control = "attached";
|
|
3664
|
+
this.followCodexRolloutDisk(existingConversation, false);
|
|
3665
|
+
this.activeConversationId = existingConversation.id;
|
|
3666
|
+
this.input.send(createEnvelope({
|
|
3667
|
+
type: "agent.v2.conversation.opened",
|
|
3668
|
+
sessionId: this.input.sessionId,
|
|
3669
|
+
payload: {
|
|
3670
|
+
conversation: existingConversation,
|
|
3671
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
3672
|
+
requestedConversationId: payload.conversationId,
|
|
3673
|
+
},
|
|
3674
|
+
}));
|
|
3675
|
+
return existingConversation;
|
|
3676
|
+
}
|
|
3677
|
+
if (existingConversation.agentSessionId) {
|
|
3249
3678
|
const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
|
|
3250
3679
|
if (
|
|
3251
3680
|
payload.conversationId &&
|
|
@@ -3288,6 +3717,7 @@ export class AgentWorkspaceProxy {
|
|
|
3288
3717
|
if (this.input.verbose) {
|
|
3289
3718
|
process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
3290
3719
|
}
|
|
3720
|
+
await this.hydrateConversationFromProvider(existingConversation, undefined);
|
|
3291
3721
|
}
|
|
3292
3722
|
}
|
|
3293
3723
|
this.activeConversationId = existingConversation.id;
|
|
@@ -3301,6 +3731,7 @@ export class AgentWorkspaceProxy {
|
|
|
3301
3731
|
},
|
|
3302
3732
|
}));
|
|
3303
3733
|
return existingConversation;
|
|
3734
|
+
}
|
|
3304
3735
|
}
|
|
3305
3736
|
|
|
3306
3737
|
try {
|
|
@@ -3318,6 +3749,7 @@ export class AgentWorkspaceProxy {
|
|
|
3318
3749
|
: await client.newSession({ cwd });
|
|
3319
3750
|
}
|
|
3320
3751
|
agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
|
|
3752
|
+
if (provider === "codex") this.ownedCodexSessionIds.add(agentSessionId);
|
|
3321
3753
|
const now = Date.now();
|
|
3322
3754
|
const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
|
|
3323
3755
|
const conversation: AgentConversation = {
|
|
@@ -3327,6 +3759,7 @@ export class AgentWorkspaceProxy {
|
|
|
3327
3759
|
provider,
|
|
3328
3760
|
cwd,
|
|
3329
3761
|
title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
|
|
3762
|
+
control: "owned",
|
|
3330
3763
|
model: payload.model ?? existingConversation?.model,
|
|
3331
3764
|
reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
|
|
3332
3765
|
permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
|
|
@@ -3475,11 +3908,19 @@ export class AgentWorkspaceProxy {
|
|
|
3475
3908
|
this.conversations.get(payload.conversationId) ??
|
|
3476
3909
|
await this.openConversation({ conversationId: payload.conversationId });
|
|
3477
3910
|
if (!conversation) return;
|
|
3911
|
+
if (conversation.control === "attached" || this.attachedExternalCodex.has(conversation.id)) {
|
|
3912
|
+
this.rejectAgentAction(
|
|
3913
|
+
conversation,
|
|
3914
|
+
`${providerLabel(conversation.provider)} 这场会话在本机终端/桌面里运行。Web 只旁观同一份记录,停止和插话请在那边操作。`,
|
|
3915
|
+
conversation.status,
|
|
3916
|
+
);
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3478
3919
|
if (!conversation.agentSessionId) {
|
|
3479
3920
|
this.rejectAgentAction(
|
|
3480
3921
|
conversation,
|
|
3481
3922
|
isLiveConversationId(conversation.id) || !isProtocolAgentProvider(conversation.provider)
|
|
3482
|
-
? `${providerLabel(conversation.provider)}
|
|
3923
|
+
? `${providerLabel(conversation.provider)} 正在本机终端中运行。请打开终端面板继续。`
|
|
3483
3924
|
: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
|
|
3484
3925
|
);
|
|
3485
3926
|
return;
|
|
@@ -3497,7 +3938,9 @@ export class AgentWorkspaceProxy {
|
|
|
3497
3938
|
}
|
|
3498
3939
|
|
|
3499
3940
|
const protocol = this.protocolForProvider(conversation.provider);
|
|
3500
|
-
|
|
3941
|
+
const advertised = this.providerCapabilities.get(conversation.provider)?.advertised
|
|
3942
|
+
?? (client instanceof AcpClient ? client.advertised : undefined);
|
|
3943
|
+
if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol, advertised)) {
|
|
3501
3944
|
this.rejectAgentAction(
|
|
3502
3945
|
conversation,
|
|
3503
3946
|
"当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
|
|
@@ -4589,32 +5032,96 @@ export class AgentWorkspaceProxy {
|
|
|
4589
5032
|
|
|
4590
5033
|
private handleSessionUpdate(params: unknown): void {
|
|
4591
5034
|
const raw = asRecord(params) ?? {};
|
|
4592
|
-
const
|
|
5035
|
+
const update = asRecord(raw.update) ?? raw;
|
|
5036
|
+
const conversationId = this.conversationIdFromParams(raw)
|
|
5037
|
+
?? this.conversationIdFromParams(update)
|
|
5038
|
+
?? this.fallbackConversationId();
|
|
5039
|
+
if (!conversationId) return;
|
|
5040
|
+
const kind = normalizedIdentifier(firstString(update, ["sessionUpdate", "session_update", "type"]));
|
|
5041
|
+
|
|
5042
|
+
if (kind === "agentthoughtchunk" || kind === "agentthought") {
|
|
5043
|
+
const contentBlock = asRecord(update.content);
|
|
5044
|
+
const thought = (typeof contentBlock?.text === "string" ? contentBlock.text : undefined)
|
|
5045
|
+
?? firstString(update, ["delta", "text", "content", "message"]);
|
|
5046
|
+
if (!thought) return;
|
|
5047
|
+
this.handleReasoningDelta({
|
|
5048
|
+
...raw,
|
|
5049
|
+
itemId: firstString(update, ["itemId", "id"]) ?? "thinking",
|
|
5050
|
+
delta: thought,
|
|
5051
|
+
});
|
|
5052
|
+
return;
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
if (kind === "toolcall" || kind === "toolcallupdate") {
|
|
5056
|
+
const toolCallId = firstString(update, ["toolCallId", "id"]) ?? id("tool");
|
|
5057
|
+
const existing = this.findTool(conversationId, toolCallId);
|
|
5058
|
+
const statusRaw = firstString(update, ["status"]);
|
|
5059
|
+
const status: AgentToolCall["status"] =
|
|
5060
|
+
statusRaw === "in_progress" ? "running"
|
|
5061
|
+
: statusRaw === "completed" || statusRaw === "failed" || statusRaw === "pending" || statusRaw === "running"
|
|
5062
|
+
? statusRaw
|
|
5063
|
+
: existing?.status ?? "running";
|
|
5064
|
+
const input = stringifyDefined(update.rawInput ?? update.input);
|
|
5065
|
+
const output = stringifyDefined(update.rawOutput ?? update.output);
|
|
5066
|
+
this.upsertTool(conversationId, {
|
|
5067
|
+
id: toolCallId,
|
|
5068
|
+
name: firstString(update, ["title", "kind", "name"]) ?? existing?.name ?? "tool",
|
|
5069
|
+
...(input ? { input } : {}),
|
|
5070
|
+
...(output ? { output } : {}),
|
|
5071
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
5072
|
+
status,
|
|
5073
|
+
});
|
|
5074
|
+
return;
|
|
5075
|
+
}
|
|
5076
|
+
|
|
5077
|
+
if (kind === "plan") {
|
|
5078
|
+
const entries = Array.isArray(update.entries) ? update.entries : [];
|
|
5079
|
+
this.handlePlanUpdated({
|
|
5080
|
+
...raw,
|
|
5081
|
+
plan: entries.map((entry) => {
|
|
5082
|
+
const step = asRecord(entry) ?? {};
|
|
5083
|
+
return {
|
|
5084
|
+
text: firstString(step, ["content", "text", "title", "description"]),
|
|
5085
|
+
status: step.status,
|
|
5086
|
+
};
|
|
5087
|
+
}),
|
|
5088
|
+
});
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
5091
|
+
|
|
5092
|
+
const nested = asRecord(raw.params) ?? asRecord(update.params) ?? {};
|
|
5093
|
+
const contentBlock = asRecord(update.content);
|
|
4593
5094
|
const text =
|
|
5095
|
+
firstString(update, ["delta", "text", "content", "message"]) ??
|
|
4594
5096
|
firstString(raw, ["delta", "text", "content", "message"]) ??
|
|
4595
|
-
firstString(nested, ["delta", "text", "content", "message"])
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
if (firstString(raw, ["toolName", "tool", "name"])) {
|
|
5097
|
+
firstString(nested, ["delta", "text", "content", "message"]) ??
|
|
5098
|
+
(typeof contentBlock?.text === "string" ? contentBlock.text : undefined);
|
|
5099
|
+
const content = contentBlocksFromItem(update).length > 0
|
|
5100
|
+
? contentBlocksFromItem(update)
|
|
5101
|
+
: contentBlocksFromItem(raw);
|
|
5102
|
+
if (firstString(raw, ["toolName", "tool", "name"]) || firstString(update, ["toolName", "tool", "name"])) {
|
|
4601
5103
|
this.upsertTool(conversationId, {
|
|
4602
|
-
id: firstString(
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
5104
|
+
id: firstString(update, ["toolCallId", "callId", "id"])
|
|
5105
|
+
?? firstString(raw, ["toolCallId", "callId", "id"])
|
|
5106
|
+
?? id("tool"),
|
|
5107
|
+
name: firstString(update, ["toolName", "tool", "name"])
|
|
5108
|
+
?? firstString(raw, ["toolName", "tool", "name"])
|
|
5109
|
+
?? "tool",
|
|
5110
|
+
input: stringify(update.input ?? update.toolInput ?? raw.input ?? raw.toolInput ?? ""),
|
|
5111
|
+
output: stringify(update.output ?? update.result ?? raw.output ?? raw.result ?? ""),
|
|
4606
5112
|
createdAt: Date.now(),
|
|
4607
|
-
status: raw.status === "completed" || raw.status === "failed" || raw.status === "running"
|
|
4608
|
-
? raw.status
|
|
5113
|
+
status: update.status === "completed" || raw.status === "completed" || update.status === "failed" || raw.status === "failed" || update.status === "running" || raw.status === "running"
|
|
5114
|
+
? ((update.status ?? raw.status) as AgentToolCall["status"])
|
|
4609
5115
|
: "running",
|
|
4610
5116
|
});
|
|
4611
5117
|
return;
|
|
4612
5118
|
}
|
|
5119
|
+
if (!text && content.length === 0) return;
|
|
4613
5120
|
const role = raw.role === "user" || raw.role === "system" ? raw.role : "assistant";
|
|
4614
5121
|
const blocks = content.length > 0 ? content : [{ type: "text" as const, text }];
|
|
4615
5122
|
const preview = textFromBlocks(blocks);
|
|
4616
5123
|
this.upsertItem(conversationId, {
|
|
4617
|
-
id: firstString(raw, ["messageId", "id"]) ?? id("msg"),
|
|
5124
|
+
id: firstString(update, ["messageId", "id"]) ?? firstString(raw, ["messageId", "id"]) ?? id("msg"),
|
|
4618
5125
|
conversationId,
|
|
4619
5126
|
type: "message",
|
|
4620
5127
|
role,
|
|
@@ -4622,7 +5129,7 @@ export class AgentWorkspaceProxy {
|
|
|
4622
5129
|
text: preview,
|
|
4623
5130
|
createdAt: Date.now(),
|
|
4624
5131
|
updatedAt: Date.now(),
|
|
4625
|
-
isStreaming: raw.done === false || raw.isStreaming === true,
|
|
5132
|
+
isStreaming: raw.done === false || raw.isStreaming === true || kind === "agentmessagechunk",
|
|
4626
5133
|
});
|
|
4627
5134
|
this.updateConversationPreview(conversationId, preview || "图片附件", raw.done === true ? "idle" : "running");
|
|
4628
5135
|
}
|