@nuvin/session 0.1.0-rc.5
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/chunk-3IPZO7LP.js +66 -0
- package/dist/chunk-7G3R25NS.js +6 -0
- package/dist/chunk-DGC7JSCG.js +0 -0
- package/dist/chunk-FR3R66RD.js +678 -0
- package/dist/chunk-UVJIG4DT.js +17 -0
- package/dist/client/directory.d.ts +141 -0
- package/dist/client/directory.d.ts.map +1 -0
- package/dist/client/endpoint.d.ts +33 -0
- package/dist/client/endpoint.d.ts.map +1 -0
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +1060 -0
- package/dist/client/session-client.d.ts +60 -0
- package/dist/client/session-client.d.ts.map +1 -0
- package/dist/client/socket.d.ts +38 -0
- package/dist/client/socket.d.ts.map +1 -0
- package/dist/client/transport.d.ts +9 -0
- package/dist/client/transport.d.ts.map +1 -0
- package/dist/client/uds-socket.d.ts +23 -0
- package/dist/client/uds-socket.d.ts.map +1 -0
- package/dist/client/websocket.d.ts +54 -0
- package/dist/client/websocket.d.ts.map +1 -0
- package/dist/controller/agent-channel.d.ts +58 -0
- package/dist/controller/agent-channel.d.ts.map +1 -0
- package/dist/controller/index.d.ts +3 -0
- package/dist/controller/index.d.ts.map +1 -0
- package/dist/controller/index.js +441 -0
- package/dist/controller/session-controller.d.ts +147 -0
- package/dist/controller/session-controller.d.ts.map +1 -0
- package/dist/controller/test-utils.d.ts +15 -0
- package/dist/controller/test-utils.d.ts.map +1 -0
- package/dist/grant/index.d.ts +33 -0
- package/dist/grant/index.d.ts.map +1 -0
- package/dist/grant/index.js +12 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +7 -0
- package/dist/protocol/types.d.ts +855 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/version.d.ts +8 -0
- package/dist/protocol/version.d.ts.map +1 -0
- package/dist/state/approvals.d.ts +34 -0
- package/dist/state/approvals.d.ts.map +1 -0
- package/dist/state/dir-access.d.ts +9 -0
- package/dist/state/dir-access.d.ts.map +1 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +48 -0
- package/dist/state/json.d.ts +7 -0
- package/dist/state/json.d.ts.map +1 -0
- package/dist/state/messages.d.ts +105 -0
- package/dist/state/messages.d.ts.map +1 -0
- package/dist/state/session.d.ts +9 -0
- package/dist/state/session.d.ts.map +1 -0
- package/dist/state/tool-preview.d.ts +31 -0
- package/dist/state/tool-preview.d.ts.map +1 -0
- package/dist/state/tool-preview.js +300 -0
- package/dist/state/workflow-view.d.ts +10 -0
- package/dist/state/workflow-view.d.ts.map +1 -0
- package/dist/test-utils/fake-relay.d.ts +24 -0
- package/dist/test-utils/fake-relay.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +2 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/index.js +250 -0
- package/dist/ui/history-grouping.d.ts +36 -0
- package/dist/ui/history-grouping.d.ts.map +1 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +135 -0
- package/dist/ui/picker-rows.d.ts +62 -0
- package/dist/ui/picker-rows.d.ts.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
// src/state/approvals.ts
|
|
2
|
+
var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
3
|
+
"FileRead",
|
|
4
|
+
"ViewFile",
|
|
5
|
+
"Ls",
|
|
6
|
+
"Grep",
|
|
7
|
+
"Glob",
|
|
8
|
+
"LoadSkill",
|
|
9
|
+
"Lsp",
|
|
10
|
+
"WebFetch",
|
|
11
|
+
"WebSearch"
|
|
12
|
+
]);
|
|
13
|
+
var BACKGROUNDLESS_TOOLS = /* @__PURE__ */ new Set([...READ_ONLY_TOOLS, "FileNew"]);
|
|
14
|
+
var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set(["AskUserTool", "TodoWrite", "UpdateTopic"]);
|
|
15
|
+
var AUTO_APPROVED_TOOLS = /* @__PURE__ */ new Set([...READ_ONLY_TOOLS, ...INTERACTIVE_TOOLS]);
|
|
16
|
+
function isAutoApprovedTool(toolName) {
|
|
17
|
+
return AUTO_APPROVED_TOOLS.has(toolName);
|
|
18
|
+
}
|
|
19
|
+
function createApprovalQueueState() {
|
|
20
|
+
return { active: null, pending: [] };
|
|
21
|
+
}
|
|
22
|
+
function enqueueApproval(state, approval) {
|
|
23
|
+
if (!state.active) {
|
|
24
|
+
return { ...state, active: approval };
|
|
25
|
+
}
|
|
26
|
+
return { ...state, pending: [...state.pending, approval] };
|
|
27
|
+
}
|
|
28
|
+
function dequeueActiveApproval(state) {
|
|
29
|
+
const [nextApproval, ...remainingApprovals] = state.pending;
|
|
30
|
+
return { active: nextApproval ?? null, pending: remainingApprovals };
|
|
31
|
+
}
|
|
32
|
+
function removeApproval(state, matches) {
|
|
33
|
+
if (state.active && matches(state.active)) {
|
|
34
|
+
return dequeueActiveApproval(state);
|
|
35
|
+
}
|
|
36
|
+
const pending = state.pending.filter((item) => !matches(item));
|
|
37
|
+
return pending.length === state.pending.length ? state : { ...state, pending };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/state/json.ts
|
|
41
|
+
function asJsonObject(value) {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/state/messages.ts
|
|
46
|
+
import { getReasoningTextFromMessage, getTextFromMessage } from "@nuvin/agent-core/formats";
|
|
47
|
+
function createMessageId(state) {
|
|
48
|
+
return `message-${state.nextId}`;
|
|
49
|
+
}
|
|
50
|
+
function withUpdatedMessage(state, id, update) {
|
|
51
|
+
return {
|
|
52
|
+
...state,
|
|
53
|
+
messages: state.messages.map((message) => message.id === id ? update(message) : message)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function addMessage(state, message) {
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
messages: [...state.messages, message],
|
|
60
|
+
nextId: state.nextId + 1
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function scopedRegistryKey(scope, id) {
|
|
64
|
+
return scope ? `${scope}::${id}` : id;
|
|
65
|
+
}
|
|
66
|
+
function upsertTrackedTextMessage(state, registryKey, messageId, role, text, mode, live, parentToolCallId, timing) {
|
|
67
|
+
const trackingKey = scopedRegistryKey(parentToolCallId, messageId);
|
|
68
|
+
const existingId = state[registryKey][trackingKey];
|
|
69
|
+
if (existingId) {
|
|
70
|
+
return withUpdatedMessage(state, existingId, (message) => {
|
|
71
|
+
if (message.role !== role) {
|
|
72
|
+
return message;
|
|
73
|
+
}
|
|
74
|
+
const updated = {
|
|
75
|
+
...message,
|
|
76
|
+
live,
|
|
77
|
+
text: mode === "append" ? `${message.text}${text}` : text
|
|
78
|
+
};
|
|
79
|
+
if (timing !== void 0 && !live && updated.completedAt === void 0) {
|
|
80
|
+
updated.completedAt = timing;
|
|
81
|
+
}
|
|
82
|
+
return updated;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const id = createMessageId(state);
|
|
86
|
+
const nextState = addMessage(state, {
|
|
87
|
+
id,
|
|
88
|
+
role,
|
|
89
|
+
text,
|
|
90
|
+
live,
|
|
91
|
+
...parentToolCallId ? { parentToolCallId } : {},
|
|
92
|
+
...timing !== void 0 ? { startedAt: timing } : {},
|
|
93
|
+
// A reasoning message that arrives already-settled stamps completedAt too.
|
|
94
|
+
...timing !== void 0 && !live ? { completedAt: timing } : {}
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
...nextState,
|
|
98
|
+
[registryKey]: {
|
|
99
|
+
...nextState[registryKey],
|
|
100
|
+
[trackingKey]: id
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function settleLiveReasoning(state, parentToolCallId, now) {
|
|
105
|
+
let changed = false;
|
|
106
|
+
const messages = state.messages.map((message) => {
|
|
107
|
+
if (message.role === "reasoning" && message.live && message.parentToolCallId === parentToolCallId) {
|
|
108
|
+
changed = true;
|
|
109
|
+
return { ...message, live: false, completedAt: message.completedAt ?? now };
|
|
110
|
+
}
|
|
111
|
+
return message;
|
|
112
|
+
});
|
|
113
|
+
return changed ? { ...state, messages } : state;
|
|
114
|
+
}
|
|
115
|
+
function summarizeToolInput(input) {
|
|
116
|
+
const objectInput = asJsonObject(input);
|
|
117
|
+
if (!objectInput) {
|
|
118
|
+
return JSON.stringify(input);
|
|
119
|
+
}
|
|
120
|
+
const command = objectInput.command;
|
|
121
|
+
if (typeof command === "string") {
|
|
122
|
+
return command;
|
|
123
|
+
}
|
|
124
|
+
const entries = Object.entries(objectInput);
|
|
125
|
+
if (entries.length === 1 && typeof entries[0]?.[1] === "string") {
|
|
126
|
+
return String(entries[0][1]);
|
|
127
|
+
}
|
|
128
|
+
return JSON.stringify(objectInput);
|
|
129
|
+
}
|
|
130
|
+
var BASH_STREAMING_PREVIEW_LINES = 5;
|
|
131
|
+
var STREAM_TAIL_BUDGET = 64 * 1024;
|
|
132
|
+
function countNewlines(text) {
|
|
133
|
+
let count = 0;
|
|
134
|
+
for (let index = 0; index < text.length; index++) {
|
|
135
|
+
if (text.charCodeAt(index) === 10) count++;
|
|
136
|
+
}
|
|
137
|
+
return count;
|
|
138
|
+
}
|
|
139
|
+
function capTail(text) {
|
|
140
|
+
return text.length > STREAM_TAIL_BUDGET ? text.slice(-STREAM_TAIL_BUDGET) : text;
|
|
141
|
+
}
|
|
142
|
+
function trailingWhitespaceLength(text) {
|
|
143
|
+
const match = /\s*$/.exec(text);
|
|
144
|
+
return match ? match[0].length : 0;
|
|
145
|
+
}
|
|
146
|
+
function seedStreamAcc(priorText) {
|
|
147
|
+
const trailLen = trailingWhitespaceLength(priorText);
|
|
148
|
+
const contentEnd = priorText.length - trailLen;
|
|
149
|
+
const trailingWs = priorText.slice(contentEnd);
|
|
150
|
+
return {
|
|
151
|
+
rawNewlines: countNewlines(priorText),
|
|
152
|
+
contentTail: capTail(priorText.slice(0, contentEnd)),
|
|
153
|
+
trailingWs: capTail(trailingWs),
|
|
154
|
+
trailingWsNewlines: countNewlines(trailingWs)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function advanceStreamAcc(acc, chunk) {
|
|
158
|
+
const rawNewlines = acc.rawNewlines + countNewlines(chunk);
|
|
159
|
+
const chunkTrailLen = trailingWhitespaceLength(chunk);
|
|
160
|
+
const chunkContentEnd = chunk.length - chunkTrailLen;
|
|
161
|
+
if (chunkContentEnd === 0) {
|
|
162
|
+
return {
|
|
163
|
+
rawNewlines,
|
|
164
|
+
contentTail: acc.contentTail,
|
|
165
|
+
trailingWs: capTail(acc.trailingWs + chunk),
|
|
166
|
+
trailingWsNewlines: acc.trailingWsNewlines + countNewlines(chunk)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const chunkTrailingWs = chunk.slice(chunkContentEnd);
|
|
170
|
+
return {
|
|
171
|
+
rawNewlines,
|
|
172
|
+
contentTail: capTail(acc.contentTail + acc.trailingWs + chunk.slice(0, chunkContentEnd)),
|
|
173
|
+
trailingWs: chunkTrailingWs,
|
|
174
|
+
trailingWsNewlines: countNewlines(chunkTrailingWs)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function previewFromStreamAcc(acc) {
|
|
178
|
+
if (acc.contentTail.length === 0) return void 0;
|
|
179
|
+
const totalLines = acc.rawNewlines - acc.trailingWsNewlines + 1;
|
|
180
|
+
let start = acc.contentTail.length;
|
|
181
|
+
let newlinesSeen = 0;
|
|
182
|
+
while (start > 0) {
|
|
183
|
+
if (acc.contentTail.charCodeAt(start - 1) === 10) {
|
|
184
|
+
newlinesSeen++;
|
|
185
|
+
if (newlinesSeen === BASH_STREAMING_PREVIEW_LINES) break;
|
|
186
|
+
}
|
|
187
|
+
start--;
|
|
188
|
+
}
|
|
189
|
+
return { text: acc.contentTail.slice(start), totalLines, acc };
|
|
190
|
+
}
|
|
191
|
+
function advanceStreamingPreview(priorText, chunk, previous) {
|
|
192
|
+
const acc = previous?.acc ?? seedStreamAcc(priorText);
|
|
193
|
+
return previewFromStreamAcc(advanceStreamAcc(acc, chunk));
|
|
194
|
+
}
|
|
195
|
+
function mergeToolResultText(currentText, result) {
|
|
196
|
+
if (currentText.length === 0) {
|
|
197
|
+
return result.output;
|
|
198
|
+
}
|
|
199
|
+
if (result.output.length === 0 || currentText === result.output) {
|
|
200
|
+
return currentText;
|
|
201
|
+
}
|
|
202
|
+
if (result.output.startsWith(currentText)) {
|
|
203
|
+
return result.output;
|
|
204
|
+
}
|
|
205
|
+
return currentText;
|
|
206
|
+
}
|
|
207
|
+
function ensureToolMessage(state, toolCall, parentToolCallId) {
|
|
208
|
+
const existingId = state.toolMessageIds[toolCall.id];
|
|
209
|
+
if (existingId) {
|
|
210
|
+
return withUpdatedMessage(state, existingId, (message) => {
|
|
211
|
+
if (message.role !== "tool" || message.input !== void 0) {
|
|
212
|
+
return message;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...message,
|
|
216
|
+
input: asJsonObject(toolCall.input)
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const id = createMessageId(state);
|
|
221
|
+
const nextState = addMessage(state, {
|
|
222
|
+
id,
|
|
223
|
+
role: "tool",
|
|
224
|
+
toolName: toolCall.name,
|
|
225
|
+
toolCallId: toolCall.id,
|
|
226
|
+
summary: summarizeToolInput(toolCall.input),
|
|
227
|
+
text: "",
|
|
228
|
+
status: "pending",
|
|
229
|
+
input: asJsonObject(toolCall.input),
|
|
230
|
+
...parentToolCallId ? { parentToolCallId } : {}
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
...nextState,
|
|
234
|
+
toolMessageIds: {
|
|
235
|
+
...nextState.toolMessageIds,
|
|
236
|
+
[toolCall.id]: id
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function setToolMessageStatus(state, toolCallId, status) {
|
|
241
|
+
const toolMessageId = state.toolMessageIds[toolCallId];
|
|
242
|
+
if (!toolMessageId) {
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
245
|
+
return withUpdatedMessage(state, toolMessageId, (message) => {
|
|
246
|
+
if (message.role !== "tool") {
|
|
247
|
+
return message;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
...message,
|
|
251
|
+
status
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function setToolMessageHidden(state, toolCallId, hidden) {
|
|
256
|
+
const toolMessageId = state.toolMessageIds[toolCallId];
|
|
257
|
+
if (!toolMessageId) {
|
|
258
|
+
return state;
|
|
259
|
+
}
|
|
260
|
+
return withUpdatedMessage(state, toolMessageId, (message) => {
|
|
261
|
+
if (message.role !== "tool") {
|
|
262
|
+
return message;
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
...message,
|
|
266
|
+
hidden
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function applyToolResult(state, result, now) {
|
|
271
|
+
const toolMessageId = state.toolMessageIds[result.callId];
|
|
272
|
+
if (!toolMessageId) {
|
|
273
|
+
const id = createMessageId(state);
|
|
274
|
+
const next = addMessage(state, {
|
|
275
|
+
id,
|
|
276
|
+
completedAt: now,
|
|
277
|
+
role: "tool",
|
|
278
|
+
toolName: result.toolName,
|
|
279
|
+
toolCallId: result.callId,
|
|
280
|
+
status: result.status,
|
|
281
|
+
summary: "",
|
|
282
|
+
text: result.output,
|
|
283
|
+
structured: result.structured
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
...next,
|
|
287
|
+
toolMessageIds: {
|
|
288
|
+
...next.toolMessageIds,
|
|
289
|
+
[result.callId]: id
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return withUpdatedMessage(state, toolMessageId, (message) => {
|
|
294
|
+
if (message.role !== "tool") {
|
|
295
|
+
return message;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
...message,
|
|
299
|
+
completedAt: message.completedAt ?? now,
|
|
300
|
+
status: result.status,
|
|
301
|
+
text: mergeToolResultText(message.text, result),
|
|
302
|
+
structured: result.structured,
|
|
303
|
+
hidden: false,
|
|
304
|
+
streamingPreview: void 0
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function createMessageState() {
|
|
309
|
+
return {
|
|
310
|
+
assistantMessageIds: {},
|
|
311
|
+
messages: [],
|
|
312
|
+
nextId: 1,
|
|
313
|
+
reasoningMessageIds: {},
|
|
314
|
+
toolMessageIds: {}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function createMessageStateFromMessages(messages) {
|
|
318
|
+
const nextId = messages.reduce((max, message) => {
|
|
319
|
+
const match = /^message-(\d+)$/.exec(message.id);
|
|
320
|
+
if (!match) return max;
|
|
321
|
+
return Math.max(max, Number(match[1]) + 1);
|
|
322
|
+
}, 1) + messages.length;
|
|
323
|
+
const toolMessageIds = {};
|
|
324
|
+
for (const message of messages) {
|
|
325
|
+
if (message.role === "tool") {
|
|
326
|
+
toolMessageIds[message.toolCallId] = message.id;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
assistantMessageIds: {},
|
|
331
|
+
messages,
|
|
332
|
+
nextId,
|
|
333
|
+
reasoningMessageIds: {},
|
|
334
|
+
toolMessageIds
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function appendUserMessage(state, text, attachmentLabels, createdAt) {
|
|
338
|
+
return addMessage(state, {
|
|
339
|
+
id: createMessageId(state),
|
|
340
|
+
role: "user",
|
|
341
|
+
text,
|
|
342
|
+
...attachmentLabels && attachmentLabels.length > 0 ? { attachmentLabels } : {},
|
|
343
|
+
...createdAt !== void 0 ? { createdAt } : {}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function appendErrorMessage(state, text) {
|
|
347
|
+
return addMessage(state, {
|
|
348
|
+
id: createMessageId(state),
|
|
349
|
+
role: "error",
|
|
350
|
+
text
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function appendInfoMessage(state, text) {
|
|
354
|
+
return addMessage(state, {
|
|
355
|
+
id: createMessageId(state),
|
|
356
|
+
role: "info",
|
|
357
|
+
text
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function appendWarningMessage(state, text) {
|
|
361
|
+
return addMessage(state, {
|
|
362
|
+
id: createMessageId(state),
|
|
363
|
+
role: "warning",
|
|
364
|
+
text
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
|
|
368
|
+
switch (event.type) {
|
|
369
|
+
case "assistant_chunk":
|
|
370
|
+
if (!event.chunk.text) {
|
|
371
|
+
return state;
|
|
372
|
+
}
|
|
373
|
+
return upsertTrackedTextMessage(
|
|
374
|
+
settleLiveReasoning(state, parentToolCallId, now),
|
|
375
|
+
"assistantMessageIds",
|
|
376
|
+
event.messageId,
|
|
377
|
+
"assistant",
|
|
378
|
+
event.chunk.text,
|
|
379
|
+
"append",
|
|
380
|
+
true,
|
|
381
|
+
parentToolCallId
|
|
382
|
+
);
|
|
383
|
+
case "assistant_message": {
|
|
384
|
+
const text = getTextFromMessage(event.message).trim();
|
|
385
|
+
if (text.length === 0) {
|
|
386
|
+
return state;
|
|
387
|
+
}
|
|
388
|
+
return upsertTrackedTextMessage(
|
|
389
|
+
settleLiveReasoning(state, parentToolCallId, now),
|
|
390
|
+
"assistantMessageIds",
|
|
391
|
+
event.message.id,
|
|
392
|
+
"assistant",
|
|
393
|
+
text,
|
|
394
|
+
"replace",
|
|
395
|
+
false,
|
|
396
|
+
parentToolCallId
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
case "reasoning_chunk":
|
|
400
|
+
if (!event.text) {
|
|
401
|
+
return state;
|
|
402
|
+
}
|
|
403
|
+
return upsertTrackedTextMessage(
|
|
404
|
+
state,
|
|
405
|
+
"reasoningMessageIds",
|
|
406
|
+
event.messageId,
|
|
407
|
+
"reasoning",
|
|
408
|
+
event.text,
|
|
409
|
+
"append",
|
|
410
|
+
true,
|
|
411
|
+
parentToolCallId,
|
|
412
|
+
now
|
|
413
|
+
);
|
|
414
|
+
case "reasoning_message": {
|
|
415
|
+
const text = getReasoningTextFromMessage(event.message).trim();
|
|
416
|
+
if (text.length === 0) {
|
|
417
|
+
return state;
|
|
418
|
+
}
|
|
419
|
+
return upsertTrackedTextMessage(
|
|
420
|
+
state,
|
|
421
|
+
"reasoningMessageIds",
|
|
422
|
+
event.message.id,
|
|
423
|
+
"reasoning",
|
|
424
|
+
text,
|
|
425
|
+
"replace",
|
|
426
|
+
false,
|
|
427
|
+
parentToolCallId,
|
|
428
|
+
now
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
case "final_message": {
|
|
432
|
+
const text = getTextFromMessage(event.message).trim();
|
|
433
|
+
if (text.length === 0) {
|
|
434
|
+
return state;
|
|
435
|
+
}
|
|
436
|
+
return upsertTrackedTextMessage(
|
|
437
|
+
settleLiveReasoning(state, parentToolCallId, now),
|
|
438
|
+
"assistantMessageIds",
|
|
439
|
+
event.message.id,
|
|
440
|
+
"assistant",
|
|
441
|
+
text,
|
|
442
|
+
"replace",
|
|
443
|
+
false,
|
|
444
|
+
parentToolCallId
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
case "tool_call":
|
|
448
|
+
return ensureToolMessage(state, event.toolCall, parentToolCallId);
|
|
449
|
+
case "tool_started": {
|
|
450
|
+
const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
|
|
451
|
+
const toolMessageId = toolState.toolMessageIds[event.toolCall.id];
|
|
452
|
+
return withUpdatedMessage(toolState, toolMessageId, (message) => {
|
|
453
|
+
if (message.role !== "tool") {
|
|
454
|
+
return message;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
...message,
|
|
458
|
+
startedAt: message.startedAt ?? now,
|
|
459
|
+
status: "running"
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
case "tool_output_chunk": {
|
|
464
|
+
const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
|
|
465
|
+
const toolMessageId = toolState.toolMessageIds[event.toolCall.id];
|
|
466
|
+
return withUpdatedMessage(toolState, toolMessageId, (message) => {
|
|
467
|
+
if (message.role !== "tool") {
|
|
468
|
+
return message;
|
|
469
|
+
}
|
|
470
|
+
const nextText = `${message.text}${event.chunk.output}`;
|
|
471
|
+
return {
|
|
472
|
+
...message,
|
|
473
|
+
text: nextText,
|
|
474
|
+
streamingPreview: message.toolName === "Bash" ? (
|
|
475
|
+
// Pass the PRIOR text (not the concatenation): the builder works
|
|
476
|
+
// from a bounded accumulator and only reads prior text on reseed,
|
|
477
|
+
// so `message.text` is never indexed and never flattened here.
|
|
478
|
+
advanceStreamingPreview(message.text, event.chunk.output, message.streamingPreview)
|
|
479
|
+
) : message.streamingPreview
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
case "tool_completed": {
|
|
484
|
+
const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
|
|
485
|
+
return applyToolResult(toolState, event.result, now);
|
|
486
|
+
}
|
|
487
|
+
case "tool_result":
|
|
488
|
+
return applyToolResult(state, event.result, now);
|
|
489
|
+
case "tool_rejected": {
|
|
490
|
+
const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
|
|
491
|
+
const toolMessageId = toolState.toolMessageIds[event.toolCall.id];
|
|
492
|
+
return withUpdatedMessage(toolState, toolMessageId, (message) => {
|
|
493
|
+
if (message.role !== "tool") {
|
|
494
|
+
return message;
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
...message,
|
|
498
|
+
completedAt: message.completedAt ?? now,
|
|
499
|
+
status: "rejected",
|
|
500
|
+
text: event.result.output,
|
|
501
|
+
structured: event.result.structured
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
case "provider_retry": {
|
|
506
|
+
const delaySeconds = Math.max(1, Math.ceil(event.delayMs / 1e3));
|
|
507
|
+
const delayText = event.delayMs > 0 ? `in ${delaySeconds}s` : "now";
|
|
508
|
+
return appendWarningMessage(
|
|
509
|
+
state,
|
|
510
|
+
`Provider request failed, retrying ${delayText} (${event.attempt}/${event.maxAttempts})\u2026`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
default:
|
|
514
|
+
return state;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/state/workflow-view.ts
|
|
519
|
+
function createWorkflowViewState(runId) {
|
|
520
|
+
return { runId, status: "running", tree: [], counts: {}, tokens: 0, lastSeq: -1, nodeKeys: {} };
|
|
521
|
+
}
|
|
522
|
+
function nodeIdentity(delta) {
|
|
523
|
+
return `${delta.nodeId}\0${delta.instanceKey ?? ""}\0${delta.roundKey ?? ""}`;
|
|
524
|
+
}
|
|
525
|
+
function coarseStatus(status) {
|
|
526
|
+
if (status === "failed") return "failed";
|
|
527
|
+
if (status === "skipped") return "skipped";
|
|
528
|
+
if (status === "aborted") return "stopped";
|
|
529
|
+
return "done";
|
|
530
|
+
}
|
|
531
|
+
function setStatus(tree, phase, title, status) {
|
|
532
|
+
const phaseIdx = tree.findIndex((p) => p.title === phase);
|
|
533
|
+
if (phaseIdx === -1) return [...tree, { title: phase, children: [{ title, status }] }];
|
|
534
|
+
const phaseNode = tree[phaseIdx];
|
|
535
|
+
const children = phaseNode.children ?? [];
|
|
536
|
+
const childIdx = children.findIndex((c) => c.title === title);
|
|
537
|
+
const nextChildren = childIdx === -1 ? [...children, { title, status }] : children.map((c, i) => i === childIdx ? { ...c, status } : c);
|
|
538
|
+
return tree.map((p, i) => i === phaseIdx ? { ...phaseNode, children: nextChildren } : p);
|
|
539
|
+
}
|
|
540
|
+
function foldWorkflowDelta(state, delta) {
|
|
541
|
+
if (delta.seq <= state.lastSeq) return state;
|
|
542
|
+
const base = { ...state, lastSeq: delta.seq };
|
|
543
|
+
switch (delta.type) {
|
|
544
|
+
case "node-started": {
|
|
545
|
+
if (delta.kind === "source" || delta.kind === "sink") return base;
|
|
546
|
+
const phase = delta.phase ?? "";
|
|
547
|
+
const title = delta.title ?? delta.nodeId;
|
|
548
|
+
return {
|
|
549
|
+
...base,
|
|
550
|
+
currentPhase: phase,
|
|
551
|
+
counts: { ...base.counts, [delta.kind]: (base.counts[delta.kind] ?? 0) + 1 },
|
|
552
|
+
nodeKeys: { ...base.nodeKeys, [nodeIdentity(delta)]: [phase, title] },
|
|
553
|
+
tree: setStatus(base.tree, phase, title, "running")
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
case "node-finished": {
|
|
557
|
+
const tokens = base.tokens + (delta.tokens ?? 0);
|
|
558
|
+
const key = base.nodeKeys[nodeIdentity(delta)];
|
|
559
|
+
if (!key) return { ...base, tokens };
|
|
560
|
+
return {
|
|
561
|
+
...base,
|
|
562
|
+
tokens,
|
|
563
|
+
tree: setStatus(base.tree, key[0], key[1], coarseStatus(delta.status))
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
case "run-finished":
|
|
567
|
+
return { ...base, status: delta.status };
|
|
568
|
+
default:
|
|
569
|
+
return base;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/state/session.ts
|
|
574
|
+
function createSessionViewState() {
|
|
575
|
+
return {
|
|
576
|
+
approval: createApprovalQueueState(),
|
|
577
|
+
authFlow: null,
|
|
578
|
+
busy: false,
|
|
579
|
+
messages: createMessageState(),
|
|
580
|
+
meta: { approvalMode: "ask before tools", cwd: "", modelName: "", persona: "default" },
|
|
581
|
+
metrics: null,
|
|
582
|
+
queuedCount: 0,
|
|
583
|
+
queued: [],
|
|
584
|
+
workflows: {},
|
|
585
|
+
question: { active: null }
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function reduceSessionEvent(state, event) {
|
|
589
|
+
switch (event.type) {
|
|
590
|
+
case "agent-event": {
|
|
591
|
+
let messages = applyAgentEvent(
|
|
592
|
+
state.messages,
|
|
593
|
+
event.event,
|
|
594
|
+
event.scope?.parentToolCallId,
|
|
595
|
+
event.at
|
|
596
|
+
);
|
|
597
|
+
if (event.toolStatus && event.event.type === "tool_call") {
|
|
598
|
+
messages = setToolMessageStatus(messages, event.event.toolCall.id, event.toolStatus);
|
|
599
|
+
}
|
|
600
|
+
return { ...state, messages };
|
|
601
|
+
}
|
|
602
|
+
case "user-message":
|
|
603
|
+
return {
|
|
604
|
+
...state,
|
|
605
|
+
messages: appendUserMessage(state.messages, event.text, event.attachmentLabels, event.at)
|
|
606
|
+
};
|
|
607
|
+
case "approval-requested":
|
|
608
|
+
return {
|
|
609
|
+
...state,
|
|
610
|
+
approval: enqueueApproval(state.approval, event.approval),
|
|
611
|
+
messages: setToolMessageHidden(state.messages, event.approval.toolCallId, true)
|
|
612
|
+
};
|
|
613
|
+
case "approval-settled": {
|
|
614
|
+
const stamped = setToolMessageStatus(state.messages, event.toolCallId, event.status);
|
|
615
|
+
return {
|
|
616
|
+
...state,
|
|
617
|
+
approval: removeApproval(state.approval, (item) => item.toolCallId === event.toolCallId),
|
|
618
|
+
// Approved tools materialize as a transcript row; rejected ones never
|
|
619
|
+
// ran and stay hidden.
|
|
620
|
+
messages: event.status === "approved" ? setToolMessageHidden(stamped, event.toolCallId, false) : stamped
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
case "question-asked":
|
|
624
|
+
return { ...state, question: { active: event.question } };
|
|
625
|
+
case "question-settled":
|
|
626
|
+
return state.question.active?.questionId === event.questionId ? { ...state, question: { active: null } } : state;
|
|
627
|
+
case "turn-status":
|
|
628
|
+
return {
|
|
629
|
+
...state,
|
|
630
|
+
busy: event.busy,
|
|
631
|
+
queuedCount: event.queuedCount,
|
|
632
|
+
queued: event.queued ?? []
|
|
633
|
+
};
|
|
634
|
+
case "info":
|
|
635
|
+
return { ...state, messages: appendInfoMessage(state.messages, event.message) };
|
|
636
|
+
case "error":
|
|
637
|
+
return { ...state, messages: appendErrorMessage(state.messages, event.message) };
|
|
638
|
+
case "session-meta":
|
|
639
|
+
return { ...state, meta: event.meta };
|
|
640
|
+
case "metrics":
|
|
641
|
+
return { ...state, metrics: event.snapshot };
|
|
642
|
+
case "auth-flow":
|
|
643
|
+
return { ...state, authFlow: event.flow };
|
|
644
|
+
case "workflow-progress": {
|
|
645
|
+
const prev = state.workflows[event.runId] ?? createWorkflowViewState(event.runId);
|
|
646
|
+
return {
|
|
647
|
+
...state,
|
|
648
|
+
workflows: { ...state.workflows, [event.runId]: foldWorkflowDelta(prev, event.delta) }
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
case "state-reset":
|
|
652
|
+
return event.state;
|
|
653
|
+
default:
|
|
654
|
+
return state;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export {
|
|
659
|
+
READ_ONLY_TOOLS,
|
|
660
|
+
BACKGROUNDLESS_TOOLS,
|
|
661
|
+
isAutoApprovedTool,
|
|
662
|
+
createApprovalQueueState,
|
|
663
|
+
enqueueApproval,
|
|
664
|
+
dequeueActiveApproval,
|
|
665
|
+
removeApproval,
|
|
666
|
+
asJsonObject,
|
|
667
|
+
setToolMessageStatus,
|
|
668
|
+
setToolMessageHidden,
|
|
669
|
+
createMessageState,
|
|
670
|
+
createMessageStateFromMessages,
|
|
671
|
+
appendUserMessage,
|
|
672
|
+
appendErrorMessage,
|
|
673
|
+
appendInfoMessage,
|
|
674
|
+
appendWarningMessage,
|
|
675
|
+
applyAgentEvent,
|
|
676
|
+
createSessionViewState,
|
|
677
|
+
reduceSessionEvent
|
|
678
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/state/dir-access.ts
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
function dirContains(dir, target) {
|
|
4
|
+
const relative2 = path.relative(path.resolve(dir), path.resolve(target));
|
|
5
|
+
return relative2 === "" || !relative2.startsWith("..") && !path.isAbsolute(relative2);
|
|
6
|
+
}
|
|
7
|
+
function resolveGrantDir(requestedPath, proposedDir, edited) {
|
|
8
|
+
const trimmed = edited?.trim();
|
|
9
|
+
if (!trimmed) return proposedDir;
|
|
10
|
+
const dir = path.resolve(trimmed);
|
|
11
|
+
return dirContains(dir, requestedPath) ? dir : proposedDir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
dirContains,
|
|
16
|
+
resolveGrantDir
|
|
17
|
+
};
|