@sumicom/quicksave 0.6.0 → 0.6.1
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/ai/asyncQueue.d.ts +7 -6
- package/dist/ai/asyncQueue.d.ts.map +1 -1
- package/dist/ai/asyncQueue.js +31 -21
- package/dist/ai/asyncQueue.js.map +1 -1
- package/dist/ai/asyncQueue.test.js +18 -18
- package/dist/ai/asyncQueue.test.js.map +1 -1
- package/dist/ai/cardBuilder.d.ts +11 -2
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +113 -21
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +19 -0
- package/dist/ai/claudeCliProvider.d.ts.map +1 -0
- package/dist/ai/claudeCliProvider.js +466 -0
- package/dist/ai/claudeCliProvider.js.map +1 -0
- package/dist/ai/claudeSdkProvider.d.ts +18 -0
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -0
- package/dist/ai/claudeSdkProvider.js +418 -0
- package/dist/ai/claudeSdkProvider.js.map +1 -0
- package/dist/ai/commitSummary.d.ts +4 -0
- package/dist/ai/commitSummary.d.ts.map +1 -1
- package/dist/ai/commitSummary.js +80 -40
- package/dist/ai/commitSummary.js.map +1 -1
- package/dist/ai/provider.d.ts +54 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +2 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/ai/sandboxMcp.d.ts +12 -0
- package/dist/ai/sandboxMcp.d.ts.map +1 -0
- package/dist/ai/sandboxMcp.js +12 -0
- package/dist/ai/sandboxMcp.js.map +1 -0
- package/dist/ai/sandboxMcpStdio.d.ts +16 -0
- package/dist/ai/sandboxMcpStdio.d.ts.map +1 -0
- package/dist/ai/sandboxMcpStdio.js +127 -0
- package/dist/ai/sandboxMcpStdio.js.map +1 -0
- package/dist/ai/sessionManager.d.ts +100 -0
- package/dist/ai/sessionManager.d.ts.map +1 -0
- package/dist/ai/sessionManager.js +595 -0
- package/dist/ai/sessionManager.js.map +1 -0
- package/dist/ai/sessionRegistry.d.ts +28 -0
- package/dist/ai/sessionRegistry.d.ts.map +1 -0
- package/dist/ai/sessionRegistry.js +148 -0
- package/dist/ai/sessionRegistry.js.map +1 -0
- package/dist/git/operations.d.ts +6 -1
- package/dist/git/operations.d.ts.map +1 -1
- package/dist/git/operations.js +31 -2
- package/dist/git/operations.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +8 -3
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +96 -12
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/handlers/messageHandler.test.js +1 -1
- package/dist/index.js +3 -40
- package/dist/index.js.map +1 -1
- package/dist/service/ensureDaemon.d.ts.map +1 -1
- package/dist/service/ensureDaemon.js +15 -2
- package/dist/service/ensureDaemon.js.map +1 -1
- package/dist/service/ipc.test.js +6 -6
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +22 -15
- package/dist/service/run.js.map +1 -1
- package/dist/service/singleton.d.ts +4 -0
- package/dist/service/singleton.d.ts.map +1 -1
- package/dist/service/singleton.js +28 -2
- package/dist/service/singleton.js.map +1 -1
- package/dist/service/singleton.test.js +1 -1
- package/package.json +5 -3
- package/dist/ai/claudeCodeService.d.ts +0 -122
- package/dist/ai/claudeCodeService.d.ts.map +0 -1
- package/dist/ai/claudeCodeService.js +0 -869
- package/dist/ai/claudeCodeService.js.map +0 -1
|
@@ -1,869 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { unstable_v2_createSession, unstable_v2_resumeSession, listSessions, getSessionMessages, listSubagents, getSubagentMessages, } from '@anthropic-ai/claude-agent-sdk';
|
|
4
|
-
import { DEFAULT_MODEL } from '@sumicom/quicksave-shared';
|
|
5
|
-
import { StreamCardBuilder, buildCardsFromHistory } from './cardBuilder.js';
|
|
6
|
-
/** Tools auto-approved at each permission level (no user prompt).
|
|
7
|
-
* Read/Glob/Grep are always auto-approved at SDK level (allowedTools).
|
|
8
|
-
* Tools NOT listed here go through canUseTool → permission prompt.
|
|
9
|
-
*
|
|
10
|
-
* Risk tiers:
|
|
11
|
-
* - Safe: Edit, Write, NotebookEdit, TodoWrite, Agent, EnterWorktree, ExitWorktree
|
|
12
|
-
* - Network: WebFetch, WebSearch
|
|
13
|
-
* - Execute: Bash
|
|
14
|
-
* - Code/Control: Skill, ToolSearch, Config
|
|
15
|
-
* - Schedule: CronCreate, CronDelete (arbitrary scheduled execution)
|
|
16
|
-
* - Remote: RemoteTrigger (triggers remote agents)
|
|
17
|
-
* - Workflow: EnterPlanMode, ExitPlanMode (ExitPlanMode has its own interactive UI)
|
|
18
|
-
*/
|
|
19
|
-
const AUTO_APPROVE = {
|
|
20
|
-
bypassPermissions: new Set([
|
|
21
|
-
// Safe
|
|
22
|
-
'Edit', 'Write', 'NotebookEdit', 'TodoWrite', 'Agent', 'EnterWorktree', 'ExitWorktree',
|
|
23
|
-
// Network
|
|
24
|
-
'WebFetch', 'WebSearch',
|
|
25
|
-
// Execute
|
|
26
|
-
'Bash',
|
|
27
|
-
// Code/Control
|
|
28
|
-
'Skill', 'ToolSearch', 'Config',
|
|
29
|
-
// Schedule + Remote
|
|
30
|
-
'CronCreate', 'CronDelete', 'CronList', 'RemoteTrigger',
|
|
31
|
-
// Workflow
|
|
32
|
-
'EnterPlanMode', 'ExitPlanMode',
|
|
33
|
-
// Background tasks
|
|
34
|
-
'TaskOutput', 'TaskStop',
|
|
35
|
-
]),
|
|
36
|
-
acceptEdits: new Set(['Edit', 'Write', 'NotebookEdit', 'TodoWrite', 'Agent', 'EnterWorktree', 'ExitWorktree']),
|
|
37
|
-
default: new Set(['TodoWrite', 'EnterWorktree', 'ExitWorktree', 'Agent']),
|
|
38
|
-
plan: new Set(),
|
|
39
|
-
};
|
|
40
|
-
function createDeferred() {
|
|
41
|
-
let resolve;
|
|
42
|
-
const promise = new Promise((r) => { resolve = r; });
|
|
43
|
-
return { promise, resolve };
|
|
44
|
-
}
|
|
45
|
-
/** Extract readable text from tool_result content (which may be a string or array of blocks). */
|
|
46
|
-
function extractToolResultText(content) {
|
|
47
|
-
if (typeof content === 'string')
|
|
48
|
-
return content;
|
|
49
|
-
if (Array.isArray(content)) {
|
|
50
|
-
return content
|
|
51
|
-
.filter((b) => b.type === 'text')
|
|
52
|
-
.map((b) => b.text || '')
|
|
53
|
-
.join('\n');
|
|
54
|
-
}
|
|
55
|
-
return JSON.stringify(content);
|
|
56
|
-
}
|
|
57
|
-
export class ClaudeCodeService extends EventEmitter {
|
|
58
|
-
sessions = new Map();
|
|
59
|
-
sessionPermissions = new Map(); // persists across active/inactive
|
|
60
|
-
sessionConfigs = new Map(); // generic per-session config
|
|
61
|
-
pendingInputRequests = new Map();
|
|
62
|
-
requestCounter = 0;
|
|
63
|
-
preferences = {
|
|
64
|
-
model: DEFAULT_MODEL,
|
|
65
|
-
};
|
|
66
|
-
constructor() {
|
|
67
|
-
super();
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Initialize preferences from the most recently used session.
|
|
71
|
-
* Uses SDK listSessions + getSessionMessages to read the last assistant message's model field.
|
|
72
|
-
*/
|
|
73
|
-
async initPreferences() {
|
|
74
|
-
try {
|
|
75
|
-
const sessions = await listSessions();
|
|
76
|
-
if (sessions.length === 0)
|
|
77
|
-
return;
|
|
78
|
-
// Sessions are sorted by lastModified desc by the SDK
|
|
79
|
-
const latest = sessions[0];
|
|
80
|
-
const sessionId = latest.sessionId;
|
|
81
|
-
const cwd = latest.cwd;
|
|
82
|
-
const msgs = await getSessionMessages(sessionId, { dir: cwd });
|
|
83
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
84
|
-
const model = msgs[i].message?.model;
|
|
85
|
-
if (model && msgs[i].type === 'assistant') {
|
|
86
|
-
this.preferences.model = model;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch { /* use defaults */ }
|
|
92
|
-
}
|
|
93
|
-
getPreferences() {
|
|
94
|
-
return { ...this.preferences };
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Update preferences. Returns the applied preferences (may differ if validation fails).
|
|
98
|
-
* Emits 'preferences-updated' if any value actually changed.
|
|
99
|
-
*/
|
|
100
|
-
setPreferences(prefs) {
|
|
101
|
-
const next = { ...this.preferences };
|
|
102
|
-
let changed = false;
|
|
103
|
-
if (prefs.model !== undefined && prefs.model !== this.preferences.model) {
|
|
104
|
-
next.model = prefs.model;
|
|
105
|
-
changed = true;
|
|
106
|
-
}
|
|
107
|
-
if (changed) {
|
|
108
|
-
this.preferences = next;
|
|
109
|
-
this.emit('preferences-updated', this.preferences);
|
|
110
|
-
}
|
|
111
|
-
return { ...this.preferences };
|
|
112
|
-
}
|
|
113
|
-
getSessionConfig(sessionId) {
|
|
114
|
-
return { ...this.sessionConfigs.get(sessionId) };
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Set a single key on a session's config. Applies known keys immediately:
|
|
118
|
-
* model / reasoningEffort → updates global preferences (takes effect next turn)
|
|
119
|
-
* permissionMode → calls setPermissionLevel (immediate)
|
|
120
|
-
*/
|
|
121
|
-
setSessionConfig(sessionId, key, value) {
|
|
122
|
-
const prev = this.sessionConfigs.get(sessionId) ?? {};
|
|
123
|
-
const next = { ...prev, [key]: value };
|
|
124
|
-
this.sessionConfigs.set(sessionId, next);
|
|
125
|
-
// Apply known keys
|
|
126
|
-
if (key === 'model' && typeof value === 'string') {
|
|
127
|
-
this.setPreferences({ model: value });
|
|
128
|
-
}
|
|
129
|
-
else if (key === 'reasoningEffort' && (typeof value === 'string' || value === null)) {
|
|
130
|
-
this.setPreferences({ reasoningEffort: value });
|
|
131
|
-
}
|
|
132
|
-
else if (key === 'permissionMode' && typeof value === 'string') {
|
|
133
|
-
this.setPermissionLevel(sessionId, value);
|
|
134
|
-
}
|
|
135
|
-
this.emit('session-config-updated', { sessionId, config: next });
|
|
136
|
-
return next;
|
|
137
|
-
}
|
|
138
|
-
/** Build and emit a session-updated event with current state. */
|
|
139
|
-
emitSessionUpdate(sessionId) {
|
|
140
|
-
const ps = this.sessions.get(sessionId);
|
|
141
|
-
const hasPendingInput = Array.from(this.pendingInputRequests.values())
|
|
142
|
-
.some((p) => p.request.sessionId === sessionId);
|
|
143
|
-
this.emit('session-updated', {
|
|
144
|
-
sessionId,
|
|
145
|
-
isActive: !!ps,
|
|
146
|
-
isStreaming: ps?.streaming ?? false,
|
|
147
|
-
hasPendingInput,
|
|
148
|
-
permissionMode: ps?.permissionLevel,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
async listAvailableSessions(cwd) {
|
|
152
|
-
const sessions = await listSessions({ dir: cwd, limit: 50 });
|
|
153
|
-
// Enrich with live state + detect pending from JSONL
|
|
154
|
-
const pendingSessionIds = new Set(Array.from(this.pendingInputRequests.values()).map((p) => p.request.sessionId));
|
|
155
|
-
// Check JSONL for sessions not in memory (cold pending detection)
|
|
156
|
-
const enriched = await Promise.all(sessions.map(async (s) => {
|
|
157
|
-
const isActive = this.sessions.has(s.sessionId);
|
|
158
|
-
const isStreaming = this.sessions.get(s.sessionId)?.streaming ?? false;
|
|
159
|
-
let hasPendingInput = pendingSessionIds.has(s.sessionId);
|
|
160
|
-
// Only check JSONL for sessions NOT in memory — in-memory sessions use
|
|
161
|
-
// pendingInputRequests as the authoritative source. JSONL may have mid-flight
|
|
162
|
-
// tool_use blocks that haven't received tool_result yet (false positive).
|
|
163
|
-
if (!hasPendingInput && !isActive) {
|
|
164
|
-
hasPendingInput = await this.detectPendingFromJSONL(s.sessionId, cwd);
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
sessionId: s.sessionId,
|
|
168
|
-
summary: s.summary,
|
|
169
|
-
lastModified: s.lastModified,
|
|
170
|
-
createdAt: s.createdAt,
|
|
171
|
-
cwd: s.cwd,
|
|
172
|
-
gitBranch: s.gitBranch,
|
|
173
|
-
isActive,
|
|
174
|
-
isStreaming,
|
|
175
|
-
hasPendingInput,
|
|
176
|
-
permissionMode: this.sessions.get(s.sessionId)?.permissionLevel ?? this.sessionPermissions.get(s.sessionId),
|
|
177
|
-
};
|
|
178
|
-
}));
|
|
179
|
-
// Sort: pending first, then active, then by lastModified
|
|
180
|
-
enriched.sort((a, b) => {
|
|
181
|
-
if (a.hasPendingInput !== b.hasPendingInput)
|
|
182
|
-
return a.hasPendingInput ? -1 : 1;
|
|
183
|
-
if (a.isActive !== b.isActive)
|
|
184
|
-
return a.isActive ? -1 : 1;
|
|
185
|
-
return b.lastModified - a.lastModified;
|
|
186
|
-
});
|
|
187
|
-
return enriched;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Check if a session's last message is an unanswered tool_use.
|
|
191
|
-
* Uses SDK getSessionMessages + listSubagents/getSubagentMessages.
|
|
192
|
-
*/
|
|
193
|
-
async detectPendingFromJSONL(sessionId, cwd) {
|
|
194
|
-
const hasPendingToolUse = (msgs, label = 'parent') => {
|
|
195
|
-
if (msgs.length === 0)
|
|
196
|
-
return false;
|
|
197
|
-
const last = msgs[msgs.length - 1];
|
|
198
|
-
if (last.type !== 'assistant')
|
|
199
|
-
return false;
|
|
200
|
-
const content = last.message?.content;
|
|
201
|
-
if (!Array.isArray(content))
|
|
202
|
-
return false;
|
|
203
|
-
const toolUseIds = content.filter((b) => b.type === 'tool_use').map((b) => b.id);
|
|
204
|
-
if (toolUseIds.length === 0)
|
|
205
|
-
return false;
|
|
206
|
-
// Check if all tool_use blocks already have a corresponding tool_result
|
|
207
|
-
const resolvedIds = new Set(msgs
|
|
208
|
-
.filter((m) => m.type === 'user')
|
|
209
|
-
.flatMap((m) => {
|
|
210
|
-
const c = m.message?.content;
|
|
211
|
-
return Array.isArray(c)
|
|
212
|
-
? c.filter((b) => b.type === 'tool_result').map((b) => b.tool_use_id)
|
|
213
|
-
: [];
|
|
214
|
-
}));
|
|
215
|
-
const unresolvedIds = toolUseIds.filter((id) => !resolvedIds.has(id));
|
|
216
|
-
if (unresolvedIds.length > 0) {
|
|
217
|
-
const toolNames = content
|
|
218
|
-
.filter((b) => b.type === 'tool_use' && unresolvedIds.includes(b.id))
|
|
219
|
-
.map((b) => b.name);
|
|
220
|
-
console.log(`[detectPending] ${label} session=${sessionId.slice(0, 8)}: unresolved tool_use: ${toolNames.join(', ')} (${unresolvedIds.length}/${toolUseIds.length})`);
|
|
221
|
-
}
|
|
222
|
-
return unresolvedIds.length > 0;
|
|
223
|
-
};
|
|
224
|
-
try {
|
|
225
|
-
// Check parent session
|
|
226
|
-
const allMessages = await getSessionMessages(sessionId, { dir: cwd });
|
|
227
|
-
if (hasPendingToolUse(allMessages))
|
|
228
|
-
return true;
|
|
229
|
-
// Check subagents — a subagent may be waiting for permission
|
|
230
|
-
try {
|
|
231
|
-
const agentIds = await listSubagents(sessionId, { dir: cwd });
|
|
232
|
-
for (const agentId of agentIds) {
|
|
233
|
-
const subMsgs = await getSubagentMessages(sessionId, agentId, { dir: cwd });
|
|
234
|
-
if (hasPendingToolUse(subMsgs, `subagent:${agentId.slice(0, 8)}`))
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch { /* no subagents */ }
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Create a V2 session with the given cwd.
|
|
247
|
-
* V2 SDKSessionOptions doesn't expose `cwd`, so we temporarily change
|
|
248
|
-
* process.cwd() around the synchronous createSession() call.
|
|
249
|
-
*/
|
|
250
|
-
createSessionWithCwd(cwd, sessionId, opts) {
|
|
251
|
-
const originalCwd = process.cwd();
|
|
252
|
-
try {
|
|
253
|
-
process.chdir(cwd);
|
|
254
|
-
// allowedTools = SDK auto-approve list (bypasses canUseTool entirely).
|
|
255
|
-
// Only list tools that should NEVER prompt at ANY permission level.
|
|
256
|
-
// Everything else goes through permissionMode → canUseTool for dynamic control.
|
|
257
|
-
const sessionOpts = {
|
|
258
|
-
model: opts.model ?? DEFAULT_MODEL,
|
|
259
|
-
allowedTools: opts.allowedTools ?? ['Read', 'Glob', 'Grep'],
|
|
260
|
-
permissionMode: 'default',
|
|
261
|
-
includePartialMessages: true,
|
|
262
|
-
settingSources: ['user', 'project', 'local'],
|
|
263
|
-
canUseTool: async (toolName, input, options) => {
|
|
264
|
-
// Resolve session ID: use mutable ref (updated when init arrives),
|
|
265
|
-
// fall back to static closure, await deferred if still unknown.
|
|
266
|
-
let resolvedSessionId = opts.sessionIdRef?.current ?? sessionId;
|
|
267
|
-
if (!resolvedSessionId && opts.sessionIdRef?.promise) {
|
|
268
|
-
resolvedSessionId = await opts.sessionIdRef.promise;
|
|
269
|
-
}
|
|
270
|
-
resolvedSessionId = resolvedSessionId ?? 'unknown';
|
|
271
|
-
// Check runtime permission level — auto-approve if tool is in the allow set
|
|
272
|
-
const ps = resolvedSessionId !== 'unknown' ? this.sessions.get(resolvedSessionId) : undefined;
|
|
273
|
-
const level = ps?.permissionLevel ?? this.sessionPermissions.get(resolvedSessionId) ?? 'acceptEdits';
|
|
274
|
-
if (AUTO_APPROVE[level].has(toolName)) {
|
|
275
|
-
// For file-writing tools, restrict auto-approval to paths inside the project cwd
|
|
276
|
-
const FILE_WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
277
|
-
if (FILE_WRITE_TOOLS.has(toolName)) {
|
|
278
|
-
const filePath = (input.file_path ?? input.path);
|
|
279
|
-
if (filePath) {
|
|
280
|
-
const resolvedFile = resolve(filePath);
|
|
281
|
-
const resolvedCwd = resolve(cwd);
|
|
282
|
-
const inScope = resolvedFile === resolvedCwd || resolvedFile.startsWith(resolvedCwd + '/');
|
|
283
|
-
if (!inScope) {
|
|
284
|
-
// Fall through to permission prompt below
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
return { behavior: 'allow', updatedInput: input };
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
return { behavior: 'allow', updatedInput: input };
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
return { behavior: 'allow', updatedInput: input };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
const requestId = `perm-${++this.requestCounter}`;
|
|
299
|
-
// AskUserQuestion: forward as question type with options
|
|
300
|
-
const isQuestion = toolName === 'AskUserQuestion';
|
|
301
|
-
const questions = isQuestion ? input.questions : undefined;
|
|
302
|
-
// Forward request to PWA
|
|
303
|
-
const request = {
|
|
304
|
-
sessionId: resolvedSessionId,
|
|
305
|
-
requestId,
|
|
306
|
-
inputType: isQuestion ? 'question' : 'permission',
|
|
307
|
-
title: isQuestion
|
|
308
|
-
? questions?.[0]?.question ?? 'Question from Claude'
|
|
309
|
-
: (options.title ?? `Allow ${toolName}?`),
|
|
310
|
-
message: isQuestion
|
|
311
|
-
? undefined
|
|
312
|
-
: (options.description ?? JSON.stringify(input).slice(0, 500)),
|
|
313
|
-
toolName,
|
|
314
|
-
toolInput: input,
|
|
315
|
-
toolUseId: options.toolUseID,
|
|
316
|
-
...(options.agentID ? { agentId: options.agentID } : {}),
|
|
317
|
-
// Include structured options for question type
|
|
318
|
-
...(isQuestion && questions ? {
|
|
319
|
-
options: questions.flatMap((q) => (q.options ?? []).map((opt) => ({
|
|
320
|
-
key: opt.label,
|
|
321
|
-
label: opt.label,
|
|
322
|
-
description: opt.description,
|
|
323
|
-
}))),
|
|
324
|
-
} : {}),
|
|
325
|
-
};
|
|
326
|
-
// Card-based: create/attach pending input on the correct card
|
|
327
|
-
const builder = ps?.cardBuilder;
|
|
328
|
-
if (builder) {
|
|
329
|
-
const pendingAttachment = {
|
|
330
|
-
sessionId: resolvedSessionId,
|
|
331
|
-
requestId,
|
|
332
|
-
inputType: request.inputType,
|
|
333
|
-
title: request.title,
|
|
334
|
-
message: request.message,
|
|
335
|
-
options: request.options,
|
|
336
|
-
};
|
|
337
|
-
// Every permission gets its own ToolCallCard.
|
|
338
|
-
// Subagent permissions are ephemeral — removed after resolution
|
|
339
|
-
// (the tool runs in the sidechain, so no result will arrive).
|
|
340
|
-
const ephemeral = !!options.agentID;
|
|
341
|
-
const cardEvt = builder.toolCallFromPermission(toolName, input, options.toolUseID, pendingAttachment, ephemeral);
|
|
342
|
-
this.emit('card-event', cardEvt);
|
|
343
|
-
}
|
|
344
|
-
this.emit('user-input-request', request);
|
|
345
|
-
// Register pending BEFORE emitting session-updated so hasPendingInput is true
|
|
346
|
-
const responsePromise = this.waitForUserInput(requestId, request, options.signal);
|
|
347
|
-
this.emitSessionUpdate(resolvedSessionId);
|
|
348
|
-
// Wait for explicit user response (no timeout — user must act)
|
|
349
|
-
const response = await responsePromise;
|
|
350
|
-
if (response.action === 'deny') {
|
|
351
|
-
return { behavior: 'deny', message: 'User denied permission' };
|
|
352
|
-
}
|
|
353
|
-
// For AskUserQuestion, inject user's answer into the tool input
|
|
354
|
-
if (isQuestion && response.response) {
|
|
355
|
-
const answers = {};
|
|
356
|
-
if (questions?.[0]?.question) {
|
|
357
|
-
answers[questions[0].question] = response.response;
|
|
358
|
-
}
|
|
359
|
-
return {
|
|
360
|
-
behavior: 'allow',
|
|
361
|
-
updatedInput: { ...input, answers },
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
return { behavior: 'allow', updatedInput: input };
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
if (opts.resumeSessionId) {
|
|
368
|
-
return unstable_v2_resumeSession(opts.resumeSessionId, sessionOpts);
|
|
369
|
-
}
|
|
370
|
-
return unstable_v2_createSession(sessionOpts);
|
|
371
|
-
}
|
|
372
|
-
finally {
|
|
373
|
-
process.chdir(originalCwd);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
waitForUserInput(requestId, request, signal) {
|
|
377
|
-
return new Promise((resolve) => {
|
|
378
|
-
this.pendingInputRequests.set(requestId, { resolve, request });
|
|
379
|
-
// Only auto-resolve if the SDK itself aborts (e.g. session closed)
|
|
380
|
-
signal?.addEventListener('abort', () => {
|
|
381
|
-
this.pendingInputRequests.delete(requestId);
|
|
382
|
-
resolve({ sessionId: '', requestId, action: 'allow' });
|
|
383
|
-
}, { once: true });
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Called by message handler when PWA sends a user input response.
|
|
388
|
-
*/
|
|
389
|
-
resolveUserInput(response) {
|
|
390
|
-
const pending = this.pendingInputRequests.get(response.requestId);
|
|
391
|
-
if (!pending)
|
|
392
|
-
return false;
|
|
393
|
-
this.pendingInputRequests.delete(response.requestId);
|
|
394
|
-
pending.resolve(response);
|
|
395
|
-
// Clear pending input on the card
|
|
396
|
-
const ps = this.sessions.get(pending.request.sessionId);
|
|
397
|
-
if (ps?.cardBuilder) {
|
|
398
|
-
const cardEvt = ps.cardBuilder.clearPendingInput(response.requestId);
|
|
399
|
-
if (cardEvt)
|
|
400
|
-
this.emit('card-event', cardEvt);
|
|
401
|
-
}
|
|
402
|
-
this.emit('user-input-resolved', { requestId: response.requestId, sessionId: pending.request.sessionId });
|
|
403
|
-
this.emitSessionUpdate(pending.request.sessionId);
|
|
404
|
-
return true;
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Get all pending user input requests (for re-sending to a reconnected client).
|
|
408
|
-
*/
|
|
409
|
-
getPendingInputRequests() {
|
|
410
|
-
return Array.from(this.pendingInputRequests.values()).map((p) => p.request);
|
|
411
|
-
}
|
|
412
|
-
/** Debug snapshot of pending inputs and active sessions. */
|
|
413
|
-
getDebugState() {
|
|
414
|
-
const pendingInputs = Array.from(this.pendingInputRequests.values()).map((p) => ({
|
|
415
|
-
requestId: p.request.requestId,
|
|
416
|
-
sessionId: p.request.sessionId,
|
|
417
|
-
toolName: p.request.toolName,
|
|
418
|
-
agentId: p.request.agentId,
|
|
419
|
-
inputType: p.request.inputType,
|
|
420
|
-
}));
|
|
421
|
-
return { pendingInputs, activeSessions: this.getActiveSessions() };
|
|
422
|
-
}
|
|
423
|
-
async startSession(opts) {
|
|
424
|
-
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
|
425
|
-
const level = validModes.includes(opts.permissionMode)
|
|
426
|
-
? opts.permissionMode : 'acceptEdits';
|
|
427
|
-
// Mutable ref so canUseTool can access the real sessionId once init arrives
|
|
428
|
-
const deferred = createDeferred();
|
|
429
|
-
const sessionIdRef = { current: null, promise: deferred.promise };
|
|
430
|
-
const session = this.createSessionWithCwd(opts.cwd, null, {
|
|
431
|
-
allowedTools: opts.allowedTools,
|
|
432
|
-
model: opts.model,
|
|
433
|
-
permissionMode: opts.permissionMode,
|
|
434
|
-
sessionIdRef,
|
|
435
|
-
});
|
|
436
|
-
const prompt = opts.systemPrompt
|
|
437
|
-
? `[System context: ${opts.systemPrompt}]\n\n${opts.prompt}`
|
|
438
|
-
: opts.prompt;
|
|
439
|
-
await session.send(prompt);
|
|
440
|
-
// Create the stream generator ONCE — reused by processTurnLoop.
|
|
441
|
-
// IMPORTANT: Do NOT use for-await+break to consume init, because break
|
|
442
|
-
// calls generator.return() which closes the generator permanently.
|
|
443
|
-
const streamGen = session.stream();
|
|
444
|
-
// Pull messages manually until we get the init message
|
|
445
|
-
let sessionId;
|
|
446
|
-
while (!sessionId) {
|
|
447
|
-
const { value: message, done } = await streamGen.next();
|
|
448
|
-
if (done)
|
|
449
|
-
throw new Error('Stream ended before init message');
|
|
450
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
451
|
-
const id = message.session_id;
|
|
452
|
-
sessionIdRef.current = id;
|
|
453
|
-
deferred.resolve(id);
|
|
454
|
-
this.sessionPermissions.set(id, level);
|
|
455
|
-
this.sessions.set(id, {
|
|
456
|
-
session,
|
|
457
|
-
sessionId: id,
|
|
458
|
-
sessionIdRef,
|
|
459
|
-
cwd: opts.cwd,
|
|
460
|
-
streaming: true,
|
|
461
|
-
permissionLevel: level,
|
|
462
|
-
cardBuilder: null,
|
|
463
|
-
_promptQueue: [],
|
|
464
|
-
_cancelled: false,
|
|
465
|
-
_loopRunning: false,
|
|
466
|
-
_streamGenerator: streamGen,
|
|
467
|
-
});
|
|
468
|
-
this.emitSessionUpdate(id);
|
|
469
|
-
sessionId = id;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
// Seed the queue with the initial streamId (prompt already sent via session.send())
|
|
473
|
-
const ps = this.sessions.get(sessionId);
|
|
474
|
-
ps._promptQueue.push({ prompt: '', streamId: opts.streamId });
|
|
475
|
-
// Start the turn loop (reuses the same streamGen — picks up remaining messages)
|
|
476
|
-
this.processTurnLoop(sessionId);
|
|
477
|
-
return sessionId;
|
|
478
|
-
}
|
|
479
|
-
async resumeSession(opts) {
|
|
480
|
-
const existing = this.sessions.get(opts.sessionId);
|
|
481
|
-
if (existing) {
|
|
482
|
-
// Hot resume — push to queue
|
|
483
|
-
console.log(`[v2] hot resume (enqueue) session=${opts.sessionId}`);
|
|
484
|
-
existing._promptQueue.push({ prompt: opts.prompt, streamId: opts.streamId });
|
|
485
|
-
// If not looping, start the loop
|
|
486
|
-
if (!existing._loopRunning) {
|
|
487
|
-
this.processTurnLoop(opts.sessionId);
|
|
488
|
-
}
|
|
489
|
-
return opts.sessionId;
|
|
490
|
-
}
|
|
491
|
-
// Cold resume: create new session with resumeSessionId
|
|
492
|
-
console.log(`[v2] cold resume session=${opts.sessionId}`);
|
|
493
|
-
const deferred = createDeferred();
|
|
494
|
-
const sessionIdRef = { current: opts.sessionId, promise: deferred.promise };
|
|
495
|
-
deferred.resolve(opts.sessionId); // already known for resume
|
|
496
|
-
const restoredLevel = this.sessionPermissions.get(opts.sessionId) ?? 'acceptEdits';
|
|
497
|
-
const session = this.createSessionWithCwd(opts.cwd, opts.sessionId, {
|
|
498
|
-
resumeSessionId: opts.sessionId,
|
|
499
|
-
sessionIdRef,
|
|
500
|
-
});
|
|
501
|
-
await session.send(opts.prompt);
|
|
502
|
-
// Create the stream generator ONCE — reused by processTurnLoop.
|
|
503
|
-
// Use manual .next() to avoid for-await+break closing the generator.
|
|
504
|
-
const streamGen = session.stream();
|
|
505
|
-
let actualSessionId;
|
|
506
|
-
while (!actualSessionId) {
|
|
507
|
-
const { value: message, done } = await streamGen.next();
|
|
508
|
-
if (done)
|
|
509
|
-
throw new Error('Stream ended before init message');
|
|
510
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
511
|
-
const id = message.session_id;
|
|
512
|
-
if (id !== opts.sessionId) {
|
|
513
|
-
console.warn(`[v2] SDK created new session ${id} instead of resuming ${opts.sessionId}`);
|
|
514
|
-
}
|
|
515
|
-
sessionIdRef.current = id;
|
|
516
|
-
this.sessionPermissions.set(id, restoredLevel);
|
|
517
|
-
this.sessions.set(id, {
|
|
518
|
-
session,
|
|
519
|
-
sessionId: id,
|
|
520
|
-
sessionIdRef,
|
|
521
|
-
cwd: opts.cwd,
|
|
522
|
-
streaming: true,
|
|
523
|
-
permissionLevel: restoredLevel,
|
|
524
|
-
cardBuilder: null,
|
|
525
|
-
_promptQueue: [],
|
|
526
|
-
_cancelled: false,
|
|
527
|
-
_loopRunning: false,
|
|
528
|
-
_streamGenerator: streamGen,
|
|
529
|
-
});
|
|
530
|
-
this.emitSessionUpdate(id);
|
|
531
|
-
actualSessionId = id;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
// Seed the queue (prompt already sent)
|
|
535
|
-
const ps = this.sessions.get(actualSessionId);
|
|
536
|
-
ps._promptQueue.push({ prompt: '', streamId: opts.streamId });
|
|
537
|
-
// Start the turn loop (reuses same streamGen)
|
|
538
|
-
this.processTurnLoop(actualSessionId);
|
|
539
|
-
return actualSessionId;
|
|
540
|
-
}
|
|
541
|
-
cancelSession(sessionId) {
|
|
542
|
-
const ps = this.sessions.get(sessionId);
|
|
543
|
-
if (!ps)
|
|
544
|
-
return false;
|
|
545
|
-
ps._cancelled = true;
|
|
546
|
-
ps._promptQueue.length = 0; // drain queue
|
|
547
|
-
ps.streaming = false;
|
|
548
|
-
return true;
|
|
549
|
-
}
|
|
550
|
-
setPermissionLevel(sessionId, level) {
|
|
551
|
-
const ps = this.sessions.get(sessionId);
|
|
552
|
-
if (ps) {
|
|
553
|
-
ps.permissionLevel = level;
|
|
554
|
-
}
|
|
555
|
-
// Persist across active/inactive lifecycle
|
|
556
|
-
this.sessionPermissions.set(sessionId, level);
|
|
557
|
-
// Always broadcast so all clients sync, even for inactive sessions
|
|
558
|
-
this.emit('session-updated', {
|
|
559
|
-
sessionId,
|
|
560
|
-
isActive: !!ps,
|
|
561
|
-
isStreaming: ps?.streaming ?? false,
|
|
562
|
-
hasPendingInput: ps ? Array.from(this.pendingInputRequests.values()).some((p) => p.request.sessionId === sessionId) : false,
|
|
563
|
-
permissionMode: level,
|
|
564
|
-
});
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
getPermissionLevel(sessionId) {
|
|
568
|
-
return this.sessions.get(sessionId)?.permissionLevel ?? 'acceptEdits';
|
|
569
|
-
}
|
|
570
|
-
getActiveSessions() {
|
|
571
|
-
const pendingSessionIds = new Set(Array.from(this.pendingInputRequests.values()).map((p) => p.request.sessionId));
|
|
572
|
-
return Array.from(this.sessions.entries()).map(([id, ps]) => ({
|
|
573
|
-
sessionId: id,
|
|
574
|
-
cwd: ps.cwd,
|
|
575
|
-
isStreaming: ps.streaming,
|
|
576
|
-
hasPendingInput: pendingSessionIds.has(id),
|
|
577
|
-
permissionMode: ps.permissionLevel,
|
|
578
|
-
}));
|
|
579
|
-
}
|
|
580
|
-
closeSession(sessionId) {
|
|
581
|
-
const ps = this.sessions.get(sessionId);
|
|
582
|
-
if (!ps)
|
|
583
|
-
return false;
|
|
584
|
-
ps._cancelled = true;
|
|
585
|
-
ps._promptQueue.length = 0;
|
|
586
|
-
ps.session.close();
|
|
587
|
-
this.sessions.delete(sessionId);
|
|
588
|
-
this.emitSessionUpdate(sessionId);
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
isStreaming(sessionId) {
|
|
592
|
-
return this.sessions.get(sessionId)?.streaming ?? false;
|
|
593
|
-
}
|
|
594
|
-
isOpen(sessionId) {
|
|
595
|
-
return this.sessions.has(sessionId);
|
|
596
|
-
}
|
|
597
|
-
getActiveSessionCount() {
|
|
598
|
-
return this.sessions.size;
|
|
599
|
-
}
|
|
600
|
-
cleanup() {
|
|
601
|
-
// Force-resolve any pending inputs on daemon shutdown
|
|
602
|
-
for (const [requestId, pending] of this.pendingInputRequests) {
|
|
603
|
-
pending.resolve({ sessionId: '', requestId, action: 'allow' });
|
|
604
|
-
}
|
|
605
|
-
this.pendingInputRequests.clear();
|
|
606
|
-
for (const [, ps] of this.sessions) {
|
|
607
|
-
try {
|
|
608
|
-
ps.session.close();
|
|
609
|
-
}
|
|
610
|
-
catch {
|
|
611
|
-
// Ignore errors during cleanup
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
this.sessions.clear();
|
|
615
|
-
}
|
|
616
|
-
/** Get the CardBuilder for a session (if streaming). Used by canUseTool. */
|
|
617
|
-
getCardBuilder(sessionId) {
|
|
618
|
-
return this.sessions.get(sessionId)?.cardBuilder ?? null;
|
|
619
|
-
}
|
|
620
|
-
/** Get card-based history for a session. */
|
|
621
|
-
async getCards(sessionId, cwd, offset = 0, limit = 50) {
|
|
622
|
-
// Always use JSONL as the source of truth for history.
|
|
623
|
-
// Overlay pendingInput from in-memory state (only matters during permission prompts).
|
|
624
|
-
const result = await buildCardsFromHistory(sessionId, cwd, offset, limit);
|
|
625
|
-
// Attach pendingInput to matching tool cards
|
|
626
|
-
const pendingByToolUseId = new Map();
|
|
627
|
-
for (const [, pending] of this.pendingInputRequests) {
|
|
628
|
-
if (pending.request.sessionId === sessionId && pending.request.toolUseId) {
|
|
629
|
-
pendingByToolUseId.set(pending.request.toolUseId, {
|
|
630
|
-
sessionId: pending.request.sessionId,
|
|
631
|
-
requestId: pending.request.requestId,
|
|
632
|
-
inputType: pending.request.inputType,
|
|
633
|
-
title: pending.request.title,
|
|
634
|
-
message: pending.request.message,
|
|
635
|
-
options: pending.request.options,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
if (pendingByToolUseId.size > 0) {
|
|
640
|
-
for (const card of result.cards) {
|
|
641
|
-
if (card.type === 'tool_call' && card.toolUseId) {
|
|
642
|
-
const pending = pendingByToolUseId.get(card.toolUseId);
|
|
643
|
-
if (pending)
|
|
644
|
-
card.pendingInput = pending;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return result;
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* Turn loop: dequeue prompt → send() → consume stream → on result, check queue → repeat.
|
|
652
|
-
* The first entry may have an empty prompt (already sent by startSession/resumeSession).
|
|
653
|
-
*/
|
|
654
|
-
async processTurnLoop(sessionId) {
|
|
655
|
-
const ps = this.sessions.get(sessionId);
|
|
656
|
-
if (!ps || ps._loopRunning)
|
|
657
|
-
return;
|
|
658
|
-
ps._loopRunning = true;
|
|
659
|
-
let textBuffer = '';
|
|
660
|
-
let bufferTimer = null;
|
|
661
|
-
const emitCard = (event) => { this.emit('card-event', event); };
|
|
662
|
-
// Create cardBuilder once, reuse across turns (accumulates all cards)
|
|
663
|
-
if (!ps.cardBuilder) {
|
|
664
|
-
ps.cardBuilder = new StreamCardBuilder(sessionId, '');
|
|
665
|
-
}
|
|
666
|
-
try {
|
|
667
|
-
while (ps._promptQueue.length > 0) {
|
|
668
|
-
const { prompt, streamId } = ps._promptQueue.shift();
|
|
669
|
-
ps._cancelled = false;
|
|
670
|
-
ps.streaming = true;
|
|
671
|
-
const cb = ps.cardBuilder;
|
|
672
|
-
cb.startNewTurn(streamId);
|
|
673
|
-
this.emitSessionUpdate(sessionId);
|
|
674
|
-
// Reset text buffering for this turn
|
|
675
|
-
textBuffer = '';
|
|
676
|
-
if (bufferTimer) {
|
|
677
|
-
clearTimeout(bufferTimer);
|
|
678
|
-
bufferTimer = null;
|
|
679
|
-
}
|
|
680
|
-
const flushText = () => {
|
|
681
|
-
if (textBuffer) {
|
|
682
|
-
console.log(`[stream] flushText len=${textBuffer.length} session=${sessionId.slice(0, 8)}`);
|
|
683
|
-
emitCard(cb.assistantText(textBuffer));
|
|
684
|
-
textBuffer = '';
|
|
685
|
-
}
|
|
686
|
-
if (bufferTimer) {
|
|
687
|
-
clearTimeout(bufferTimer);
|
|
688
|
-
bufferTimer = null;
|
|
689
|
-
}
|
|
690
|
-
};
|
|
691
|
-
const bufferText = (text) => {
|
|
692
|
-
textBuffer += text;
|
|
693
|
-
if (!bufferTimer) {
|
|
694
|
-
bufferTimer = setTimeout(flushText, 150);
|
|
695
|
-
}
|
|
696
|
-
if (textBuffer.length > 2048) {
|
|
697
|
-
flushText();
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
// Send prompt if non-empty (empty means prompt was already sent before loop started)
|
|
701
|
-
if (prompt) {
|
|
702
|
-
emitCard(cb.userMessage(prompt));
|
|
703
|
-
await ps.session.send(prompt);
|
|
704
|
-
// New turn = new generator (v2 API: one stream() call per send())
|
|
705
|
-
ps._streamGenerator = ps.session.stream();
|
|
706
|
-
}
|
|
707
|
-
// Consume this turn's stream using manual .next() to avoid
|
|
708
|
-
// for-await+break calling generator.return() which kills the stream.
|
|
709
|
-
const gen = ps._streamGenerator;
|
|
710
|
-
if (!gen) {
|
|
711
|
-
console.error(`[stream] no generator for session=${sessionId}`);
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
714
|
-
let turnDone = false;
|
|
715
|
-
try {
|
|
716
|
-
while (!turnDone && !ps._cancelled) {
|
|
717
|
-
const { value: message, done } = await gen.next();
|
|
718
|
-
if (done) {
|
|
719
|
-
turnDone = true;
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
// Subagent lifecycle events
|
|
723
|
-
if (message.type === 'system' && message.subtype === 'task_started') {
|
|
724
|
-
const m = message;
|
|
725
|
-
flushText();
|
|
726
|
-
emitCard(cb.subagentStart(m.description ?? '', m.task_id, m.tool_use_id));
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
if (message.type === 'system' && message.subtype === 'task_progress') {
|
|
730
|
-
const m = message;
|
|
731
|
-
const cardEvt = cb.subagentProgress(m.task_id, m.tool_use_id, m.usage?.tool_uses, m.last_tool_name);
|
|
732
|
-
if (cardEvt)
|
|
733
|
-
emitCard(cardEvt);
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
if (message.type === 'system' && message.subtype === 'task_notification') {
|
|
737
|
-
const m = message;
|
|
738
|
-
const cardEvt = cb.subagentEnd(m.task_id, m.tool_use_id, m.status, m.summary);
|
|
739
|
-
if (cardEvt)
|
|
740
|
-
emitCard(cardEvt);
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
// Session state changed — 'idle' means turn is over
|
|
744
|
-
if (message.type === 'system' && message.subtype === 'session_state_changed') {
|
|
745
|
-
const state = message.state;
|
|
746
|
-
if (state === 'idle') {
|
|
747
|
-
flushText();
|
|
748
|
-
console.log(`[stream] session_state_changed=idle session=${sessionId}`);
|
|
749
|
-
}
|
|
750
|
-
continue;
|
|
751
|
-
}
|
|
752
|
-
// Streaming partial events
|
|
753
|
-
if (message.type === 'stream_event') {
|
|
754
|
-
const event = message.event;
|
|
755
|
-
if (event?.type === 'content_block_delta') {
|
|
756
|
-
const delta = event.delta;
|
|
757
|
-
if (delta?.type === 'text_delta' && delta.text) {
|
|
758
|
-
bufferText(delta.text);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
else if (!event?.type?.includes('delta')) {
|
|
762
|
-
console.log(`[stream] stream_event type=${event?.type} session=${sessionId.slice(0, 8)}`);
|
|
763
|
-
}
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
// Complete assistant messages — skip sidechain (subagent) messages.
|
|
767
|
-
if (message.type === 'assistant') {
|
|
768
|
-
if (message.agentId)
|
|
769
|
-
continue; // sidechain message — ignore
|
|
770
|
-
flushText();
|
|
771
|
-
const betaMessage = message.message;
|
|
772
|
-
const blocks = betaMessage?.content ?? [];
|
|
773
|
-
for (const block of blocks) {
|
|
774
|
-
if (block.type === 'thinking' && block.thinking) {
|
|
775
|
-
emitCard(cb.thinkingBlock(block.thinking));
|
|
776
|
-
}
|
|
777
|
-
else if (block.type === 'redacted_thinking') {
|
|
778
|
-
emitCard(cb.thinkingBlock('[Redacted thinking]'));
|
|
779
|
-
}
|
|
780
|
-
else if (block.type === 'text' && block.text) {
|
|
781
|
-
emitCard(cb.assistantText(block.text));
|
|
782
|
-
}
|
|
783
|
-
else if (block.type === 'tool_use') {
|
|
784
|
-
// Agent tool calls are represented by SubagentCards (via task_started).
|
|
785
|
-
if (block.name !== 'Agent') {
|
|
786
|
-
emitCard(cb.toolUse(block.name, block.input ?? {}, block.id));
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
else if (block.type === 'server_tool_use' || block.type === 'mcp_tool_use') {
|
|
790
|
-
emitCard(cb.toolUse(block.name ?? block.type, block.input ?? {}, block.id ?? ''));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
continue;
|
|
794
|
-
}
|
|
795
|
-
// User messages contain tool results — skip sidechain messages.
|
|
796
|
-
if (message.type === 'user') {
|
|
797
|
-
if (message.agentId)
|
|
798
|
-
continue; // sidechain message — ignore
|
|
799
|
-
const userMsg = message.message;
|
|
800
|
-
if (userMsg?.content && Array.isArray(userMsg.content)) {
|
|
801
|
-
for (const block of userMsg.content) {
|
|
802
|
-
if (block.type === 'tool_result') {
|
|
803
|
-
const resultContent = extractToolResultText(block.content);
|
|
804
|
-
const cardEvt = cb.toolResult(block.tool_use_id, resultContent, !!block.is_error);
|
|
805
|
-
if (cardEvt)
|
|
806
|
-
emitCard(cardEvt);
|
|
807
|
-
}
|
|
808
|
-
// Handle server/MCP tool results in streaming
|
|
809
|
-
if (block.type === 'web_search_tool_result' || block.type === 'web_fetch_tool_result' ||
|
|
810
|
-
block.type === 'mcp_tool_result' || block.type === 'code_execution_tool_result' ||
|
|
811
|
-
block.type === 'tool_search_tool_result') {
|
|
812
|
-
const resultContent = extractToolResultText(block.content ?? block.text ?? '');
|
|
813
|
-
const parentId = block.tool_use_id;
|
|
814
|
-
if (parentId) {
|
|
815
|
-
const cardEvt = cb.toolResult(parentId, resultContent, !!block.is_error);
|
|
816
|
-
if (cardEvt)
|
|
817
|
-
emitCard(cardEvt);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
// ── Result — exit this turn's stream, loop will check queue ──
|
|
825
|
-
if (message.type === 'result') {
|
|
826
|
-
if (message.session_id !== sessionId) {
|
|
827
|
-
console.log(`[stream] skip subagent result session=${message.session_id?.slice(0, 8)}`);
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
flushText();
|
|
831
|
-
const result = message;
|
|
832
|
-
console.log(`[stream] result session=${sessionId} subtype=${result.subtype} cost=$${result.total_cost_usd?.toFixed(4) ?? '?'}`);
|
|
833
|
-
const streamEnd = {
|
|
834
|
-
streamId,
|
|
835
|
-
sessionId,
|
|
836
|
-
success: result.subtype === 'success',
|
|
837
|
-
error: result.subtype !== 'success'
|
|
838
|
-
? (result.errors?.join('; ') || `Session ended: ${result.subtype}`)
|
|
839
|
-
: undefined,
|
|
840
|
-
totalCostUsd: result.total_cost_usd,
|
|
841
|
-
tokenUsage: result.usage
|
|
842
|
-
? { input: result.usage.input_tokens, output: result.usage.output_tokens }
|
|
843
|
-
: undefined,
|
|
844
|
-
};
|
|
845
|
-
this.emit('card-stream-end', streamEnd);
|
|
846
|
-
turnDone = true; // exit this turn's stream without closing generator
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
catch (error) {
|
|
851
|
-
flushText();
|
|
852
|
-
console.error(`[stream] error session=${sessionId}:`, error);
|
|
853
|
-
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
854
|
-
this.emit('card-stream-end', { streamId, sessionId, success: false, error: msg });
|
|
855
|
-
}
|
|
856
|
-
// Turn done — finalize text, keep cardBuilder (accumulates across turns)
|
|
857
|
-
const finalizeEvent = cb.finalizeAssistantText();
|
|
858
|
-
if (finalizeEvent)
|
|
859
|
-
emitCard(finalizeEvent);
|
|
860
|
-
ps.streaming = false;
|
|
861
|
-
this.emitSessionUpdate(sessionId);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
finally {
|
|
865
|
-
ps._loopRunning = false;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
//# sourceMappingURL=claudeCodeService.js.map
|