@tea-agent/loop-agent 0.40.0-next.4 → 0.40.0-next.6
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/CHANGELOG.md +20 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/shell-executor.js +4 -1
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/worker/console/chat/chat-event-store.js +62 -4
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +292 -29
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +14 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/routes.js +309 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +529 -186
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-DGt5TRsk.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DKBSJsXm.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-BnhooRBs.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-Bm9qkrJP.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-B2cFh4bO.js} +1 -1
- package/dist/worker/console/static/assets/channel-DSgFX07x.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DhvWRI03.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-chzTP_2w.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-SdWIR12m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C2-8Eo_A.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-DvbE1yQW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-CEenyPTz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-CcaZrq0k.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-FGngOTto.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-jT_yX3sf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-CjE9LQOM.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-unJObRls.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-unJObRls.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-GQWns1tb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CGRpxWNJ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-Cd3uFGE4.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-DmDHuUW0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-kAr--hF3.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-woahL2Oh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-7syNVaNf.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-BELPXiaY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-CrPa4aXi.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-B48u2VVe.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-DhBTfwTF.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CAeZ5nDU.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CkIFeFLN.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-t0HGYDel.js} +1 -1
- package/dist/worker/console/static/assets/index-B4l5ag79.js +407 -0
- package/dist/worker/console/static/assets/index-CwVHWB71.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-WM0HZhe4.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-CV5byHe0.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-lIyWkgnw.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-BIH4YNRY.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-lQ0ZGrWc.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-BYdXtke8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-k93vkIa4.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-Bm6AqvSy.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-DnJW6Xpf.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-CMzm4ObL.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-fwFF5dFQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-tzl9VDUc.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B26V09DD.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-D2npyCeP.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-CTJtkRU2.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-B8lk4Lzu.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BmOcMTJE.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-C3yP53hH.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Beah-dq0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-Bck7xjhe.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CCntr1w7.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-DsCqCBZz.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-Br2l90OM.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +26 -6
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +3 -2
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/docs/README.md +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/harness.json +3 -3
- package/package.json +3 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { aggregateUsage, } from "./usage.js";
|
|
2
|
+
function finiteNonNegative(value) {
|
|
3
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
4
|
+
? value
|
|
5
|
+
: undefined;
|
|
6
|
+
}
|
|
7
|
+
function eventTime(event) {
|
|
8
|
+
const value = Date.parse(event.at);
|
|
9
|
+
return Number.isFinite(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function usageSample(data) {
|
|
12
|
+
if (!data || typeof data !== "object")
|
|
13
|
+
return undefined;
|
|
14
|
+
const source = data;
|
|
15
|
+
const sample = {};
|
|
16
|
+
const numberFields = [
|
|
17
|
+
"inputTokens",
|
|
18
|
+
"outputTokens",
|
|
19
|
+
"cacheReadTokens",
|
|
20
|
+
"cacheWriteTokens",
|
|
21
|
+
"totalTokens",
|
|
22
|
+
"cost",
|
|
23
|
+
];
|
|
24
|
+
for (const field of numberFields) {
|
|
25
|
+
const value = finiteNonNegative(source[field]);
|
|
26
|
+
if (value !== undefined)
|
|
27
|
+
sample[field] = value;
|
|
28
|
+
}
|
|
29
|
+
if (typeof source.estimated === "boolean")
|
|
30
|
+
sample.estimated = source.estimated;
|
|
31
|
+
if (typeof source.responseKey === "string" && source.responseKey) {
|
|
32
|
+
sample.responseKey = source.responseKey;
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(sample).length > 0 ? sample : undefined;
|
|
35
|
+
}
|
|
36
|
+
function contextUsage(data) {
|
|
37
|
+
if (!data || typeof data !== "object")
|
|
38
|
+
return undefined;
|
|
39
|
+
const raw = data.contextUsage;
|
|
40
|
+
if (!raw || typeof raw !== "object")
|
|
41
|
+
return undefined;
|
|
42
|
+
const source = raw;
|
|
43
|
+
const contextWindow = finiteNonNegative(source.contextWindow);
|
|
44
|
+
if (contextWindow === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
const tokens = source.tokens === null ? null : finiteNonNegative(source.tokens) ?? null;
|
|
47
|
+
const percent = source.percent === null ? null : finiteNonNegative(source.percent) ?? null;
|
|
48
|
+
return { tokens, contextWindow, percent };
|
|
49
|
+
}
|
|
50
|
+
function stepTiming(data) {
|
|
51
|
+
if (!data || typeof data !== "object")
|
|
52
|
+
return undefined;
|
|
53
|
+
const raw = data.timing;
|
|
54
|
+
if (!raw || typeof raw !== "object")
|
|
55
|
+
return undefined;
|
|
56
|
+
const source = raw;
|
|
57
|
+
return {
|
|
58
|
+
llmMs: finiteNonNegative(source.llmMs),
|
|
59
|
+
ttftMs: finiteNonNegative(source.ttftMs),
|
|
60
|
+
decodeMs: finiteNonNegative(source.decodeMs),
|
|
61
|
+
outputTokens: finiteNonNegative(source.outputTokens),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fold the durable event log into the same whole-session statistics DSH shows
|
|
66
|
+
* below its composer. Missing timing evidence is omitted instead of inferred.
|
|
67
|
+
*/
|
|
68
|
+
export function deriveChatSessionTelemetry(events) {
|
|
69
|
+
const turns = new Set();
|
|
70
|
+
const stepStarts = new Map();
|
|
71
|
+
const toolStarts = new Map();
|
|
72
|
+
const usageSamples = [];
|
|
73
|
+
let latestContextUsage;
|
|
74
|
+
let steps = 0;
|
|
75
|
+
let llmMs = 0;
|
|
76
|
+
let toolMs = 0;
|
|
77
|
+
let ttftMs = 0;
|
|
78
|
+
let ttftSteps = 0;
|
|
79
|
+
let decodeMs = 0;
|
|
80
|
+
let decodeTokens = 0;
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
const at = eventTime(event);
|
|
83
|
+
if (event.kind === "usage") {
|
|
84
|
+
const sample = usageSample(event.data);
|
|
85
|
+
if (sample)
|
|
86
|
+
usageSamples.push(sample);
|
|
87
|
+
latestContextUsage = contextUsage(event.data) ?? latestContextUsage;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (event.kind === "agent_start") {
|
|
91
|
+
if (at !== undefined)
|
|
92
|
+
stepStarts.set(event.turnId, at);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (event.kind === "tool_call") {
|
|
96
|
+
const toolCallId = event.data.toolCallId;
|
|
97
|
+
if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
|
|
98
|
+
toolStarts.set(`${event.turnId}\u0000${toolCallId}`, at);
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (event.kind === "tool_result") {
|
|
103
|
+
const toolCallId = event.data.toolCallId;
|
|
104
|
+
if (typeof toolCallId === "string" && toolCallId && at !== undefined) {
|
|
105
|
+
const key = `${event.turnId}\u0000${toolCallId}`;
|
|
106
|
+
const startedAt = toolStarts.get(key);
|
|
107
|
+
if (startedAt !== undefined && at >= startedAt)
|
|
108
|
+
toolMs += at - startedAt;
|
|
109
|
+
toolStarts.delete(key);
|
|
110
|
+
// The next assistant step starts after the last completed tool.
|
|
111
|
+
stepStarts.set(event.turnId, at);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (event.kind !== "message_end")
|
|
116
|
+
continue;
|
|
117
|
+
turns.add(event.turnId);
|
|
118
|
+
steps += 1;
|
|
119
|
+
const recorded = stepTiming(event.data);
|
|
120
|
+
const startedAt = stepStarts.get(event.turnId);
|
|
121
|
+
if (recorded?.llmMs !== undefined)
|
|
122
|
+
llmMs += recorded.llmMs;
|
|
123
|
+
else if (at !== undefined && startedAt !== undefined && at >= startedAt) {
|
|
124
|
+
llmMs += at - startedAt;
|
|
125
|
+
}
|
|
126
|
+
if (recorded?.ttftMs !== undefined) {
|
|
127
|
+
ttftMs += recorded.ttftMs;
|
|
128
|
+
ttftSteps += 1;
|
|
129
|
+
}
|
|
130
|
+
if (recorded?.decodeMs !== undefined &&
|
|
131
|
+
recorded.decodeMs > 0 &&
|
|
132
|
+
recorded.outputTokens !== undefined) {
|
|
133
|
+
decodeMs += recorded.decodeMs;
|
|
134
|
+
decodeTokens += recorded.outputTokens;
|
|
135
|
+
}
|
|
136
|
+
stepStarts.delete(event.turnId);
|
|
137
|
+
}
|
|
138
|
+
const usage = aggregateUsage(usageSamples);
|
|
139
|
+
return {
|
|
140
|
+
stats: {
|
|
141
|
+
turns: turns.size,
|
|
142
|
+
steps,
|
|
143
|
+
llmMs,
|
|
144
|
+
toolMs,
|
|
145
|
+
ttftMs,
|
|
146
|
+
ttftSteps,
|
|
147
|
+
decodeMs,
|
|
148
|
+
decodeTokens,
|
|
149
|
+
},
|
|
150
|
+
usage: {
|
|
151
|
+
...usage,
|
|
152
|
+
...(latestContextUsage ? { contextUsage: latestContextUsage } : {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function billedInputTokens(usage) {
|
|
157
|
+
return usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
158
|
+
}
|
|
159
|
+
export function cacheHitPercent(usage) {
|
|
160
|
+
const billed = billedInputTokens(usage);
|
|
161
|
+
return billed > 0 ? Math.round((usage.cacheReadTokens / billed) * 100) : null;
|
|
162
|
+
}
|
|
163
|
+
export function formatSessionDuration(ms) {
|
|
164
|
+
const seconds = ms / 1_000;
|
|
165
|
+
if (seconds < 60)
|
|
166
|
+
return `${Math.round(seconds * 10) / 10}s`;
|
|
167
|
+
const whole = Math.round(seconds);
|
|
168
|
+
return `${Math.floor(whole / 60)}m${whole % 60}s`;
|
|
169
|
+
}
|
|
170
|
+
export function formatSessionTokens(tokens) {
|
|
171
|
+
const scaled = (value) => value >= 100
|
|
172
|
+
? String(Math.round(value))
|
|
173
|
+
: String(Math.round(value * 10) / 10);
|
|
174
|
+
if (tokens < 1_000)
|
|
175
|
+
return String(tokens);
|
|
176
|
+
if (tokens < 1_000_000)
|
|
177
|
+
return `${scaled(tokens / 1_000)}K`;
|
|
178
|
+
return `${scaled(tokens / 1_000_000)}M`;
|
|
179
|
+
}
|
|
@@ -17,7 +17,7 @@ import { dispatchOperatorAction } from "../operator-actions.js";
|
|
|
17
17
|
import { authorizeOperatorChatTool } from "./tools.js";
|
|
18
18
|
import { projectOperationForChat } from "./chat-event-store.js";
|
|
19
19
|
import { scrubSecrets } from "./explore-tools.js";
|
|
20
|
-
import { hashDeferredTurnPayload,
|
|
20
|
+
import { hashDeferredTurnPayload, parseDeferredTurns, } from "./deferred-turn.js";
|
|
21
21
|
const CHAT_TITLE_MAX_CHARS = 80;
|
|
22
22
|
/** Safe, one-line bounded title used for placeholders and model output. */
|
|
23
23
|
export function cleanChatTitle(value) {
|
|
@@ -54,7 +54,13 @@ export class ChatSessionStore {
|
|
|
54
54
|
const raw = await readJsonIfExists(recordPath(this.appData, sessionId));
|
|
55
55
|
if (!raw || raw.sessionId !== sessionId)
|
|
56
56
|
return undefined;
|
|
57
|
-
const
|
|
57
|
+
const deferredTurns = parseDeferredTurns(raw.deferredTurns);
|
|
58
|
+
const legacyDeferredTurn = parseDeferredTurns(raw.deferredTurn ? [raw.deferredTurn] : [])[0];
|
|
59
|
+
const normalizedDeferredTurns = deferredTurns.length > 0
|
|
60
|
+
? deferredTurns
|
|
61
|
+
: legacyDeferredTurn
|
|
62
|
+
? [legacyDeferredTurn]
|
|
63
|
+
: [];
|
|
58
64
|
return {
|
|
59
65
|
schemaVersion: 2,
|
|
60
66
|
sessionId,
|
|
@@ -69,7 +75,14 @@ export class ChatSessionStore {
|
|
|
69
75
|
...(typeof raw.modelProvider === "string" ? { modelProvider: raw.modelProvider } : {}),
|
|
70
76
|
...(typeof raw.modelId === "string" ? { modelId: raw.modelId } : {}),
|
|
71
77
|
...(typeof raw.thinkingLevel === "string" ? { thinkingLevel: raw.thinkingLevel } : {}),
|
|
72
|
-
...(
|
|
78
|
+
...(typeof raw.workspaceId === "string" && raw.workspaceId
|
|
79
|
+
? { workspaceId: raw.workspaceId }
|
|
80
|
+
: {}),
|
|
81
|
+
deferredTurns: normalizedDeferredTurns,
|
|
82
|
+
...(normalizedDeferredTurns[0] ? { deferredTurn: normalizedDeferredTurns[0] } : {}),
|
|
83
|
+
deferredQueueRevision: Number.isSafeInteger(raw.deferredQueueRevision) && (raw.deferredQueueRevision ?? 0) >= 0
|
|
84
|
+
? raw.deferredQueueRevision
|
|
85
|
+
: 0,
|
|
73
86
|
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
|
74
87
|
toolInvocations: Array.isArray(raw.toolInvocations) ? raw.toolInvocations : [],
|
|
75
88
|
};
|
|
@@ -125,6 +138,7 @@ export class ChatSessionStore {
|
|
|
125
138
|
repoFingerprint: this.appData.fingerprint,
|
|
126
139
|
repoRootDisplay: this.appData.repoRoot,
|
|
127
140
|
state: "active",
|
|
141
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
128
142
|
...(input.sessionFile ? { sessionFile: input.sessionFile } : {}),
|
|
129
143
|
createdAt: now,
|
|
130
144
|
updatedAt: now,
|
|
@@ -150,7 +164,8 @@ export class ChatSessionStore {
|
|
|
150
164
|
...(input.targetSessionFile ? { sessionFile: input.targetSessionFile } : { sessionFile: undefined }),
|
|
151
165
|
createdAt: now,
|
|
152
166
|
updatedAt: now,
|
|
153
|
-
|
|
167
|
+
deferredTurns: [],
|
|
168
|
+
deferredQueueRevision: 0,
|
|
154
169
|
messages: structuredClone(source.messages),
|
|
155
170
|
toolInvocations: structuredClone(source.toolInvocations),
|
|
156
171
|
};
|
|
@@ -232,35 +247,21 @@ export class ChatSessionStore {
|
|
|
232
247
|
return { ok: false, code: "INVALID_INPUT" };
|
|
233
248
|
}
|
|
234
249
|
const payloadHash = hashDeferredTurnPayload({ text, images: input.images });
|
|
235
|
-
const
|
|
250
|
+
const queue = [...(record.deferredTurns ?? [])];
|
|
251
|
+
const existingIndex = queue.findIndex((item) => item.clientRequestId === clientRequestId);
|
|
252
|
+
const existing = existingIndex >= 0 ? queue[existingIndex] : undefined;
|
|
236
253
|
if (existing) {
|
|
237
|
-
if (existing.
|
|
238
|
-
if (existing.payloadHash !== payloadHash) {
|
|
239
|
-
return {
|
|
240
|
-
ok: false,
|
|
241
|
-
code: "REQUEST_ID_REUSE_CONFLICT",
|
|
242
|
-
deferredTurn: existing,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
254
|
+
if (existing.payloadHash === payloadHash) {
|
|
245
255
|
return { ok: true, deferredTurn: existing, idempotent: true };
|
|
246
256
|
}
|
|
247
|
-
if (existing.state !== "queued") {
|
|
248
|
-
return {
|
|
249
|
-
ok: false,
|
|
250
|
-
code: "NOT_REPLACEABLE",
|
|
251
|
-
deferredTurn: existing,
|
|
252
|
-
};
|
|
257
|
+
if (existing.state !== "queued" && existing.state !== "failed") {
|
|
258
|
+
return { ok: false, code: "NOT_REPLACEABLE", deferredTurn: existing };
|
|
253
259
|
}
|
|
254
260
|
if (input.expectedRevision !== existing.revision) {
|
|
255
|
-
return {
|
|
256
|
-
ok: false,
|
|
257
|
-
code: "REVISION_CONFLICT",
|
|
258
|
-
deferredTurn: existing,
|
|
259
|
-
};
|
|
261
|
+
return { ok: false, code: "REVISION_CONFLICT", deferredTurn: existing };
|
|
260
262
|
}
|
|
261
263
|
}
|
|
262
|
-
else if (input.expectedRevision !== undefined &&
|
|
263
|
-
input.expectedRevision !== 0) {
|
|
264
|
+
else if (input.expectedRevision !== undefined && input.expectedRevision !== 0) {
|
|
264
265
|
return { ok: false, code: "REVISION_CONFLICT" };
|
|
265
266
|
}
|
|
266
267
|
const now = new Date().toISOString();
|
|
@@ -276,31 +277,41 @@ export class ChatSessionStore {
|
|
|
276
277
|
createdAt: existing?.createdAt ?? now,
|
|
277
278
|
updatedAt: now,
|
|
278
279
|
};
|
|
279
|
-
|
|
280
|
+
if (existingIndex >= 0)
|
|
281
|
+
queue[existingIndex] = deferredTurn;
|
|
282
|
+
else
|
|
283
|
+
queue.push(deferredTurn);
|
|
284
|
+
record.deferredTurns = queue;
|
|
285
|
+
record.deferredTurn = queue[0];
|
|
286
|
+
record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
|
|
280
287
|
record.updatedAt = now;
|
|
281
288
|
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
282
289
|
return { ok: true, deferredTurn, idempotent: false };
|
|
283
290
|
});
|
|
284
291
|
}
|
|
285
|
-
async
|
|
292
|
+
async listDeferredTurns(sessionId) {
|
|
293
|
+
const record = await this.get(sessionId);
|
|
294
|
+
return record ? [...(record.deferredTurns ?? [])] : [];
|
|
295
|
+
}
|
|
296
|
+
async claimDeferredTurn(sessionId, expectedRevision, clientRequestId) {
|
|
286
297
|
return this.serialize(sessionId, async () => {
|
|
287
298
|
const record = await this.get(sessionId);
|
|
288
|
-
const
|
|
299
|
+
const queue = [...(record?.deferredTurns ?? [])];
|
|
300
|
+
const index = clientRequestId
|
|
301
|
+
? queue.findIndex((item) => item.clientRequestId === clientRequestId)
|
|
302
|
+
: queue.findIndex((item) => item.state === "queued");
|
|
303
|
+
const existing = index >= 0 ? queue[index] : undefined;
|
|
289
304
|
if (!record || !existing)
|
|
290
305
|
return { ok: false, code: "NOT_FOUND" };
|
|
291
|
-
if (existing.revision !== expectedRevision)
|
|
306
|
+
if (existing.revision !== expectedRevision)
|
|
292
307
|
return { ok: false, code: "REVISION_CONFLICT", deferredTurn: existing };
|
|
293
|
-
|
|
294
|
-
if (existing.state !== "queued") {
|
|
308
|
+
if (existing.state !== "queued")
|
|
295
309
|
return { ok: false, code: "NOT_CLAIMABLE", deferredTurn: existing };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
updatedAt: new Date().toISOString(),
|
|
302
|
-
};
|
|
303
|
-
record.deferredTurn = next;
|
|
310
|
+
const next = { ...existing, revision: existing.revision + 1, state: "dispatching", updatedAt: new Date().toISOString() };
|
|
311
|
+
queue[index] = next;
|
|
312
|
+
record.deferredTurns = queue;
|
|
313
|
+
record.deferredTurn = queue[0];
|
|
314
|
+
record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
|
|
304
315
|
record.updatedAt = next.updatedAt;
|
|
305
316
|
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
306
317
|
return { ok: true, deferredTurn: next };
|
|
@@ -309,20 +320,20 @@ export class ChatSessionStore {
|
|
|
309
320
|
async transitionDeferredTurn(sessionId, input) {
|
|
310
321
|
return this.serialize(sessionId, async () => {
|
|
311
322
|
const record = await this.get(sessionId);
|
|
312
|
-
const
|
|
323
|
+
const queue = [...(record?.deferredTurns ?? [])];
|
|
324
|
+
const index = input.clientRequestId
|
|
325
|
+
? queue.findIndex((item) => item.clientRequestId === input.clientRequestId)
|
|
326
|
+
: queue.findIndex((item) => item.revision === input.expectedRevision);
|
|
327
|
+
const existing = index >= 0 ? queue[index] : undefined;
|
|
313
328
|
if (!record || !existing || existing.revision !== input.expectedRevision)
|
|
314
329
|
return undefined;
|
|
315
|
-
const next = {
|
|
316
|
-
...existing,
|
|
317
|
-
revision: existing.revision + 1,
|
|
318
|
-
state: input.state,
|
|
319
|
-
updatedAt: new Date().toISOString(),
|
|
320
|
-
...(input.acceptedTurnId ? { acceptedTurnId: input.acceptedTurnId } : {}),
|
|
321
|
-
...(input.lastError ? { lastError: input.lastError } : {}),
|
|
322
|
-
};
|
|
330
|
+
const next = { ...existing, revision: existing.revision + 1, state: input.state, updatedAt: new Date().toISOString(), ...(input.acceptedTurnId ? { acceptedTurnId: input.acceptedTurnId } : {}), ...(input.lastError ? { lastError: input.lastError } : {}) };
|
|
323
331
|
if (!input.lastError)
|
|
324
332
|
delete next.lastError;
|
|
325
|
-
|
|
333
|
+
queue[index] = next;
|
|
334
|
+
record.deferredTurns = queue;
|
|
335
|
+
record.deferredTurn = queue[0];
|
|
336
|
+
record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
|
|
326
337
|
record.updatedAt = next.updatedAt;
|
|
327
338
|
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
328
339
|
return next;
|
|
@@ -331,17 +342,19 @@ export class ChatSessionStore {
|
|
|
331
342
|
async deleteDeferredTurn(sessionId, input) {
|
|
332
343
|
return this.serialize(sessionId, async () => {
|
|
333
344
|
const record = await this.get(sessionId);
|
|
334
|
-
const
|
|
335
|
-
|
|
345
|
+
const queue = [...(record?.deferredTurns ?? [])];
|
|
346
|
+
const index = queue.findIndex((item) => item.clientRequestId === input.clientRequestId);
|
|
347
|
+
const existing = index >= 0 ? queue[index] : undefined;
|
|
348
|
+
if (!record || !existing)
|
|
336
349
|
return { ok: false, code: "NOT_FOUND" };
|
|
337
|
-
|
|
338
|
-
if (existing.state !== "queued" && existing.state !== "failed") {
|
|
350
|
+
if (existing.state !== "queued" && existing.state !== "failed")
|
|
339
351
|
return { ok: false, code: "NOT_CANCELLABLE", deferredTurn: existing };
|
|
340
|
-
|
|
341
|
-
if (existing.revision !== input.expectedRevision) {
|
|
352
|
+
if (existing.revision !== input.expectedRevision)
|
|
342
353
|
return { ok: false, code: "REVISION_CONFLICT", deferredTurn: existing };
|
|
343
|
-
|
|
344
|
-
|
|
354
|
+
queue.splice(index, 1);
|
|
355
|
+
record.deferredTurns = queue;
|
|
356
|
+
record.deferredTurn = queue[0];
|
|
357
|
+
record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
|
|
345
358
|
record.updatedAt = new Date().toISOString();
|
|
346
359
|
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
347
360
|
return { ok: true, deferredTurn: existing };
|
|
@@ -350,11 +363,15 @@ export class ChatSessionStore {
|
|
|
350
363
|
async clearDeferredTurnForRequest(sessionId, clientRequestId) {
|
|
351
364
|
return this.serialize(sessionId, async () => {
|
|
352
365
|
const record = await this.get(sessionId);
|
|
353
|
-
const
|
|
354
|
-
|
|
366
|
+
const queue = [...(record?.deferredTurns ?? [])];
|
|
367
|
+
const index = queue.findIndex((item) => item.clientRequestId === clientRequestId);
|
|
368
|
+
const existing = index >= 0 ? queue[index] : undefined;
|
|
369
|
+
if (!record || !existing)
|
|
355
370
|
return undefined;
|
|
356
|
-
|
|
357
|
-
|
|
371
|
+
queue.splice(index, 1);
|
|
372
|
+
record.deferredTurns = queue;
|
|
373
|
+
record.deferredTurn = queue[0];
|
|
374
|
+
record.deferredQueueRevision = (record.deferredQueueRevision ?? 0) + 1;
|
|
358
375
|
record.updatedAt = new Date().toISOString();
|
|
359
376
|
await writeSecureJson(recordPath(this.appData, sessionId), record);
|
|
360
377
|
return { deferredTurn: existing, clearedRevision: existing.revision + 1 };
|
|
@@ -561,9 +578,22 @@ export async function runChatTurn(options) {
|
|
|
561
578
|
}
|
|
562
579
|
case "error":
|
|
563
580
|
turnError = { code: "CHAT_TURN_ERROR", message: event.message };
|
|
581
|
+
pending.push(store.appendMessage(sessionId, {
|
|
582
|
+
role: "assistant",
|
|
583
|
+
kind: "provider-error",
|
|
584
|
+
text: event.providerError.message,
|
|
585
|
+
providerError: event.providerError,
|
|
586
|
+
...(event.content && event.content.length > 0
|
|
587
|
+
? { content: event.content }
|
|
588
|
+
: {}),
|
|
589
|
+
}));
|
|
564
590
|
break;
|
|
565
591
|
}
|
|
566
|
-
}, {
|
|
592
|
+
}, {
|
|
593
|
+
signal: options.signal,
|
|
594
|
+
images: options.images,
|
|
595
|
+
...(options.turnId ? { turnId: options.turnId } : {}),
|
|
596
|
+
});
|
|
567
597
|
}
|
|
568
598
|
catch (error) {
|
|
569
599
|
await Promise.allSettled(pending);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export function normalizeTodos(raw) {
|
|
2
|
+
if (!Array.isArray(raw))
|
|
3
|
+
throw new Error("todos must be an array");
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
return raw.map((item, index) => {
|
|
6
|
+
if (!item || typeof item !== "object")
|
|
7
|
+
throw new Error(`todo item ${index + 1} must be an object`);
|
|
8
|
+
const value = item;
|
|
9
|
+
const keys = Object.keys(value);
|
|
10
|
+
if (keys.some((key) => key !== "content" && key !== "status"))
|
|
11
|
+
throw new Error(`todo item ${index + 1} contains unknown fields`);
|
|
12
|
+
const content = typeof value.content === "string" ? value.content.trim() : "";
|
|
13
|
+
if (!content)
|
|
14
|
+
throw new Error(`todo item ${index + 1} content must not be empty`);
|
|
15
|
+
if (seen.has(content))
|
|
16
|
+
throw new Error(`duplicate todo content: ${content}`);
|
|
17
|
+
seen.add(content);
|
|
18
|
+
if (value.status !== "pending" && value.status !== "in_progress" && value.status !== "completed")
|
|
19
|
+
throw new Error(`todo item ${index + 1} has invalid status`);
|
|
20
|
+
return { content, status: value.status };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function countTodos(todos) {
|
|
24
|
+
return {
|
|
25
|
+
pending: todos.filter((todo) => todo.status === "pending").length,
|
|
26
|
+
inProgress: todos.filter((todo) => todo.status === "in_progress").length,
|
|
27
|
+
completed: todos.filter((todo) => todo.status === "completed").length,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export class TodoListRegistry {
|
|
31
|
+
current = new Map();
|
|
32
|
+
turns = new Map();
|
|
33
|
+
listeners = new Set();
|
|
34
|
+
subscribe(listener) {
|
|
35
|
+
this.listeners.add(listener);
|
|
36
|
+
return () => this.listeners.delete(listener);
|
|
37
|
+
}
|
|
38
|
+
startTurn(sessionId, turnId) {
|
|
39
|
+
this.turns.set(sessionId, turnId);
|
|
40
|
+
this.current.delete(sessionId);
|
|
41
|
+
this.publish({ type: "cleared", sessionId, turnId, todos: [] });
|
|
42
|
+
}
|
|
43
|
+
write(sessionId, toolCallId, raw) {
|
|
44
|
+
const todos = normalizeTodos(raw);
|
|
45
|
+
this.current.set(sessionId, todos);
|
|
46
|
+
this.publish({
|
|
47
|
+
type: "updated",
|
|
48
|
+
sessionId,
|
|
49
|
+
turnId: this.turns.get(sessionId),
|
|
50
|
+
toolCallId,
|
|
51
|
+
todos,
|
|
52
|
+
});
|
|
53
|
+
return { todos, counts: countTodos(todos) };
|
|
54
|
+
}
|
|
55
|
+
get(sessionId) {
|
|
56
|
+
const todos = this.current.get(sessionId);
|
|
57
|
+
return todos ? todos.map((todo) => ({ ...todo })) : null;
|
|
58
|
+
}
|
|
59
|
+
clear(sessionId) {
|
|
60
|
+
this.current.delete(sessionId);
|
|
61
|
+
this.turns.delete(sessionId);
|
|
62
|
+
}
|
|
63
|
+
publish(change) {
|
|
64
|
+
for (const listener of this.listeners) {
|
|
65
|
+
try {
|
|
66
|
+
listener(change);
|
|
67
|
+
}
|
|
68
|
+
catch { /* observers must not break tool execution */ }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function projectCurrentTodos(events) {
|
|
73
|
+
let current = null;
|
|
74
|
+
for (const event of events) {
|
|
75
|
+
if (event.kind === "todo_cleared")
|
|
76
|
+
current = null;
|
|
77
|
+
if (event.kind === "todo_updated") {
|
|
78
|
+
const data = event.data;
|
|
79
|
+
try {
|
|
80
|
+
current = normalizeTodos(data.todos);
|
|
81
|
+
}
|
|
82
|
+
catch { /* ignore malformed historical data */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return current;
|
|
86
|
+
}
|
|
87
|
+
export function buildTodoCompactionInstructions(rawInstructions, todos) {
|
|
88
|
+
const todoReminder = todos && todos.length > 0
|
|
89
|
+
? [
|
|
90
|
+
"Preserve the current Todo plan in the compaction summary and continuation context. Do not drop or invent tasks. Current whole-list state:",
|
|
91
|
+
JSON.stringify(todos),
|
|
92
|
+
"Keep each item's content and status (pending, in_progress, completed).",
|
|
93
|
+
].join("\n")
|
|
94
|
+
: undefined;
|
|
95
|
+
return [rawInstructions?.trim(), todoReminder]
|
|
96
|
+
.filter((value) => Boolean(value))
|
|
97
|
+
.join("\n\n") || undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Ephemeral model context used after a successful Pi compaction in the same
|
|
101
|
+
* turn. The Todo event remains the durable/UI source of truth; this text only
|
|
102
|
+
* prevents the compacted continuation from losing the current whole-list
|
|
103
|
+
* state. A later turn/start clears the registry, matching DSH's lifetime.
|
|
104
|
+
*/
|
|
105
|
+
export function buildTodoContinuationContext(todos) {
|
|
106
|
+
if (!todos || todos.length === 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
return [
|
|
109
|
+
"<current_todo_plan>",
|
|
110
|
+
"Continue the active turn using this exact current Todo whole-list state after context compaction. Do not drop, rename, or invent tasks.",
|
|
111
|
+
JSON.stringify(todos),
|
|
112
|
+
"Preserve each item's content and status (pending, in_progress, completed).",
|
|
113
|
+
"</current_todo_plan>",
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
@@ -141,6 +141,48 @@ export function getToolPreview(args, options) {
|
|
|
141
141
|
return "";
|
|
142
142
|
return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
|
|
143
143
|
}
|
|
144
|
+
/** Stable DSH-style tool row identity; unknown tools keep their real name. */
|
|
145
|
+
export function getToolPresentation(toolName) {
|
|
146
|
+
const raw = toolName?.trim() || "Tool";
|
|
147
|
+
const name = raw.toLowerCase();
|
|
148
|
+
if (name === "bash" || name === "shell" || name.includes("exec_command")) {
|
|
149
|
+
return { kind: "bash", label: "Bash" };
|
|
150
|
+
}
|
|
151
|
+
if (name === "read" ||
|
|
152
|
+
name === "read_file" ||
|
|
153
|
+
name.startsWith("read_") ||
|
|
154
|
+
name.includes("view_file")) {
|
|
155
|
+
return { kind: "read", label: "Read" };
|
|
156
|
+
}
|
|
157
|
+
if (name === "search" ||
|
|
158
|
+
name === "grep" ||
|
|
159
|
+
name === "glob" ||
|
|
160
|
+
name.includes("search") ||
|
|
161
|
+
name.includes("codegraph") ||
|
|
162
|
+
name.includes("lsp")) {
|
|
163
|
+
return { kind: "search", label: name === "glob" ? "Glob" : "Search" };
|
|
164
|
+
}
|
|
165
|
+
if (name === "write" ||
|
|
166
|
+
name === "edit" ||
|
|
167
|
+
name.includes("apply_patch") ||
|
|
168
|
+
name.includes("write_file") ||
|
|
169
|
+
name.includes("edit_file")) {
|
|
170
|
+
return {
|
|
171
|
+
kind: "write",
|
|
172
|
+
label: name === "write" || name.includes("write_file") ? "Write" : "Edit",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (name === "web" ||
|
|
176
|
+
name.includes("browser") ||
|
|
177
|
+
name.includes("fetch") ||
|
|
178
|
+
name.includes("web_")) {
|
|
179
|
+
return { kind: "web", label: "Web" };
|
|
180
|
+
}
|
|
181
|
+
if (name === "mcp" || name.startsWith("mcp_") || name.includes("__")) {
|
|
182
|
+
return { kind: "mcp", label: name === "mcp" ? "MCP" : raw };
|
|
183
|
+
}
|
|
184
|
+
return { kind: "tool", label: raw };
|
|
185
|
+
}
|
|
144
186
|
/** Section labels for ToolCard expand body. */
|
|
145
187
|
export function getToolSectionLabels(toolName) {
|
|
146
188
|
const name = (toolName ?? "").toLowerCase();
|