linkshell-cli 0.2.72 → 0.2.74

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.
@@ -0,0 +1,1086 @@
1
+ import { basename } from "node:path";
2
+ import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
3
+ import { AcpClient } from "./acp-client.js";
4
+ import { resolveAgentCommand } from "./provider-resolver.js";
5
+ const PERMISSION_TIMEOUT_MS = 5 * 60_000;
6
+ const MAX_TIMELINE_ITEMS = 200;
7
+ function id(prefix) {
8
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
9
+ }
10
+ function stringify(value) {
11
+ if (typeof value === "string")
12
+ return value;
13
+ try {
14
+ return JSON.stringify(value, null, 2);
15
+ }
16
+ catch {
17
+ return String(value);
18
+ }
19
+ }
20
+ function asRecord(value) {
21
+ return typeof value === "object" && value ? value : undefined;
22
+ }
23
+ function firstString(value, keys) {
24
+ if (!value)
25
+ return undefined;
26
+ for (const key of keys) {
27
+ const next = value[key];
28
+ if (typeof next === "string" && next.length > 0)
29
+ return next;
30
+ }
31
+ return undefined;
32
+ }
33
+ function extractItem(value) {
34
+ const raw = asRecord(value);
35
+ if (!raw)
36
+ return undefined;
37
+ return asRecord(raw.item) ?? raw;
38
+ }
39
+ function stringifyDefined(value) {
40
+ if (value === undefined || value === null || value === "")
41
+ return undefined;
42
+ return stringify(value);
43
+ }
44
+ function appendCapped(current, delta, maxLength) {
45
+ const next = `${current ?? ""}${delta}`;
46
+ if (next.length <= maxLength)
47
+ return next;
48
+ return next.slice(next.length - maxLength);
49
+ }
50
+ function decodeBase64(value) {
51
+ if (!value)
52
+ return undefined;
53
+ try {
54
+ return Buffer.from(value, "base64").toString("utf8");
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ function normalizeToolStatus(value, completedFallback = false) {
61
+ if (value === "completed" || value === "succeeded" || value === "success" || value === "applied") {
62
+ return "completed";
63
+ }
64
+ if (value === "failed" || value === "error" || value === "declined" || value === "cancelled") {
65
+ return "failed";
66
+ }
67
+ if (value === "pending" || value === "queued")
68
+ return "pending";
69
+ if (value === "running" || value === "inProgress" || value === "executing")
70
+ return "running";
71
+ return completedFallback ? "completed" : "running";
72
+ }
73
+ function normalizePlanStatus(value) {
74
+ if (value === "completed" || value === "done")
75
+ return "completed";
76
+ if (value === "inProgress" || value === "running" || value === "active")
77
+ return "in_progress";
78
+ return "pending";
79
+ }
80
+ function nameFromToolMethod(method) {
81
+ if (method.includes("commandExecution"))
82
+ return "命令";
83
+ if (method.includes("fileChange"))
84
+ return "文件修改";
85
+ if (method.includes("mcpToolCall"))
86
+ return "MCP 工具";
87
+ return "工具";
88
+ }
89
+ function toolNameFromItem(item) {
90
+ const itemType = firstString(item, ["type"]);
91
+ if (itemType === "commandExecution")
92
+ return "命令";
93
+ if (itemType === "fileChange")
94
+ return "文件修改";
95
+ if (itemType === "mcpToolCall") {
96
+ const server = firstString(item, ["server"]);
97
+ const tool = firstString(item, ["tool", "toolName", "name"]);
98
+ return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
99
+ }
100
+ if (itemType === "dynamicToolCall") {
101
+ const namespace = firstString(item, ["namespace"]);
102
+ const tool = firstString(item, ["tool", "toolName", "name"]);
103
+ return [namespace, tool].filter(Boolean).join(" · ") || "工具";
104
+ }
105
+ return firstString(item, ["toolName", "tool", "name", "title"]) ?? itemType;
106
+ }
107
+ function summarizeFileChanges(changes) {
108
+ const lines = changes
109
+ .map((change) => {
110
+ const raw = asRecord(change);
111
+ if (!raw)
112
+ return undefined;
113
+ const path = firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
114
+ firstString(asRecord(raw.update), ["path", "file", "filePath"]);
115
+ const kind = firstString(raw, ["kind", "type", "operation", "action"]);
116
+ return [kind, path].filter(Boolean).join(" ");
117
+ })
118
+ .filter((line) => Boolean(line));
119
+ return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
120
+ }
121
+ function toolInputFromItem(item) {
122
+ const itemType = firstString(item, ["type"]);
123
+ if (itemType === "commandExecution") {
124
+ const command = firstString(item, ["command"]);
125
+ const cwd = firstString(item, ["cwd"]);
126
+ if (command && cwd)
127
+ return `${command}\n\ncwd: ${cwd}`;
128
+ return command ?? cwd;
129
+ }
130
+ if (itemType === "fileChange") {
131
+ const changes = Array.isArray(item.changes) ? item.changes : [];
132
+ return summarizeFileChanges(changes);
133
+ }
134
+ return stringifyDefined(item.arguments ?? item.input ?? item.toolInput);
135
+ }
136
+ function titleFromCwd(cwd) {
137
+ return basename(cwd) || cwd || "Agent";
138
+ }
139
+ function textFromBlocks(blocks) {
140
+ return blocks
141
+ .map((block) => block.type === "text" ? block.text ?? "" : `[${block.mimeType ?? "image"} attachment]`)
142
+ .filter(Boolean)
143
+ .join("\n");
144
+ }
145
+ function previewText(text) {
146
+ return text.replace(/\s+/g, " ").trim().slice(0, 160);
147
+ }
148
+ export class AgentWorkspaceProxy {
149
+ input;
150
+ client;
151
+ initialized = false;
152
+ status = "unavailable";
153
+ error;
154
+ activeConversationId;
155
+ currentTurnId;
156
+ conversations = new Map();
157
+ conversationByAgentSessionId = new Map();
158
+ timelines = new Map();
159
+ toolOutputBuffers = new Map();
160
+ pendingPermissions = new Map();
161
+ permissionWaiters = new Map();
162
+ permissionSources = new Map();
163
+ toolConversationIds = new Map();
164
+ agentProtocol;
165
+ constructor(input) {
166
+ this.input = input;
167
+ }
168
+ async handleEnvelope(envelope) {
169
+ switch (envelope.type) {
170
+ case "agent.v2.capabilities.request":
171
+ await this.initialize();
172
+ this.sendCapabilities();
173
+ break;
174
+ case "agent.v2.conversation.open": {
175
+ const payload = parseTypedPayload("agent.v2.conversation.open", envelope.payload);
176
+ await this.openConversation(payload);
177
+ break;
178
+ }
179
+ case "agent.v2.conversation.list": {
180
+ const payload = parseTypedPayload("agent.v2.conversation.list", envelope.payload);
181
+ const conversations = [...this.conversations.values()].filter((conversation) => payload.includeArchived ? true : !conversation.archived);
182
+ this.input.send(createEnvelope({
183
+ type: "agent.v2.conversation.list.result",
184
+ sessionId: this.input.sessionId,
185
+ payload: { conversations },
186
+ }));
187
+ break;
188
+ }
189
+ case "agent.v2.snapshot.request": {
190
+ const payload = parseTypedPayload("agent.v2.snapshot.request", envelope.payload);
191
+ this.sendSnapshot(payload.conversationId);
192
+ break;
193
+ }
194
+ case "agent.v2.prompt": {
195
+ const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
196
+ await this.sendPrompt(payload);
197
+ break;
198
+ }
199
+ case "agent.v2.cancel": {
200
+ const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
201
+ const conversation = this.conversations.get(payload.conversationId);
202
+ this.cancelPendingPermissions(payload.conversationId);
203
+ this.client?.cancel({
204
+ sessionId: conversation?.agentSessionId,
205
+ turnId: this.currentTurnId,
206
+ });
207
+ this.currentTurnId = undefined;
208
+ this.updateConversationStatus(payload.conversationId, "idle");
209
+ this.emitStatus(payload.conversationId, "idle", "已停止");
210
+ break;
211
+ }
212
+ case "agent.v2.permission.respond": {
213
+ const payload = parseTypedPayload("agent.v2.permission.respond", envelope.payload);
214
+ this.respondPermission(payload);
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ stop() {
220
+ this.client?.stop();
221
+ this.client = undefined;
222
+ }
223
+ async initialize() {
224
+ if (this.initialized)
225
+ return;
226
+ await this.ensureClient();
227
+ }
228
+ async ensureClient() {
229
+ if (this.client)
230
+ return;
231
+ const resolved = resolveAgentCommand({
232
+ provider: this.input.provider,
233
+ command: this.input.command,
234
+ });
235
+ if (!resolved) {
236
+ this.status = "unavailable";
237
+ this.error = `Agent Workspace requires --agent-command for ${this.input.provider}`;
238
+ return;
239
+ }
240
+ try {
241
+ this.agentProtocol = resolved.protocol;
242
+ this.client = new AcpClient({
243
+ command: resolved.command,
244
+ protocol: resolved.protocol,
245
+ framing: resolved.framing,
246
+ cwd: this.input.cwd,
247
+ onNotification: (method, params) => this.handleNotification(method, params),
248
+ onRequest: (method, params) => this.handleRequest(method, params),
249
+ onExit: (message) => this.handleExit(message),
250
+ });
251
+ await this.client.initialize();
252
+ this.initialized = true;
253
+ this.status = "idle";
254
+ this.error = undefined;
255
+ }
256
+ catch (error) {
257
+ this.client?.stop();
258
+ this.client = undefined;
259
+ this.status = "error";
260
+ this.error = error instanceof Error ? error.message : String(error);
261
+ }
262
+ }
263
+ sendCapabilities() {
264
+ const enabled = Boolean(this.client && this.initialized && !this.error);
265
+ const supportsImages = enabled && this.agentProtocol === "codex-app-server";
266
+ this.input.send(createEnvelope({
267
+ type: "agent.v2.capabilities",
268
+ sessionId: this.input.sessionId,
269
+ payload: {
270
+ enabled,
271
+ provider: this.input.provider,
272
+ protocolVersion: 1,
273
+ workspaceProtocolVersion: 2,
274
+ error: enabled ? undefined : this.error,
275
+ supportsSessionList: enabled,
276
+ supportsSessionLoad: enabled,
277
+ supportsImages,
278
+ supportsAudio: false,
279
+ supportsPermission: enabled,
280
+ supportsPlan: enabled,
281
+ supportsCancel: enabled,
282
+ },
283
+ }));
284
+ }
285
+ async openConversation(payload) {
286
+ await this.ensureClient();
287
+ this.sendCapabilities();
288
+ if (!this.client)
289
+ return undefined;
290
+ const cwd = payload.cwd ?? this.input.cwd;
291
+ let agentSessionId = payload.agentSessionId;
292
+ const existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
293
+ (agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
294
+ if (existingConversation) {
295
+ this.activeConversationId = existingConversation.id;
296
+ this.input.send(createEnvelope({
297
+ type: "agent.v2.conversation.opened",
298
+ sessionId: this.input.sessionId,
299
+ payload: {
300
+ conversation: existingConversation,
301
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
302
+ },
303
+ }));
304
+ return existingConversation;
305
+ }
306
+ try {
307
+ const result = agentSessionId
308
+ ? await this.client.loadSession({ sessionId: agentSessionId, cwd })
309
+ : await this.client.newSession({ cwd });
310
+ agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
311
+ const now = Date.now();
312
+ const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
313
+ const conversation = {
314
+ id: conversationId,
315
+ agentSessionId,
316
+ provider: payload.provider ?? this.input.provider,
317
+ cwd,
318
+ title: payload.title ?? titleFromCwd(cwd),
319
+ model: payload.model,
320
+ reasoningEffort: payload.reasoningEffort,
321
+ permissionMode: payload.permissionMode,
322
+ status: "idle",
323
+ archived: false,
324
+ lastActivityAt: now,
325
+ createdAt: now,
326
+ };
327
+ this.conversations.set(conversation.id, conversation);
328
+ this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
329
+ this.activeConversationId = conversation.id;
330
+ this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
331
+ this.input.send(createEnvelope({
332
+ type: "agent.v2.conversation.opened",
333
+ sessionId: this.input.sessionId,
334
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
335
+ }));
336
+ return conversation;
337
+ }
338
+ catch (error) {
339
+ const message = error instanceof Error ? error.message : String(error);
340
+ this.status = "error";
341
+ this.error = message;
342
+ const fallbackId = payload.conversationId ?? id("agent-conversation");
343
+ const now = Date.now();
344
+ const conversation = {
345
+ id: fallbackId,
346
+ provider: payload.provider ?? this.input.provider,
347
+ cwd,
348
+ title: payload.title ?? titleFromCwd(cwd),
349
+ model: payload.model,
350
+ reasoningEffort: payload.reasoningEffort,
351
+ permissionMode: payload.permissionMode,
352
+ status: "error",
353
+ archived: false,
354
+ lastMessagePreview: message,
355
+ lastActivityAt: now,
356
+ createdAt: now,
357
+ };
358
+ this.conversations.set(conversation.id, conversation);
359
+ this.activeConversationId = conversation.id;
360
+ this.addItem(conversation.id, {
361
+ id: id("error"),
362
+ conversationId: conversation.id,
363
+ type: "error",
364
+ error: message,
365
+ createdAt: now,
366
+ });
367
+ this.input.send(createEnvelope({
368
+ type: "agent.v2.conversation.opened",
369
+ sessionId: this.input.sessionId,
370
+ payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
371
+ }));
372
+ return conversation;
373
+ }
374
+ }
375
+ async sendPrompt(payload) {
376
+ const conversation = this.conversations.get(payload.conversationId) ??
377
+ await this.openConversation({ conversationId: payload.conversationId });
378
+ if (!conversation || !this.client || !conversation.agentSessionId)
379
+ return;
380
+ if (payload.contentBlocks.some((block) => block.type === "image") && this.agentProtocol !== "codex-app-server") {
381
+ conversation.status = "idle";
382
+ conversation.lastActivityAt = Date.now();
383
+ this.emitConversation(conversation);
384
+ this.addItem(conversation.id, {
385
+ id: id("error"),
386
+ conversationId: conversation.id,
387
+ type: "error",
388
+ error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
389
+ createdAt: Date.now(),
390
+ });
391
+ return;
392
+ }
393
+ conversation.model = payload.model ?? conversation.model;
394
+ conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
395
+ conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
396
+ conversation.status = "running";
397
+ conversation.lastActivityAt = Date.now();
398
+ this.activeConversationId = conversation.id;
399
+ const userText = textFromBlocks(payload.contentBlocks);
400
+ this.addItem(conversation.id, {
401
+ id: payload.clientMessageId,
402
+ conversationId: conversation.id,
403
+ type: "message",
404
+ role: "user",
405
+ content: payload.contentBlocks,
406
+ text: userText,
407
+ createdAt: Date.now(),
408
+ });
409
+ this.emitConversation(conversation);
410
+ try {
411
+ const result = await this.client.prompt({
412
+ sessionId: conversation.agentSessionId,
413
+ content: payload.contentBlocks,
414
+ clientMessageId: payload.clientMessageId,
415
+ model: payload.model,
416
+ reasoningEffort: payload.reasoningEffort,
417
+ permissionMode: payload.permissionMode,
418
+ cwd: conversation.cwd,
419
+ });
420
+ this.currentTurnId = this.extractTurnId(result) ?? this.currentTurnId;
421
+ if (conversation.status === "running") {
422
+ this.updateConversationStatus(conversation.id, "idle");
423
+ }
424
+ }
425
+ catch (error) {
426
+ const message = error instanceof Error ? error.message : String(error);
427
+ this.updateConversationStatus(conversation.id, "error", message);
428
+ this.addItem(conversation.id, {
429
+ id: id("error"),
430
+ conversationId: conversation.id,
431
+ type: "error",
432
+ error: message,
433
+ createdAt: Date.now(),
434
+ });
435
+ }
436
+ }
437
+ handleRequest(method, params) {
438
+ if (method === "session/request_permission" ||
439
+ method.endsWith("/requestApproval") ||
440
+ method === "mcpServer/elicitation/request" ||
441
+ method === "item/tool/requestUserInput") {
442
+ return this.handlePermission(params, true, method);
443
+ }
444
+ if (this.input.verbose) {
445
+ process.stderr.write(`[agent:v2:request] unsupported ${method}\n`);
446
+ }
447
+ return {};
448
+ }
449
+ handleNotification(method, params) {
450
+ if (this.input.verbose) {
451
+ process.stderr.write(`[agent:v2] ${method} ${stringify(params).slice(0, 500)}\n`);
452
+ }
453
+ if (method === "initialized" ||
454
+ method.startsWith("account/") ||
455
+ method.startsWith("mcpServer/startupStatus/") ||
456
+ method === "thread/status/changed" ||
457
+ method === "thread/tokenUsage/updated" ||
458
+ method === "turn/diff/updated" ||
459
+ method === "serverRequest/resolved" ||
460
+ method === "mcpServer/oauthLogin/completed") {
461
+ return;
462
+ }
463
+ const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
464
+ if (method === "thread/started") {
465
+ const agentSessionId = this.extractSessionId(params);
466
+ if (agentSessionId && conversationId) {
467
+ this.conversationByAgentSessionId.set(agentSessionId, conversationId);
468
+ const conversation = this.conversations.get(conversationId);
469
+ if (conversation)
470
+ conversation.agentSessionId = agentSessionId;
471
+ }
472
+ return;
473
+ }
474
+ if (method === "turn/started") {
475
+ this.currentTurnId = this.extractTurnId(params) ?? this.currentTurnId;
476
+ if (conversationId)
477
+ this.updateConversationStatus(conversationId, "running");
478
+ return;
479
+ }
480
+ if (method === "turn/completed") {
481
+ this.currentTurnId = undefined;
482
+ if (conversationId)
483
+ this.updateConversationStatus(conversationId, "idle");
484
+ return;
485
+ }
486
+ if (method === "session/request_permission") {
487
+ this.handlePermission(params, false, method);
488
+ return;
489
+ }
490
+ switch (method) {
491
+ case "item/agentMessage/delta":
492
+ this.handleAgentMessageDelta(params);
493
+ return;
494
+ case "turn/plan/updated":
495
+ this.handlePlanUpdated(params);
496
+ return;
497
+ case "item/plan/delta":
498
+ this.handlePlanDelta(params);
499
+ return;
500
+ case "item/started":
501
+ this.handleItemStarted(params);
502
+ return;
503
+ case "item/completed":
504
+ this.handleItemCompleted(params);
505
+ return;
506
+ case "item/commandExecution/outputDelta":
507
+ case "item/fileChange/outputDelta":
508
+ case "item/mcpToolCall/progress":
509
+ this.handleToolDelta(method, params);
510
+ return;
511
+ case "item/fileChange/patchUpdated":
512
+ this.handleFilePatchUpdated(params);
513
+ return;
514
+ case "command/exec/outputDelta":
515
+ this.handleCommandExecDelta(params);
516
+ return;
517
+ case "item/autoApprovalReview/started":
518
+ case "item/autoApprovalReview/completed":
519
+ case "item/commandExecution/terminalInteraction":
520
+ return;
521
+ }
522
+ if (method === "session/update") {
523
+ this.handleSessionUpdate(params);
524
+ return;
525
+ }
526
+ }
527
+ handleAgentMessageDelta(params) {
528
+ const raw = asRecord(params);
529
+ if (!raw)
530
+ return;
531
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
532
+ if (!conversationId)
533
+ return;
534
+ const itemId = firstString(raw, ["itemId", "id", "messageId"]) ?? id("msg");
535
+ const delta = firstString(raw, ["delta", "text", "content"]);
536
+ if (!delta)
537
+ return;
538
+ const existing = this.findItem(conversationId, itemId);
539
+ const text = `${existing?.text ?? ""}${delta}`;
540
+ const item = {
541
+ id: itemId,
542
+ conversationId,
543
+ type: "message",
544
+ role: "assistant",
545
+ content: [{ type: "text", text }],
546
+ text,
547
+ createdAt: existing?.createdAt ?? Date.now(),
548
+ updatedAt: Date.now(),
549
+ isStreaming: true,
550
+ };
551
+ this.upsertItem(conversationId, item);
552
+ this.updateConversationPreview(conversationId, text, "running");
553
+ }
554
+ handlePlanUpdated(params) {
555
+ const raw = asRecord(params);
556
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
557
+ if (!conversationId)
558
+ return;
559
+ const plan = Array.isArray(raw?.plan) ? raw.plan : [];
560
+ const steps = plan
561
+ .map((entry, index) => {
562
+ const step = asRecord(entry);
563
+ const text = firstString(step, ["text", "title", "description", "message"]);
564
+ if (!text)
565
+ return undefined;
566
+ return {
567
+ id: firstString(step, ["id"]) ?? `plan-${index + 1}`,
568
+ text,
569
+ status: normalizePlanStatus(step?.status),
570
+ };
571
+ })
572
+ .filter((step) => Boolean(step));
573
+ if (steps.length === 0)
574
+ return;
575
+ this.upsertItem(conversationId, {
576
+ id: "plan",
577
+ conversationId,
578
+ type: "plan",
579
+ plan: steps,
580
+ createdAt: this.findItem(conversationId, "plan")?.createdAt ?? Date.now(),
581
+ updatedAt: Date.now(),
582
+ });
583
+ }
584
+ handlePlanDelta(params) {
585
+ const raw = asRecord(params);
586
+ if (!raw)
587
+ return;
588
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
589
+ if (!conversationId)
590
+ return;
591
+ const itemId = firstString(raw, ["itemId", "id"]) ?? "plan";
592
+ const delta = firstString(raw, ["delta", "text"]);
593
+ if (!delta)
594
+ return;
595
+ const existing = this.findItem(conversationId, itemId);
596
+ const text = `${existing?.text ?? ""}${delta}`;
597
+ const step = { id: itemId, text, status: "in_progress" };
598
+ this.upsertItem(conversationId, {
599
+ id: itemId,
600
+ conversationId,
601
+ type: "plan",
602
+ text,
603
+ plan: [step],
604
+ createdAt: existing?.createdAt ?? Date.now(),
605
+ updatedAt: Date.now(),
606
+ });
607
+ }
608
+ handleItemStarted(params) {
609
+ const item = extractItem(params);
610
+ if (!item)
611
+ return;
612
+ const itemType = firstString(item, ["type"]);
613
+ if (itemType === "agentMessage" || itemType === "assistantMessage") {
614
+ this.handleCompletedMessageItem(item, true);
615
+ return;
616
+ }
617
+ if (itemType === "plan") {
618
+ this.handlePlanUpdated({ plan: [item] });
619
+ return;
620
+ }
621
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
622
+ const toolCall = this.toolCallFromItem(item, "running");
623
+ if (!conversationId || !toolCall)
624
+ return;
625
+ this.toolConversationIds.set(toolCall.id, conversationId);
626
+ this.upsertTool(conversationId, toolCall);
627
+ }
628
+ handleItemCompleted(params) {
629
+ const item = extractItem(params);
630
+ if (!item)
631
+ return;
632
+ const itemType = firstString(item, ["type"]);
633
+ if (itemType === "agentMessage" || itemType === "assistantMessage") {
634
+ this.handleCompletedMessageItem(item, false);
635
+ return;
636
+ }
637
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
638
+ const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
639
+ if (!conversationId || !toolCall)
640
+ return;
641
+ const bufferedOutput = this.toolOutputBuffers.get(toolCall.id);
642
+ if (bufferedOutput && !toolCall.output)
643
+ toolCall.output = bufferedOutput;
644
+ this.upsertTool(conversationId, toolCall);
645
+ }
646
+ handleToolDelta(method, params) {
647
+ const raw = asRecord(params);
648
+ if (!raw)
649
+ return;
650
+ const itemId = firstString(raw, ["itemId", "id", "toolCallId"]) ?? id("tool");
651
+ const delta = firstString(raw, ["delta", "message", "text"]);
652
+ if (!delta)
653
+ return;
654
+ const conversationId = this.conversationIdFromParams(raw) ??
655
+ this.toolConversationIds.get(itemId) ??
656
+ this.activeConversationId;
657
+ if (!conversationId)
658
+ return;
659
+ const output = appendCapped(this.toolOutputBuffers.get(itemId), delta, 6000);
660
+ this.toolOutputBuffers.set(itemId, output);
661
+ const existing = this.findTool(conversationId, itemId);
662
+ this.upsertTool(conversationId, {
663
+ id: itemId,
664
+ name: existing?.name ?? nameFromToolMethod(method),
665
+ input: existing?.input,
666
+ output,
667
+ createdAt: existing?.createdAt ?? Date.now(),
668
+ status: "running",
669
+ });
670
+ }
671
+ handleFilePatchUpdated(params) {
672
+ const raw = asRecord(params);
673
+ if (!raw)
674
+ return;
675
+ const itemId = firstString(raw, ["itemId", "id"]) ?? id("file");
676
+ const conversationId = this.conversationIdFromParams(raw) ??
677
+ this.toolConversationIds.get(itemId) ??
678
+ this.activeConversationId;
679
+ if (!conversationId)
680
+ return;
681
+ const output = summarizeFileChanges(Array.isArray(raw.changes) ? raw.changes : []);
682
+ const existing = this.findTool(conversationId, itemId);
683
+ this.upsertTool(conversationId, {
684
+ id: itemId,
685
+ name: existing?.name ?? "文件修改",
686
+ input: existing?.input,
687
+ output: output || existing?.output,
688
+ createdAt: existing?.createdAt ?? Date.now(),
689
+ status: existing?.status ?? "running",
690
+ });
691
+ }
692
+ handleCommandExecDelta(params) {
693
+ const raw = asRecord(params);
694
+ if (!raw)
695
+ return;
696
+ const processId = firstString(raw, ["processId", "id"]) ?? id("exec");
697
+ const delta = firstString(raw, ["delta", "text"]) ??
698
+ decodeBase64(firstString(raw, ["deltaBase64"]));
699
+ if (!delta)
700
+ return;
701
+ const conversationId = this.conversationIdFromParams(raw) ??
702
+ this.toolConversationIds.get(processId) ??
703
+ this.activeConversationId;
704
+ if (!conversationId)
705
+ return;
706
+ const output = appendCapped(this.toolOutputBuffers.get(processId), delta, 6000);
707
+ this.toolOutputBuffers.set(processId, output);
708
+ const existing = this.findTool(conversationId, processId);
709
+ this.upsertTool(conversationId, {
710
+ id: processId,
711
+ name: existing?.name ?? "命令输出",
712
+ input: existing?.input,
713
+ output,
714
+ createdAt: existing?.createdAt ?? Date.now(),
715
+ status: "running",
716
+ });
717
+ }
718
+ handleCompletedMessageItem(item, streaming) {
719
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
720
+ if (!conversationId)
721
+ return;
722
+ const itemId = firstString(item, ["id"]) ?? id("msg");
723
+ const existing = this.findItem(conversationId, itemId);
724
+ const content = firstString(item, ["text", "content", "message"]) ?? existing?.text;
725
+ if (!content)
726
+ return;
727
+ this.upsertItem(conversationId, {
728
+ id: itemId,
729
+ conversationId,
730
+ type: "message",
731
+ role: "assistant",
732
+ content: [{ type: "text", text: content }],
733
+ text: content,
734
+ createdAt: existing?.createdAt ?? Date.now(),
735
+ updatedAt: Date.now(),
736
+ isStreaming: streaming,
737
+ });
738
+ this.updateConversationPreview(conversationId, content, streaming ? "running" : "idle");
739
+ }
740
+ handleSessionUpdate(params) {
741
+ const raw = asRecord(params) ?? {};
742
+ const nested = asRecord(raw.params) ?? {};
743
+ const text = firstString(raw, ["delta", "text", "content", "message"]) ??
744
+ firstString(nested, ["delta", "text", "content", "message"]);
745
+ if (!text)
746
+ return;
747
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
748
+ if (!conversationId)
749
+ return;
750
+ if (firstString(raw, ["toolName", "tool", "name"])) {
751
+ this.upsertTool(conversationId, {
752
+ id: firstString(raw, ["toolCallId", "callId", "id"]) ?? id("tool"),
753
+ name: firstString(raw, ["toolName", "tool", "name"]) ?? "tool",
754
+ input: stringify(raw.input ?? raw.toolInput ?? ""),
755
+ output: stringify(raw.output ?? raw.result ?? ""),
756
+ createdAt: Date.now(),
757
+ status: raw.status === "completed" || raw.status === "failed" || raw.status === "running"
758
+ ? raw.status
759
+ : "running",
760
+ });
761
+ return;
762
+ }
763
+ const role = raw.role === "user" || raw.role === "system" ? raw.role : "assistant";
764
+ this.upsertItem(conversationId, {
765
+ id: firstString(raw, ["messageId", "id"]) ?? id("msg"),
766
+ conversationId,
767
+ type: "message",
768
+ role,
769
+ content: [{ type: "text", text }],
770
+ text,
771
+ createdAt: Date.now(),
772
+ updatedAt: Date.now(),
773
+ isStreaming: raw.done === false || raw.isStreaming === true,
774
+ });
775
+ this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
776
+ }
777
+ toolCallFromItem(item, fallbackStatus) {
778
+ const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
779
+ if (!itemId)
780
+ return undefined;
781
+ const itemType = firstString(item, ["type"]);
782
+ const output = firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
783
+ stringifyDefined(item.result ?? item.error ?? item.contentItems);
784
+ return {
785
+ id: itemId,
786
+ name: toolNameFromItem(item) ?? itemType ?? "tool",
787
+ input: toolInputFromItem(item),
788
+ output: output ?? this.toolOutputBuffers.get(itemId),
789
+ createdAt: Date.now(),
790
+ status: normalizeToolStatus(item.status, fallbackStatus === "completed"),
791
+ };
792
+ }
793
+ handlePermission(params, waitForResponse, source) {
794
+ const raw = asRecord(params) ?? {};
795
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
796
+ if (!conversationId)
797
+ return waitForResponse ? Promise.resolve({ outcome: { outcome: "cancelled" } }) : undefined;
798
+ const requestId = firstString(raw, ["requestId", "id", "permissionId"]) ?? id("perm");
799
+ const rawToolCall = asRecord(raw.toolCall) ?? raw;
800
+ const permission = {
801
+ requestId,
802
+ toolName: firstString(rawToolCall, ["toolName", "tool", "name", "title", "kind"]),
803
+ toolInput: stringify(rawToolCall.input ?? rawToolCall.toolInput ?? rawToolCall),
804
+ context: firstString(raw, ["context", "description", "message", "title"]),
805
+ options: parsePermissionOptions(raw.options),
806
+ };
807
+ this.pendingPermissions.set(requestId, permission);
808
+ if (source)
809
+ this.permissionSources.set(requestId, source);
810
+ this.updateConversationStatus(conversationId, "waiting_permission");
811
+ const item = {
812
+ id: `permission:${requestId}`,
813
+ conversationId,
814
+ type: "permission",
815
+ permission,
816
+ createdAt: Date.now(),
817
+ updatedAt: Date.now(),
818
+ };
819
+ this.upsertItem(conversationId, item);
820
+ this.input.send(createEnvelope({
821
+ type: "agent.v2.permission.request",
822
+ sessionId: this.input.sessionId,
823
+ payload: { conversationId, ...permission, item },
824
+ }));
825
+ if (!waitForResponse)
826
+ return;
827
+ return new Promise((resolve) => {
828
+ const timer = setTimeout(() => {
829
+ this.pendingPermissions.delete(requestId);
830
+ this.permissionWaiters.delete(requestId);
831
+ this.permissionSources.delete(requestId);
832
+ resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
833
+ this.updateConversationStatus(conversationId, "idle");
834
+ }, PERMISSION_TIMEOUT_MS);
835
+ this.permissionWaiters.set(requestId, { resolve, timer });
836
+ });
837
+ }
838
+ respondPermission(payload) {
839
+ const permission = this.pendingPermissions.get(payload.requestId);
840
+ this.pendingPermissions.delete(payload.requestId);
841
+ const selectedOptionId = payload.optionId ?? selectPermissionOption(permission, payload.outcome);
842
+ const waiter = this.permissionWaiters.get(payload.requestId);
843
+ if (waiter) {
844
+ clearTimeout(waiter.timer);
845
+ this.permissionWaiters.delete(payload.requestId);
846
+ waiter.resolve(formatPermissionResponse(this.permissionSources.get(payload.requestId), payload.outcome, selectedOptionId));
847
+ this.permissionSources.delete(payload.requestId);
848
+ }
849
+ else {
850
+ this.client?.respondPermission({
851
+ sessionId: this.conversations.get(payload.conversationId)?.agentSessionId,
852
+ requestId: payload.requestId,
853
+ outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
854
+ optionId: selectedOptionId,
855
+ });
856
+ }
857
+ this.updateConversationStatus(payload.conversationId, "running");
858
+ }
859
+ addItem(conversationId, item) {
860
+ const timeline = this.timelines.get(conversationId) ?? [];
861
+ timeline.push(item);
862
+ timeline.sort((a, b) => a.createdAt - b.createdAt);
863
+ if (timeline.length > MAX_TIMELINE_ITEMS) {
864
+ timeline.splice(0, timeline.length - MAX_TIMELINE_ITEMS);
865
+ }
866
+ this.timelines.set(conversationId, timeline);
867
+ this.emitItem(conversationId, item);
868
+ }
869
+ upsertItem(conversationId, item) {
870
+ const timeline = this.timelines.get(conversationId) ?? [];
871
+ const index = timeline.findIndex((entry) => entry.id === item.id);
872
+ if (index >= 0)
873
+ timeline[index] = item;
874
+ else
875
+ timeline.push(item);
876
+ timeline.sort((a, b) => a.createdAt - b.createdAt);
877
+ if (timeline.length > MAX_TIMELINE_ITEMS) {
878
+ timeline.splice(0, timeline.length - MAX_TIMELINE_ITEMS);
879
+ }
880
+ this.timelines.set(conversationId, timeline);
881
+ this.emitItem(conversationId, item);
882
+ }
883
+ upsertTool(conversationId, toolCall) {
884
+ const existing = this.findTool(conversationId, toolCall.id);
885
+ const nextToolCall = {
886
+ ...toolCall,
887
+ createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
888
+ };
889
+ this.toolConversationIds.set(nextToolCall.id, conversationId);
890
+ this.upsertItem(conversationId, {
891
+ id: `tool:${nextToolCall.id}`,
892
+ conversationId,
893
+ type: "tool_call",
894
+ toolCall: nextToolCall,
895
+ createdAt: nextToolCall.createdAt ?? Date.now(),
896
+ updatedAt: Date.now(),
897
+ });
898
+ }
899
+ findItem(conversationId, itemId) {
900
+ return this.timelines.get(conversationId)?.find((item) => item.id === itemId);
901
+ }
902
+ findTool(conversationId, toolId) {
903
+ const item = this.timelines.get(conversationId)?.find((entry) => entry.type === "tool_call" && entry.toolCall?.id === toolId);
904
+ return item?.toolCall;
905
+ }
906
+ emitItem(conversationId, item) {
907
+ const conversation = this.conversations.get(conversationId);
908
+ this.input.send(createEnvelope({
909
+ type: "agent.v2.event",
910
+ sessionId: this.input.sessionId,
911
+ payload: { conversationId, conversation, item },
912
+ }));
913
+ }
914
+ emitConversation(conversation) {
915
+ this.input.send(createEnvelope({
916
+ type: "agent.v2.event",
917
+ sessionId: this.input.sessionId,
918
+ payload: { conversationId: conversation.id, conversation },
919
+ }));
920
+ }
921
+ emitStatus(conversationId, status, text) {
922
+ this.addItem(conversationId, {
923
+ id: id("status"),
924
+ conversationId,
925
+ type: "status",
926
+ status,
927
+ text,
928
+ createdAt: Date.now(),
929
+ });
930
+ }
931
+ updateConversationPreview(conversationId, text, status) {
932
+ const conversation = this.conversations.get(conversationId);
933
+ if (!conversation)
934
+ return;
935
+ conversation.lastMessagePreview = previewText(text);
936
+ conversation.lastActivityAt = Date.now();
937
+ if (status)
938
+ conversation.status = status;
939
+ this.emitConversation(conversation);
940
+ }
941
+ updateConversationStatus(conversationId, status, error) {
942
+ const conversation = this.conversations.get(conversationId);
943
+ if (!conversation)
944
+ return;
945
+ conversation.status = status;
946
+ if (error)
947
+ conversation.lastMessagePreview = error;
948
+ conversation.lastActivityAt = Date.now();
949
+ this.emitConversation(conversation);
950
+ }
951
+ sendSnapshot(conversationId) {
952
+ const conversations = [...this.conversations.values()];
953
+ const items = conversationId
954
+ ? this.timelines.get(conversationId) ?? []
955
+ : [...this.timelines.values()].flat();
956
+ this.input.send(createEnvelope({
957
+ type: "agent.v2.snapshot",
958
+ sessionId: this.input.sessionId,
959
+ payload: {
960
+ conversations,
961
+ activeConversationId: this.activeConversationId,
962
+ items,
963
+ },
964
+ }));
965
+ }
966
+ conversationIdFromParams(params) {
967
+ const raw = asRecord(params);
968
+ const agentSessionId = this.extractSessionId(raw);
969
+ if (agentSessionId)
970
+ return this.conversationByAgentSessionId.get(agentSessionId);
971
+ const threadId = firstString(raw, ["threadId", "sessionId", "agentSessionId"]);
972
+ if (threadId)
973
+ return this.conversationByAgentSessionId.get(threadId);
974
+ return undefined;
975
+ }
976
+ handleExit(message) {
977
+ this.cancelPendingPermissions();
978
+ this.status = "error";
979
+ this.error = message;
980
+ this.client = undefined;
981
+ for (const conversation of this.conversations.values()) {
982
+ conversation.status = "error";
983
+ conversation.lastMessagePreview = message;
984
+ conversation.lastActivityAt = Date.now();
985
+ this.emitConversation(conversation);
986
+ this.addItem(conversation.id, {
987
+ id: id("error"),
988
+ conversationId: conversation.id,
989
+ type: "error",
990
+ error: message,
991
+ createdAt: Date.now(),
992
+ });
993
+ }
994
+ }
995
+ cancelPendingPermissions(conversationId) {
996
+ for (const [requestId, waiter] of this.permissionWaiters) {
997
+ clearTimeout(waiter.timer);
998
+ waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
999
+ this.pendingPermissions.delete(requestId);
1000
+ this.permissionSources.delete(requestId);
1001
+ }
1002
+ this.permissionWaiters.clear();
1003
+ if (conversationId)
1004
+ this.updateConversationStatus(conversationId, "idle");
1005
+ }
1006
+ extractSessionId(value) {
1007
+ const raw = asRecord(value);
1008
+ if (!raw)
1009
+ return undefined;
1010
+ const thread = asRecord(raw.thread);
1011
+ if (thread) {
1012
+ const threadId = firstString(thread, ["id", "threadId"]);
1013
+ if (threadId)
1014
+ return threadId;
1015
+ }
1016
+ return firstString(raw, ["sessionId", "id", "agentSessionId", "threadId"]);
1017
+ }
1018
+ extractTurnId(value) {
1019
+ const raw = asRecord(value);
1020
+ if (!raw)
1021
+ return undefined;
1022
+ const turn = asRecord(raw.turn);
1023
+ if (turn) {
1024
+ const turnId = firstString(turn, ["id", "turnId"]);
1025
+ if (turnId)
1026
+ return turnId;
1027
+ }
1028
+ return firstString(raw, ["turnId", "id"]);
1029
+ }
1030
+ }
1031
+ function parsePermissionOptions(value) {
1032
+ if (!Array.isArray(value)) {
1033
+ return [
1034
+ { id: "allow", label: "允许", kind: "allow" },
1035
+ { id: "deny", label: "拒绝", kind: "deny" },
1036
+ ];
1037
+ }
1038
+ const options = value
1039
+ .map((entry, index) => {
1040
+ const raw = asRecord(entry) ?? {};
1041
+ const idValue = raw.optionId ?? raw.id ?? raw.kind ?? `option-${index + 1}`;
1042
+ const labelValue = raw.name ?? raw.label ?? raw.kind ?? String(idValue);
1043
+ const id = String(idValue);
1044
+ const label = String(labelValue);
1045
+ const normalized = `${id} ${label}`.toLowerCase();
1046
+ const kind = normalized.includes("reject") || normalized.includes("deny")
1047
+ ? "deny"
1048
+ : normalized.includes("allow")
1049
+ ? "allow"
1050
+ : "other";
1051
+ return { id, label, kind };
1052
+ })
1053
+ .filter((option) => option.id.length > 0 && option.label.length > 0);
1054
+ return options.length > 0
1055
+ ? options
1056
+ : [
1057
+ { id: "allow", label: "允许", kind: "allow" },
1058
+ { id: "deny", label: "拒绝", kind: "deny" },
1059
+ ];
1060
+ }
1061
+ function selectPermissionOption(permission, outcome) {
1062
+ if (outcome === "cancelled")
1063
+ return "cancelled";
1064
+ const option = permission?.options.find((item) => item.kind === outcome);
1065
+ return option?.id ?? outcome;
1066
+ }
1067
+ function formatPermissionResponse(source, outcome, optionId) {
1068
+ if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
1069
+ return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };
1070
+ }
1071
+ if (source === "item/permissions/requestApproval") {
1072
+ if (outcome === "allow") {
1073
+ return {
1074
+ permissions: { type: "managed", network: { enabled: true }, fileSystem: { type: "fullAccess" } },
1075
+ scope: optionId.includes("session") ? "session" : "turn",
1076
+ };
1077
+ }
1078
+ return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
1079
+ }
1080
+ return {
1081
+ outcome: outcome === "cancelled"
1082
+ ? { outcome: "cancelled" }
1083
+ : { outcome: "selected", optionId },
1084
+ };
1085
+ }
1086
+ //# sourceMappingURL=agent-workspace.js.map