linkshell-cli 0.2.88 → 0.2.89

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.
@@ -41,6 +41,87 @@ interface AgentPermission {
41
41
  options: { id: string; label: string; kind: "allow" | "deny" | "other" }[];
42
42
  }
43
43
 
44
+ type AgentTimelineKind =
45
+ | "chat"
46
+ | "thinking"
47
+ | "tool_activity"
48
+ | "command_execution"
49
+ | "file_change"
50
+ | "subagent_action"
51
+ | "plan"
52
+ | "user_input_prompt"
53
+ | "review"
54
+ | "context_compaction";
55
+
56
+ interface AgentFileChangeEntry {
57
+ path: string;
58
+ kind?: string;
59
+ added?: number;
60
+ removed?: number;
61
+ }
62
+
63
+ interface AgentFileChange {
64
+ entries: AgentFileChangeEntry[];
65
+ diff?: string;
66
+ summary?: string;
67
+ changeSetId?: string;
68
+ status?: AgentToolCall["status"];
69
+ }
70
+
71
+ interface AgentCommandExecution {
72
+ command?: string;
73
+ cwd?: string;
74
+ output?: string;
75
+ exitCode?: number | null;
76
+ status?: AgentToolCall["status"];
77
+ }
78
+
79
+ interface AgentStructuredInputOption {
80
+ id: string;
81
+ label: string;
82
+ description?: string;
83
+ }
84
+
85
+ interface AgentStructuredInputQuestion {
86
+ id: string;
87
+ header?: string;
88
+ question: string;
89
+ isOther?: boolean;
90
+ isSecret?: boolean;
91
+ selectionLimit?: number;
92
+ options?: AgentStructuredInputOption[];
93
+ }
94
+
95
+ interface AgentStructuredInput {
96
+ requestId: string;
97
+ questions: AgentStructuredInputQuestion[];
98
+ }
99
+
100
+ interface AgentSubagentRef {
101
+ threadId: string;
102
+ agentId?: string;
103
+ nickname?: string;
104
+ role?: string;
105
+ model?: string;
106
+ prompt?: string;
107
+ }
108
+
109
+ interface AgentSubagentState {
110
+ threadId: string;
111
+ status: string;
112
+ message?: string;
113
+ }
114
+
115
+ interface AgentSubagentAction {
116
+ tool: string;
117
+ status: string;
118
+ prompt?: string;
119
+ model?: string;
120
+ receiverThreadIds: string[];
121
+ receiverAgents: AgentSubagentRef[];
122
+ agentStates: Record<string, AgentSubagentState>;
123
+ }
124
+
44
125
  interface AgentConversation {
45
126
  id: string;
46
127
  agentSessionId?: string;
@@ -61,10 +142,17 @@ interface AgentTimelineItem {
61
142
  id: string;
62
143
  conversationId: string;
63
144
  type: "message" | "tool_call" | "plan" | "permission" | "status" | "error";
145
+ kind?: AgentTimelineKind;
146
+ turnId?: string;
147
+ itemId?: string;
64
148
  role?: "user" | "assistant" | "system";
65
149
  content?: AgentContentBlock[];
66
150
  text?: string;
67
151
  toolCall?: AgentToolCall;
152
+ commandExecution?: AgentCommandExecution;
153
+ fileChange?: AgentFileChange;
154
+ subagent?: AgentSubagentAction;
155
+ structuredInput?: AgentStructuredInput;
68
156
  plan?: AgentPlanStep[];
69
157
  permission?: AgentPermission;
70
158
  status?: AgentStatus;
@@ -80,6 +168,11 @@ interface PendingPermissionWaiter {
80
168
  timer: ReturnType<typeof setTimeout>;
81
169
  }
82
170
 
171
+ interface PendingStructuredInputWaiter {
172
+ resolve: (value: unknown) => void;
173
+ timer: ReturnType<typeof setTimeout>;
174
+ }
175
+
83
176
  const PERMISSION_TIMEOUT_MS = 5 * 60_000;
84
177
  const MAX_TIMELINE_ITEMS = 200;
85
178
 
@@ -109,6 +202,32 @@ function firstString(value: Record<string, unknown> | undefined, keys: string[])
109
202
  return undefined;
110
203
  }
111
204
 
205
+ function normalizedIdentifier(value: string | undefined): string {
206
+ return (value ?? "").toLowerCase().replace(/[_\-\s/]+/g, "");
207
+ }
208
+
209
+ function firstNumber(value: Record<string, unknown> | undefined, keys: string[]): number | undefined {
210
+ if (!value) return undefined;
211
+ for (const key of keys) {
212
+ const next = value[key];
213
+ if (typeof next === "number" && Number.isFinite(next)) return next;
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ function stringArray(value: unknown): string[] {
219
+ if (!Array.isArray(value)) return [];
220
+ return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
221
+ }
222
+
223
+ function arrayFromKeys(value: Record<string, unknown>, keys: string[]): unknown[] {
224
+ for (const key of keys) {
225
+ const next = value[key];
226
+ if (Array.isArray(next)) return next;
227
+ }
228
+ return [];
229
+ }
230
+
112
231
  function extractItem(value: unknown): Record<string, unknown> | undefined {
113
232
  const raw = asRecord(value);
114
233
  if (!raw) return undefined;
@@ -161,24 +280,29 @@ function nameFromToolMethod(method: string): string {
161
280
  }
162
281
 
163
282
  function isToolItemType(itemType: string | undefined): boolean {
283
+ const normalized = normalizedIdentifier(itemType);
164
284
  return (
165
- itemType === "commandExecution" ||
166
- itemType === "fileChange" ||
167
- itemType === "mcpToolCall" ||
168
- itemType === "dynamicToolCall"
285
+ normalized === "commandexecution" ||
286
+ normalized === "filechange" ||
287
+ normalized === "diff" ||
288
+ normalized === "toolcall" ||
289
+ normalized === "mcptoolcall" ||
290
+ normalized === "dynamictoolcall"
169
291
  );
170
292
  }
171
293
 
172
294
  function toolNameFromItem(item: Record<string, unknown>): string | undefined {
173
295
  const itemType = firstString(item, ["type"]);
174
- if (itemType === "commandExecution") return "命令";
175
- if (itemType === "fileChange") return "文件修改";
176
- if (itemType === "mcpToolCall") {
296
+ const normalized = normalizedIdentifier(itemType);
297
+ if (normalized === "commandexecution") return "命令";
298
+ if (normalized === "filechange" || normalized === "diff") return "文件修改";
299
+ if (normalized === "toolcall") return firstString(item, ["toolName", "tool", "name", "title"]) ?? "工具";
300
+ if (normalized === "mcptoolcall") {
177
301
  const server = firstString(item, ["server"]);
178
302
  const tool = firstString(item, ["tool", "toolName", "name"]);
179
303
  return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
180
304
  }
181
- if (itemType === "dynamicToolCall") {
305
+ if (normalized === "dynamictoolcall") {
182
306
  const namespace = firstString(item, ["namespace"]);
183
307
  const tool = firstString(item, ["tool", "toolName", "name"]);
184
308
  return [namespace, tool].filter(Boolean).join(" · ") || "工具";
@@ -201,6 +325,95 @@ function summarizeFileChanges(changes: unknown[]): string | undefined {
201
325
  return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
202
326
  }
203
327
 
328
+ function fileChangeEntriesFromItem(item: Record<string, unknown>): AgentFileChangeEntry[] {
329
+ const changes = Array.isArray(item.changes) ? item.changes : [];
330
+ const entries: AgentFileChangeEntry[] = [];
331
+ for (const change of changes) {
332
+ const raw = asRecord(change);
333
+ if (!raw) continue;
334
+ const path =
335
+ firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
336
+ firstString(asRecord(raw.update), ["path", "file", "filePath"]);
337
+ if (!path) continue;
338
+ const totals = asRecord(raw.totals) ?? asRecord(raw.diffStats) ?? asRecord(raw.stats);
339
+ const entry: AgentFileChangeEntry = { path };
340
+ const kind = firstString(raw, ["kind", "type", "operation", "action"]);
341
+ const added = firstNumber(raw, ["added", "additions"]) ?? firstNumber(totals, ["added", "additions"]);
342
+ const removed = firstNumber(raw, ["removed", "deletions"]) ?? firstNumber(totals, ["removed", "deletions"]);
343
+ if (kind) entry.kind = kind;
344
+ if (added !== undefined) entry.added = added;
345
+ if (removed !== undefined) entry.removed = removed;
346
+ entries.push(entry);
347
+ }
348
+ const directPath = firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
349
+ if (entries.length === 0 && directPath) {
350
+ const entry: AgentFileChangeEntry = { path: directPath };
351
+ const kind = firstString(item, ["kind", "type", "operation", "action"]);
352
+ if (kind) entry.kind = kind;
353
+ return [entry];
354
+ }
355
+ return entries;
356
+ }
357
+
358
+ function commandExecutionFromItem(
359
+ item: Record<string, unknown>,
360
+ status: AgentToolCall["status"],
361
+ output?: string,
362
+ ): AgentCommandExecution | undefined {
363
+ const command = firstString(item, ["command"]);
364
+ const cwd = firstString(item, ["cwd"]);
365
+ const exitCode = firstNumber(item, ["exitCode", "code"]);
366
+ if (!command && !cwd && !output && exitCode === undefined) return undefined;
367
+ return { command, cwd, output, exitCode: exitCode ?? undefined, status };
368
+ }
369
+
370
+ function fileChangeFromItem(
371
+ item: Record<string, unknown>,
372
+ status: AgentToolCall["status"],
373
+ diff?: string,
374
+ ): AgentFileChange | undefined {
375
+ const entries = fileChangeEntriesFromItem(item);
376
+ const summary = summarizeFileChanges(Array.isArray(item.changes) ? item.changes : []);
377
+ const changeSetId = firstString(item, ["changeSetId", "changesetId", "patchId"]);
378
+ if (entries.length === 0 && !diff && !summary && !changeSetId) return undefined;
379
+ return { entries, diff, summary, changeSetId, status };
380
+ }
381
+
382
+ function commandExecutionFromTool(toolCall: AgentToolCall): AgentCommandExecution | undefined {
383
+ const input = toolCall.input?.trim();
384
+ if (!input && !toolCall.output) return undefined;
385
+ const [commandPart, cwdPart] = input?.split(/\n\ncwd:\s*/i) ?? [];
386
+ return {
387
+ command: commandPart || input,
388
+ cwd: cwdPart,
389
+ output: toolCall.output,
390
+ status: toolCall.status,
391
+ };
392
+ }
393
+
394
+ function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefined {
395
+ const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
396
+ const entries: AgentFileChangeEntry[] = (toolCall.input ?? "")
397
+ .split("\n")
398
+ .map((line) => line.trim())
399
+ .filter(Boolean)
400
+ .map((line) => {
401
+ const [kind, ...rest] = line.split(/\s+/);
402
+ const path = rest.length > 0 ? rest.join(" ") : kind;
403
+ const entry: AgentFileChangeEntry = { path: path ?? line };
404
+ if (rest.length > 0 && kind) entry.kind = kind;
405
+ return entry;
406
+ })
407
+ .filter((entry) => entry.path.length > 0);
408
+ if (entries.length === 0 && !diff && !toolCall.output) return undefined;
409
+ return {
410
+ entries,
411
+ diff,
412
+ summary: diff ? undefined : toolCall.output,
413
+ status: toolCall.status,
414
+ };
415
+ }
416
+
204
417
  function looksLikeDiff(text: string): boolean {
205
418
  const value = text.trim();
206
419
  return (
@@ -247,13 +460,14 @@ function extractDiffText(value: unknown): string | undefined {
247
460
 
248
461
  function toolInputFromItem(item: Record<string, unknown>): string | undefined {
249
462
  const itemType = firstString(item, ["type"]);
250
- if (itemType === "commandExecution") {
463
+ const normalized = normalizedIdentifier(itemType);
464
+ if (normalized === "commandexecution") {
251
465
  const command = firstString(item, ["command"]);
252
466
  const cwd = firstString(item, ["cwd"]);
253
467
  if (command && cwd) return `${command}\n\ncwd: ${cwd}`;
254
468
  return command ?? cwd;
255
469
  }
256
- if (itemType === "fileChange") {
470
+ if (normalized === "filechange" || normalized === "diff") {
257
471
  const changes = Array.isArray(item.changes) ? item.changes : [];
258
472
  return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
259
473
  }
@@ -271,6 +485,167 @@ function textFromBlocks(blocks: AgentContentBlock[]): string {
271
485
  .join("\n");
272
486
  }
273
487
 
488
+ function isSubagentItemType(itemType: string | undefined): boolean {
489
+ const normalized = normalizedIdentifier(itemType);
490
+ return (
491
+ normalized === "collabagenttoolcall" ||
492
+ normalized === "collabtoolcall" ||
493
+ normalized.startsWith("collabagentspawn") ||
494
+ normalized.startsWith("collabwaiting") ||
495
+ normalized.startsWith("collabclose") ||
496
+ normalized.startsWith("collabresume") ||
497
+ normalized.startsWith("collabagentinteraction")
498
+ );
499
+ }
500
+
501
+ function parseSubagentRef(value: unknown): AgentSubagentRef | undefined {
502
+ const raw = asRecord(value);
503
+ if (!raw) return undefined;
504
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
505
+ if (!threadId) return undefined;
506
+ return {
507
+ threadId,
508
+ agentId: firstString(raw, ["agentId", "agentID"]),
509
+ nickname: firstString(raw, ["nickname", "name", "label"]),
510
+ role: firstString(raw, ["role", "kind"]),
511
+ model: firstString(raw, ["model", "modelName"]),
512
+ prompt: firstString(raw, ["prompt", "instructions", "message"]),
513
+ };
514
+ }
515
+
516
+ function parseSubagentStates(value: unknown): Record<string, AgentSubagentState> {
517
+ const result: Record<string, AgentSubagentState> = {};
518
+ if (Array.isArray(value)) {
519
+ for (const entry of value) {
520
+ const raw = asRecord(entry);
521
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
522
+ const status = firstString(raw, ["status", "state", "phase"]);
523
+ if (!threadId || !status) continue;
524
+ result[threadId] = {
525
+ threadId,
526
+ status,
527
+ message: firstString(raw, ["message", "summary", "text"]),
528
+ };
529
+ }
530
+ return result;
531
+ }
532
+ const raw = asRecord(value);
533
+ if (!raw) return result;
534
+ for (const [threadId, entry] of Object.entries(raw)) {
535
+ const state = asRecord(entry);
536
+ if (state) {
537
+ result[threadId] = {
538
+ threadId,
539
+ status: firstString(state, ["status", "state", "phase"]) ?? "running",
540
+ message: firstString(state, ["message", "summary", "text"]),
541
+ };
542
+ } else if (typeof entry === "string") {
543
+ result[threadId] = { threadId, status: entry };
544
+ }
545
+ }
546
+ return result;
547
+ }
548
+
549
+ function parseStructuredInputOption(value: unknown, index: number): AgentStructuredInputOption | undefined {
550
+ const raw = asRecord(value);
551
+ if (!raw) {
552
+ if (typeof value === "string" && value.trim()) {
553
+ return { id: `option-${index + 1}`, label: value.trim() };
554
+ }
555
+ return undefined;
556
+ }
557
+ const label = firstString(raw, ["label", "title", "text", "value"]);
558
+ if (!label) return undefined;
559
+ return {
560
+ id: firstString(raw, ["id", "optionId", "value"]) ?? `option-${index + 1}`,
561
+ label,
562
+ description: firstString(raw, ["description", "detail", "subtitle"]),
563
+ };
564
+ }
565
+
566
+ function parseStructuredInputQuestion(value: unknown, index: number): AgentStructuredInputQuestion | undefined {
567
+ const raw = asRecord(value);
568
+ if (!raw) return undefined;
569
+ const question = firstString(raw, ["question", "prompt", "text", "message", "label"]);
570
+ if (!question) return undefined;
571
+ const options = arrayFromKeys(raw, ["options", "choices", "items"])
572
+ .map(parseStructuredInputOption)
573
+ .filter((option): option is AgentStructuredInputOption => Boolean(option));
574
+ return {
575
+ id: firstString(raw, ["id", "questionId", "key"]) ?? `question-${index + 1}`,
576
+ header: firstString(raw, ["header", "title"]),
577
+ question,
578
+ isOther: raw.isOther === true,
579
+ isSecret: raw.isSecret === true || raw.secret === true,
580
+ selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
581
+ options: options.length > 0 ? options : undefined,
582
+ };
583
+ }
584
+
585
+ function decodeStructuredInput(value: unknown): AgentStructuredInput | undefined {
586
+ const raw = asRecord(value) ?? {};
587
+ const questions = arrayFromKeys(raw, ["questions", "items", "prompts"])
588
+ .map(parseStructuredInputQuestion)
589
+ .filter((question): question is AgentStructuredInputQuestion => Boolean(question));
590
+ if (questions.length === 0) {
591
+ const single = parseStructuredInputQuestion(raw, 0);
592
+ if (single) questions.push(single);
593
+ }
594
+ if (questions.length === 0) return undefined;
595
+ return {
596
+ requestId: firstString(raw, ["requestId", "id", "inputId"]) ?? id("input"),
597
+ questions,
598
+ };
599
+ }
600
+
601
+ function decodeSubagentAction(
602
+ item: Record<string, unknown>,
603
+ status: "running" | "completed" | "failed" | "pending",
604
+ ): AgentSubagentAction | undefined {
605
+ const nested = asRecord(item.action) ?? asRecord(item.toolCall) ?? asRecord(item.call) ?? {};
606
+ const receiverAgents = [
607
+ ...arrayFromKeys(item, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
608
+ ...arrayFromKeys(nested, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
609
+ ].filter((entry): entry is AgentSubagentRef => Boolean(entry));
610
+ const receiverThreadIds = [
611
+ ...stringArray(item.receiverThreadIds),
612
+ ...stringArray(item.threadIds),
613
+ ...stringArray(item.childThreadIds),
614
+ ...stringArray(item.agentThreadIds),
615
+ ...stringArray(nested.receiverThreadIds),
616
+ ...stringArray(nested.threadIds),
617
+ ...receiverAgents.map((agent) => agent.threadId),
618
+ ].filter((threadId, index, array) => array.indexOf(threadId) === index);
619
+ const agentStates = {
620
+ ...parseSubagentStates(item.agentStates ?? item.states ?? item.statusByThread),
621
+ ...parseSubagentStates(nested.agentStates ?? nested.states ?? nested.statusByThread),
622
+ };
623
+ if (receiverThreadIds.length === 0 && Object.keys(agentStates).length === 0) return undefined;
624
+ return {
625
+ tool: firstString(item, ["tool", "toolName", "name", "type"]) ??
626
+ firstString(nested, ["tool", "toolName", "name", "type"]) ??
627
+ "subagent",
628
+ status,
629
+ prompt: firstString(item, ["prompt", "instructions", "message"]) ??
630
+ firstString(nested, ["prompt", "instructions", "message"]),
631
+ model: firstString(item, ["model", "modelName"]) ?? firstString(nested, ["model", "modelName"]),
632
+ receiverThreadIds,
633
+ receiverAgents,
634
+ agentStates,
635
+ };
636
+ }
637
+
638
+ function summarizeSubagentAction(action: AgentSubagentAction): string {
639
+ const count = Math.max(1, action.receiverThreadIds.length, action.receiverAgents.length);
640
+ const normalized = normalizedIdentifier(action.tool);
641
+ if (normalized.includes("spawn")) return `启动 ${count} 个子 Agent`;
642
+ if (normalized.includes("wait")) return `等待 ${count} 个子 Agent`;
643
+ if (normalized.includes("resume")) return `恢复 ${count} 个子 Agent`;
644
+ if (normalized.includes("close")) return `关闭 ${count} 个子 Agent`;
645
+ if (normalized.includes("sendinput")) return `更新 ${count} 个子 Agent`;
646
+ return count === 1 ? "子 Agent 活动" : `${count} 个子 Agent 活动`;
647
+ }
648
+
274
649
  function previewText(text: string): string {
275
650
  return text.replace(/\s+/g, " ").trim().slice(0, 160);
276
651
  }
@@ -308,6 +683,8 @@ export class AgentWorkspaceProxy {
308
683
  private pendingPermissions = new Map<string, AgentPermission>();
309
684
  private permissionWaiters = new Map<string, PendingPermissionWaiter>();
310
685
  private permissionSources = new Map<string, string>();
686
+ private pendingStructuredInputs = new Map<string, { conversationId: string; input: AgentStructuredInput }>();
687
+ private structuredInputWaiters = new Map<string, PendingStructuredInputWaiter>();
311
688
  private toolConversationIds = new Map<string, string>();
312
689
  private agentProtocol: AgentProtocol | undefined;
313
690
 
@@ -373,6 +750,11 @@ export class AgentWorkspaceProxy {
373
750
  this.respondPermission(payload);
374
751
  break;
375
752
  }
753
+ case "agent.v2.structured_input.respond": {
754
+ const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
755
+ this.respondStructuredInput(payload);
756
+ break;
757
+ }
376
758
  }
377
759
  }
378
760
 
@@ -670,6 +1052,9 @@ export class AgentWorkspaceProxy {
670
1052
  }
671
1053
 
672
1054
  private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
1055
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1056
+ return this.handleStructuredInput(params, true);
1057
+ }
673
1058
  if (isPermissionRequestMethod(method)) {
674
1059
  return this.handlePermission(params, true, method);
675
1060
  }
@@ -696,6 +1081,10 @@ export class AgentWorkspaceProxy {
696
1081
  }
697
1082
 
698
1083
  const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
1084
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1085
+ this.handleStructuredInput(params);
1086
+ return;
1087
+ }
699
1088
  if (isPermissionRequestMethod(method)) {
700
1089
  this.handlePermission(params, false, method);
701
1090
  return;
@@ -845,14 +1234,20 @@ export class AgentWorkspaceProxy {
845
1234
  const item = extractItem(params);
846
1235
  if (!item) return;
847
1236
  const itemType = firstString(item, ["type"]);
848
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1237
+ const normalizedItemType = normalizedIdentifier(itemType);
1238
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
849
1239
  this.handleCompletedMessageItem(item, true);
850
1240
  return;
851
1241
  }
852
- if (itemType === "plan") {
1242
+ if (normalizedItemType === "plan") {
853
1243
  this.handlePlanUpdated({ plan: [item] });
854
1244
  return;
855
1245
  }
1246
+ if (isSubagentItemType(itemType)) {
1247
+ this.handleSubagentItem(item, "running", true);
1248
+ return;
1249
+ }
1250
+ if (this.handleSemanticSystemItem(item, "running", true)) return;
856
1251
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
857
1252
  const toolCall = this.toolCallFromItem(item, "running");
858
1253
  if (!conversationId || !toolCall) return;
@@ -864,10 +1259,20 @@ export class AgentWorkspaceProxy {
864
1259
  const item = extractItem(params);
865
1260
  if (!item) return;
866
1261
  const itemType = firstString(item, ["type"]);
867
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1262
+ const normalizedItemType = normalizedIdentifier(itemType);
1263
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
868
1264
  this.handleCompletedMessageItem(item, false);
869
1265
  return;
870
1266
  }
1267
+ if (normalizedItemType === "plan") {
1268
+ this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
1269
+ return;
1270
+ }
1271
+ if (isSubagentItemType(itemType)) {
1272
+ this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
1273
+ return;
1274
+ }
1275
+ if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false)) return;
871
1276
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
872
1277
  const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
873
1278
  if (!conversationId || !toolCall) return;
@@ -1029,6 +1434,127 @@ export class AgentWorkspaceProxy {
1029
1434
  this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
1030
1435
  }
1031
1436
 
1437
+ private handleSemanticSystemItem(
1438
+ item: Record<string, unknown>,
1439
+ status: AgentToolCall["status"],
1440
+ streaming: boolean,
1441
+ ): boolean {
1442
+ const itemType = firstString(item, ["type"]);
1443
+ const normalized = normalizedIdentifier(itemType);
1444
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1445
+ if (!conversationId) return false;
1446
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
1447
+ const existing = this.findItem(conversationId, itemId);
1448
+ const base = {
1449
+ id: itemId,
1450
+ conversationId,
1451
+ type: "status" as const,
1452
+ role: "system" as const,
1453
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1454
+ itemId,
1455
+ createdAt: existing?.createdAt ?? Date.now(),
1456
+ updatedAt: Date.now(),
1457
+ isStreaming: streaming,
1458
+ };
1459
+
1460
+ if (normalized === "reasoning" || normalized === "thinking") {
1461
+ const text = firstString(item, ["text", "content", "summary", "message"]) ??
1462
+ stringifyDefined(item.contentItems ?? item.summary);
1463
+ this.upsertItem(conversationId, {
1464
+ ...base,
1465
+ kind: "thinking",
1466
+ text: text ?? (streaming ? "正在思考" : "完成思考"),
1467
+ });
1468
+ return true;
1469
+ }
1470
+
1471
+ if (normalized === "enteredreviewmode") {
1472
+ const target = firstString(item, ["review", "target", "label"]) ?? "changes";
1473
+ this.upsertItem(conversationId, {
1474
+ ...base,
1475
+ kind: "review",
1476
+ text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
1477
+ });
1478
+ return true;
1479
+ }
1480
+
1481
+ if (normalized === "contextcompaction") {
1482
+ this.upsertItem(conversationId, {
1483
+ ...base,
1484
+ kind: "context_compaction",
1485
+ text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
1486
+ });
1487
+ return true;
1488
+ }
1489
+
1490
+ return false;
1491
+ }
1492
+
1493
+ private handleSubagentItem(
1494
+ item: Record<string, unknown>,
1495
+ status: AgentToolCall["status"],
1496
+ streaming: boolean,
1497
+ ): void {
1498
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1499
+ if (!conversationId) return;
1500
+ const subagent = decodeSubagentAction(item, status);
1501
+ if (!subagent) return;
1502
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
1503
+ const text = summarizeSubagentAction(subagent);
1504
+ const existing = this.findItem(conversationId, itemId);
1505
+ this.upsertItem(conversationId, {
1506
+ id: itemId,
1507
+ conversationId,
1508
+ type: "status",
1509
+ kind: "subagent_action",
1510
+ role: "system",
1511
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1512
+ itemId,
1513
+ text,
1514
+ subagent,
1515
+ createdAt: existing?.createdAt ?? Date.now(),
1516
+ updatedAt: Date.now(),
1517
+ isStreaming: streaming,
1518
+ });
1519
+ this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
1520
+ }
1521
+
1522
+ private handleStructuredInput(params: unknown, waitForResponse = false): Promise<unknown> | void {
1523
+ const raw = asRecord(params) ?? {};
1524
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1525
+ if (!conversationId) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1526
+ const structuredInput = decodeStructuredInput(raw);
1527
+ if (!structuredInput) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1528
+ const text = structuredInput.questions.map((question) => question.question).join("\n");
1529
+ this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
1530
+ this.upsertItem(conversationId, {
1531
+ id: `input:${structuredInput.requestId}`,
1532
+ conversationId,
1533
+ type: "status",
1534
+ kind: "user_input_prompt",
1535
+ role: "system",
1536
+ text,
1537
+ structuredInput,
1538
+ metadata: { inputPending: true },
1539
+ createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
1540
+ updatedAt: Date.now(),
1541
+ });
1542
+ this.updateConversationPreview(conversationId, "需要用户输入", "running");
1543
+ if (!waitForResponse) return;
1544
+ return new Promise((resolve) => {
1545
+ const timer = setTimeout(() => {
1546
+ this.pendingStructuredInputs.delete(structuredInput.requestId);
1547
+ this.structuredInputWaiters.delete(structuredInput.requestId);
1548
+ resolve(formatStructuredInputResponse({}));
1549
+ this.markStructuredInput(conversationId, structuredInput.requestId, {
1550
+ inputPending: false,
1551
+ inputError: "等待用户输入超时",
1552
+ });
1553
+ }, PERMISSION_TIMEOUT_MS);
1554
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
1555
+ });
1556
+ }
1557
+
1032
1558
  private toolCallFromItem(
1033
1559
  item: Record<string, unknown>,
1034
1560
  fallbackStatus: AgentToolCall["status"],
@@ -1036,13 +1562,14 @@ export class AgentWorkspaceProxy {
1036
1562
  const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
1037
1563
  if (!itemId) return undefined;
1038
1564
  const itemType = firstString(item, ["type"]);
1565
+ const normalizedItemType = normalizedIdentifier(itemType);
1039
1566
  const name = toolNameFromItem(item);
1040
1567
  if (!name && !isToolItemType(itemType)) return undefined;
1041
1568
  const bufferedOutput = this.toolOutputBuffers.get(itemId);
1042
1569
  const rawOutput =
1043
1570
  firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
1044
1571
  stringifyDefined(item.result ?? item.error ?? item.contentItems);
1045
- const output = itemType === "fileChange"
1572
+ const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
1046
1573
  ? extractDiffText(item) ?? bufferedOutput ?? rawOutput
1047
1574
  : rawOutput ?? bufferedOutput;
1048
1575
  return {
@@ -1134,6 +1661,41 @@ export class AgentWorkspaceProxy {
1134
1661
  this.updateConversationStatus(payload.conversationId, "running");
1135
1662
  }
1136
1663
 
1664
+ private respondStructuredInput(payload: {
1665
+ conversationId: string;
1666
+ requestId: string;
1667
+ answers: Record<string, string[]>;
1668
+ }): void {
1669
+ const pending = this.pendingStructuredInputs.get(payload.requestId);
1670
+ this.pendingStructuredInputs.delete(payload.requestId);
1671
+ const waiter = this.structuredInputWaiters.get(payload.requestId);
1672
+ if (waiter) {
1673
+ clearTimeout(waiter.timer);
1674
+ this.structuredInputWaiters.delete(payload.requestId);
1675
+ waiter.resolve(formatStructuredInputResponse(payload.answers));
1676
+ }
1677
+ this.markStructuredInput(payload.conversationId, payload.requestId, {
1678
+ inputPending: false,
1679
+ inputSubmitted: true,
1680
+ answers: payload.answers,
1681
+ });
1682
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
1683
+ }
1684
+
1685
+ private markStructuredInput(
1686
+ conversationId: string,
1687
+ requestId: string,
1688
+ metadata: Record<string, unknown>,
1689
+ ): void {
1690
+ const item = this.findItem(conversationId, `input:${requestId}`);
1691
+ if (!item) return;
1692
+ this.upsertItem(conversationId, {
1693
+ ...item,
1694
+ metadata: { ...(item.metadata ?? {}), ...metadata },
1695
+ updatedAt: Date.now(),
1696
+ });
1697
+ }
1698
+
1137
1699
  private addItem(conversationId: string, item: AgentTimelineItem): void {
1138
1700
  const timeline = this.timelines.get(conversationId) ?? [];
1139
1701
  timeline.push(item);
@@ -1173,11 +1735,20 @@ export class AgentWorkspaceProxy {
1173
1735
  };
1174
1736
  this.toolConversationIds.set(toolCall.id, conversationId);
1175
1737
  this.toolConversationIds.set(nextToolCall.id, conversationId);
1738
+ const kind: AgentTimelineKind = nextToolCall.name.includes("文件")
1739
+ ? "file_change"
1740
+ : nextToolCall.name.includes("命令")
1741
+ ? "command_execution"
1742
+ : "tool_activity";
1176
1743
  this.upsertItem(conversationId, {
1177
1744
  id: `tool:${nextToolCall.id}`,
1178
1745
  conversationId,
1179
1746
  type: "tool_call",
1747
+ kind,
1748
+ itemId: nextToolCall.id,
1180
1749
  toolCall: nextToolCall,
1750
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
1751
+ fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
1181
1752
  createdAt: nextToolCall.createdAt ?? Date.now(),
1182
1753
  updatedAt: Date.now(),
1183
1754
  });
@@ -1380,6 +1951,19 @@ export class AgentWorkspaceProxy {
1380
1951
  this.permissionSources.delete(requestId);
1381
1952
  }
1382
1953
  this.permissionWaiters.clear();
1954
+ for (const [requestId, waiter] of this.structuredInputWaiters) {
1955
+ clearTimeout(waiter.timer);
1956
+ waiter.resolve(formatStructuredInputResponse({}));
1957
+ const pending = this.pendingStructuredInputs.get(requestId);
1958
+ if (pending) {
1959
+ this.markStructuredInput(pending.conversationId, requestId, {
1960
+ inputPending: false,
1961
+ inputError: "已停止",
1962
+ });
1963
+ }
1964
+ this.pendingStructuredInputs.delete(requestId);
1965
+ }
1966
+ this.structuredInputWaiters.clear();
1383
1967
  if (conversationId) this.updateConversationStatus(conversationId, "idle");
1384
1968
  }
1385
1969
 
@@ -1452,11 +2036,21 @@ function isPermissionRequestMethod(method: string): boolean {
1452
2036
  return (
1453
2037
  method === "session/request_permission" ||
1454
2038
  method.endsWith("/requestApproval") ||
1455
- method === "mcpServer/elicitation/request" ||
1456
- method === "item/tool/requestUserInput"
2039
+ method === "mcpServer/elicitation/request"
1457
2040
  );
1458
2041
  }
1459
2042
 
2043
+ function formatStructuredInputResponse(answers: Record<string, string[]>): unknown {
2044
+ return {
2045
+ answers: Object.fromEntries(
2046
+ Object.entries(answers).map(([questionId, values]) => [
2047
+ questionId,
2048
+ { answers: values.map((value) => value.trim()).filter(Boolean) },
2049
+ ]),
2050
+ ),
2051
+ };
2052
+ }
2053
+
1460
2054
  function formatPermissionResponse(
1461
2055
  source: string | undefined,
1462
2056
  outcome: "allow" | "deny" | "cancelled",