linkshell-cli 0.2.87 → 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.
@@ -30,6 +30,32 @@ function firstString(value, keys) {
30
30
  }
31
31
  return undefined;
32
32
  }
33
+ function normalizedIdentifier(value) {
34
+ return (value ?? "").toLowerCase().replace(/[_\-\s/]+/g, "");
35
+ }
36
+ function firstNumber(value, keys) {
37
+ if (!value)
38
+ return undefined;
39
+ for (const key of keys) {
40
+ const next = value[key];
41
+ if (typeof next === "number" && Number.isFinite(next))
42
+ return next;
43
+ }
44
+ return undefined;
45
+ }
46
+ function stringArray(value) {
47
+ if (!Array.isArray(value))
48
+ return [];
49
+ return value.filter((entry) => typeof entry === "string" && entry.length > 0);
50
+ }
51
+ function arrayFromKeys(value, keys) {
52
+ for (const key of keys) {
53
+ const next = value[key];
54
+ if (Array.isArray(next))
55
+ return next;
56
+ }
57
+ return [];
58
+ }
33
59
  function extractItem(value) {
34
60
  const raw = asRecord(value);
35
61
  if (!raw)
@@ -87,23 +113,29 @@ function nameFromToolMethod(method) {
87
113
  return "工具";
88
114
  }
89
115
  function isToolItemType(itemType) {
90
- return (itemType === "commandExecution" ||
91
- itemType === "fileChange" ||
92
- itemType === "mcpToolCall" ||
93
- itemType === "dynamicToolCall");
116
+ const normalized = normalizedIdentifier(itemType);
117
+ return (normalized === "commandexecution" ||
118
+ normalized === "filechange" ||
119
+ normalized === "diff" ||
120
+ normalized === "toolcall" ||
121
+ normalized === "mcptoolcall" ||
122
+ normalized === "dynamictoolcall");
94
123
  }
95
124
  function toolNameFromItem(item) {
96
125
  const itemType = firstString(item, ["type"]);
97
- if (itemType === "commandExecution")
126
+ const normalized = normalizedIdentifier(itemType);
127
+ if (normalized === "commandexecution")
98
128
  return "命令";
99
- if (itemType === "fileChange")
129
+ if (normalized === "filechange" || normalized === "diff")
100
130
  return "文件修改";
101
- if (itemType === "mcpToolCall") {
131
+ if (normalized === "toolcall")
132
+ return firstString(item, ["toolName", "tool", "name", "title"]) ?? "工具";
133
+ if (normalized === "mcptoolcall") {
102
134
  const server = firstString(item, ["server"]);
103
135
  const tool = firstString(item, ["tool", "toolName", "name"]);
104
136
  return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
105
137
  }
106
- if (itemType === "dynamicToolCall") {
138
+ if (normalized === "dynamictoolcall") {
107
139
  const namespace = firstString(item, ["namespace"]);
108
140
  const tool = firstString(item, ["tool", "toolName", "name"]);
109
141
  return [namespace, tool].filter(Boolean).join(" · ") || "工具";
@@ -124,6 +156,92 @@ function summarizeFileChanges(changes) {
124
156
  .filter((line) => Boolean(line));
125
157
  return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
126
158
  }
159
+ function fileChangeEntriesFromItem(item) {
160
+ const changes = Array.isArray(item.changes) ? item.changes : [];
161
+ const entries = [];
162
+ for (const change of changes) {
163
+ const raw = asRecord(change);
164
+ if (!raw)
165
+ continue;
166
+ const path = firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
167
+ firstString(asRecord(raw.update), ["path", "file", "filePath"]);
168
+ if (!path)
169
+ continue;
170
+ const totals = asRecord(raw.totals) ?? asRecord(raw.diffStats) ?? asRecord(raw.stats);
171
+ const entry = { path };
172
+ const kind = firstString(raw, ["kind", "type", "operation", "action"]);
173
+ const added = firstNumber(raw, ["added", "additions"]) ?? firstNumber(totals, ["added", "additions"]);
174
+ const removed = firstNumber(raw, ["removed", "deletions"]) ?? firstNumber(totals, ["removed", "deletions"]);
175
+ if (kind)
176
+ entry.kind = kind;
177
+ if (added !== undefined)
178
+ entry.added = added;
179
+ if (removed !== undefined)
180
+ entry.removed = removed;
181
+ entries.push(entry);
182
+ }
183
+ const directPath = firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
184
+ if (entries.length === 0 && directPath) {
185
+ const entry = { path: directPath };
186
+ const kind = firstString(item, ["kind", "type", "operation", "action"]);
187
+ if (kind)
188
+ entry.kind = kind;
189
+ return [entry];
190
+ }
191
+ return entries;
192
+ }
193
+ function commandExecutionFromItem(item, status, output) {
194
+ const command = firstString(item, ["command"]);
195
+ const cwd = firstString(item, ["cwd"]);
196
+ const exitCode = firstNumber(item, ["exitCode", "code"]);
197
+ if (!command && !cwd && !output && exitCode === undefined)
198
+ return undefined;
199
+ return { command, cwd, output, exitCode: exitCode ?? undefined, status };
200
+ }
201
+ function fileChangeFromItem(item, status, diff) {
202
+ const entries = fileChangeEntriesFromItem(item);
203
+ const summary = summarizeFileChanges(Array.isArray(item.changes) ? item.changes : []);
204
+ const changeSetId = firstString(item, ["changeSetId", "changesetId", "patchId"]);
205
+ if (entries.length === 0 && !diff && !summary && !changeSetId)
206
+ return undefined;
207
+ return { entries, diff, summary, changeSetId, status };
208
+ }
209
+ function commandExecutionFromTool(toolCall) {
210
+ const input = toolCall.input?.trim();
211
+ if (!input && !toolCall.output)
212
+ return undefined;
213
+ const [commandPart, cwdPart] = input?.split(/\n\ncwd:\s*/i) ?? [];
214
+ return {
215
+ command: commandPart || input,
216
+ cwd: cwdPart,
217
+ output: toolCall.output,
218
+ status: toolCall.status,
219
+ };
220
+ }
221
+ function fileChangeFromTool(toolCall) {
222
+ const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
223
+ const entries = (toolCall.input ?? "")
224
+ .split("\n")
225
+ .map((line) => line.trim())
226
+ .filter(Boolean)
227
+ .map((line) => {
228
+ const [kind, ...rest] = line.split(/\s+/);
229
+ const path = rest.length > 0 ? rest.join(" ") : kind;
230
+ const entry = { path: path ?? line };
231
+ if (rest.length > 0 && kind)
232
+ entry.kind = kind;
233
+ return entry;
234
+ })
235
+ .filter((entry) => entry.path.length > 0);
236
+ if (entries.length === 0 && !diff && !toolCall.output)
237
+ return undefined;
238
+ return {
239
+ entries,
240
+ diff,
241
+ summary: diff ? undefined : toolCall.output,
242
+ status: toolCall.status,
243
+ };
244
+ }
127
245
  function looksLikeDiff(text) {
128
246
  const value = text.trim();
129
247
  return (value.startsWith("diff --git ") ||
@@ -170,14 +288,15 @@ function extractDiffText(value) {
170
288
  }
171
289
  function toolInputFromItem(item) {
172
290
  const itemType = firstString(item, ["type"]);
173
- if (itemType === "commandExecution") {
291
+ const normalized = normalizedIdentifier(itemType);
292
+ if (normalized === "commandexecution") {
174
293
  const command = firstString(item, ["command"]);
175
294
  const cwd = firstString(item, ["cwd"]);
176
295
  if (command && cwd)
177
296
  return `${command}\n\ncwd: ${cwd}`;
178
297
  return command ?? cwd;
179
298
  }
180
- if (itemType === "fileChange") {
299
+ if (normalized === "filechange" || normalized === "diff") {
181
300
  const changes = Array.isArray(item.changes) ? item.changes : [];
182
301
  return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
183
302
  }
@@ -192,6 +311,170 @@ function textFromBlocks(blocks) {
192
311
  .filter(Boolean)
193
312
  .join("\n");
194
313
  }
314
+ function isSubagentItemType(itemType) {
315
+ const normalized = normalizedIdentifier(itemType);
316
+ return (normalized === "collabagenttoolcall" ||
317
+ normalized === "collabtoolcall" ||
318
+ normalized.startsWith("collabagentspawn") ||
319
+ normalized.startsWith("collabwaiting") ||
320
+ normalized.startsWith("collabclose") ||
321
+ normalized.startsWith("collabresume") ||
322
+ normalized.startsWith("collabagentinteraction"));
323
+ }
324
+ function parseSubagentRef(value) {
325
+ const raw = asRecord(value);
326
+ if (!raw)
327
+ return undefined;
328
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
329
+ if (!threadId)
330
+ return undefined;
331
+ return {
332
+ threadId,
333
+ agentId: firstString(raw, ["agentId", "agentID"]),
334
+ nickname: firstString(raw, ["nickname", "name", "label"]),
335
+ role: firstString(raw, ["role", "kind"]),
336
+ model: firstString(raw, ["model", "modelName"]),
337
+ prompt: firstString(raw, ["prompt", "instructions", "message"]),
338
+ };
339
+ }
340
+ function parseSubagentStates(value) {
341
+ const result = {};
342
+ if (Array.isArray(value)) {
343
+ for (const entry of value) {
344
+ const raw = asRecord(entry);
345
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
346
+ const status = firstString(raw, ["status", "state", "phase"]);
347
+ if (!threadId || !status)
348
+ continue;
349
+ result[threadId] = {
350
+ threadId,
351
+ status,
352
+ message: firstString(raw, ["message", "summary", "text"]),
353
+ };
354
+ }
355
+ return result;
356
+ }
357
+ const raw = asRecord(value);
358
+ if (!raw)
359
+ return result;
360
+ for (const [threadId, entry] of Object.entries(raw)) {
361
+ const state = asRecord(entry);
362
+ if (state) {
363
+ result[threadId] = {
364
+ threadId,
365
+ status: firstString(state, ["status", "state", "phase"]) ?? "running",
366
+ message: firstString(state, ["message", "summary", "text"]),
367
+ };
368
+ }
369
+ else if (typeof entry === "string") {
370
+ result[threadId] = { threadId, status: entry };
371
+ }
372
+ }
373
+ return result;
374
+ }
375
+ function parseStructuredInputOption(value, index) {
376
+ const raw = asRecord(value);
377
+ if (!raw) {
378
+ if (typeof value === "string" && value.trim()) {
379
+ return { id: `option-${index + 1}`, label: value.trim() };
380
+ }
381
+ return undefined;
382
+ }
383
+ const label = firstString(raw, ["label", "title", "text", "value"]);
384
+ if (!label)
385
+ return undefined;
386
+ return {
387
+ id: firstString(raw, ["id", "optionId", "value"]) ?? `option-${index + 1}`,
388
+ label,
389
+ description: firstString(raw, ["description", "detail", "subtitle"]),
390
+ };
391
+ }
392
+ function parseStructuredInputQuestion(value, index) {
393
+ const raw = asRecord(value);
394
+ if (!raw)
395
+ return undefined;
396
+ const question = firstString(raw, ["question", "prompt", "text", "message", "label"]);
397
+ if (!question)
398
+ return undefined;
399
+ const options = arrayFromKeys(raw, ["options", "choices", "items"])
400
+ .map(parseStructuredInputOption)
401
+ .filter((option) => Boolean(option));
402
+ return {
403
+ id: firstString(raw, ["id", "questionId", "key"]) ?? `question-${index + 1}`,
404
+ header: firstString(raw, ["header", "title"]),
405
+ question,
406
+ isOther: raw.isOther === true,
407
+ isSecret: raw.isSecret === true || raw.secret === true,
408
+ selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
409
+ options: options.length > 0 ? options : undefined,
410
+ };
411
+ }
412
+ function decodeStructuredInput(value) {
413
+ const raw = asRecord(value) ?? {};
414
+ const questions = arrayFromKeys(raw, ["questions", "items", "prompts"])
415
+ .map(parseStructuredInputQuestion)
416
+ .filter((question) => Boolean(question));
417
+ if (questions.length === 0) {
418
+ const single = parseStructuredInputQuestion(raw, 0);
419
+ if (single)
420
+ questions.push(single);
421
+ }
422
+ if (questions.length === 0)
423
+ return undefined;
424
+ return {
425
+ requestId: firstString(raw, ["requestId", "id", "inputId"]) ?? id("input"),
426
+ questions,
427
+ };
428
+ }
429
+ function decodeSubagentAction(item, status) {
430
+ const nested = asRecord(item.action) ?? asRecord(item.toolCall) ?? asRecord(item.call) ?? {};
431
+ const receiverAgents = [
432
+ ...arrayFromKeys(item, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
433
+ ...arrayFromKeys(nested, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
434
+ ].filter((entry) => Boolean(entry));
435
+ const receiverThreadIds = [
436
+ ...stringArray(item.receiverThreadIds),
437
+ ...stringArray(item.threadIds),
438
+ ...stringArray(item.childThreadIds),
439
+ ...stringArray(item.agentThreadIds),
440
+ ...stringArray(nested.receiverThreadIds),
441
+ ...stringArray(nested.threadIds),
442
+ ...receiverAgents.map((agent) => agent.threadId),
443
+ ].filter((threadId, index, array) => array.indexOf(threadId) === index);
444
+ const agentStates = {
445
+ ...parseSubagentStates(item.agentStates ?? item.states ?? item.statusByThread),
446
+ ...parseSubagentStates(nested.agentStates ?? nested.states ?? nested.statusByThread),
447
+ };
448
+ if (receiverThreadIds.length === 0 && Object.keys(agentStates).length === 0)
449
+ return undefined;
450
+ return {
451
+ tool: firstString(item, ["tool", "toolName", "name", "type"]) ??
452
+ firstString(nested, ["tool", "toolName", "name", "type"]) ??
453
+ "subagent",
454
+ status,
455
+ prompt: firstString(item, ["prompt", "instructions", "message"]) ??
456
+ firstString(nested, ["prompt", "instructions", "message"]),
457
+ model: firstString(item, ["model", "modelName"]) ?? firstString(nested, ["model", "modelName"]),
458
+ receiverThreadIds,
459
+ receiverAgents,
460
+ agentStates,
461
+ };
462
+ }
463
+ function summarizeSubagentAction(action) {
464
+ const count = Math.max(1, action.receiverThreadIds.length, action.receiverAgents.length);
465
+ const normalized = normalizedIdentifier(action.tool);
466
+ if (normalized.includes("spawn"))
467
+ return `启动 ${count} 个子 Agent`;
468
+ if (normalized.includes("wait"))
469
+ return `等待 ${count} 个子 Agent`;
470
+ if (normalized.includes("resume"))
471
+ return `恢复 ${count} 个子 Agent`;
472
+ if (normalized.includes("close"))
473
+ return `关闭 ${count} 个子 Agent`;
474
+ if (normalized.includes("sendinput"))
475
+ return `更新 ${count} 个子 Agent`;
476
+ return count === 1 ? "子 Agent 活动" : `${count} 个子 Agent 活动`;
477
+ }
195
478
  function previewText(text) {
196
479
  return text.replace(/\s+/g, " ").trim().slice(0, 160);
197
480
  }
@@ -229,6 +512,8 @@ export class AgentWorkspaceProxy {
229
512
  pendingPermissions = new Map();
230
513
  permissionWaiters = new Map();
231
514
  permissionSources = new Map();
515
+ pendingStructuredInputs = new Map();
516
+ structuredInputWaiters = new Map();
232
517
  toolConversationIds = new Map();
233
518
  agentProtocol;
234
519
  constructor(input) {
@@ -283,6 +568,11 @@ export class AgentWorkspaceProxy {
283
568
  this.respondPermission(payload);
284
569
  break;
285
570
  }
571
+ case "agent.v2.structured_input.respond": {
572
+ const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
573
+ this.respondStructuredInput(payload);
574
+ break;
575
+ }
286
576
  }
287
577
  }
288
578
  stop() {
@@ -534,6 +824,9 @@ export class AgentWorkspaceProxy {
534
824
  }
535
825
  }
536
826
  handleRequest(method, params) {
827
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
828
+ return this.handleStructuredInput(params, true);
829
+ }
537
830
  if (isPermissionRequestMethod(method)) {
538
831
  return this.handlePermission(params, true, method);
539
832
  }
@@ -556,6 +849,10 @@ export class AgentWorkspaceProxy {
556
849
  return;
557
850
  }
558
851
  const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
852
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
853
+ this.handleStructuredInput(params);
854
+ return;
855
+ }
559
856
  if (isPermissionRequestMethod(method)) {
560
857
  this.handlePermission(params, false, method);
561
858
  return;
@@ -712,14 +1009,21 @@ export class AgentWorkspaceProxy {
712
1009
  if (!item)
713
1010
  return;
714
1011
  const itemType = firstString(item, ["type"]);
715
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1012
+ const normalizedItemType = normalizedIdentifier(itemType);
1013
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
716
1014
  this.handleCompletedMessageItem(item, true);
717
1015
  return;
718
1016
  }
719
- if (itemType === "plan") {
1017
+ if (normalizedItemType === "plan") {
720
1018
  this.handlePlanUpdated({ plan: [item] });
721
1019
  return;
722
1020
  }
1021
+ if (isSubagentItemType(itemType)) {
1022
+ this.handleSubagentItem(item, "running", true);
1023
+ return;
1024
+ }
1025
+ if (this.handleSemanticSystemItem(item, "running", true))
1026
+ return;
723
1027
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
724
1028
  const toolCall = this.toolCallFromItem(item, "running");
725
1029
  if (!conversationId || !toolCall)
@@ -732,10 +1036,21 @@ export class AgentWorkspaceProxy {
732
1036
  if (!item)
733
1037
  return;
734
1038
  const itemType = firstString(item, ["type"]);
735
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1039
+ const normalizedItemType = normalizedIdentifier(itemType);
1040
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
736
1041
  this.handleCompletedMessageItem(item, false);
737
1042
  return;
738
1043
  }
1044
+ if (normalizedItemType === "plan") {
1045
+ this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
1046
+ return;
1047
+ }
1048
+ if (isSubagentItemType(itemType)) {
1049
+ this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
1050
+ return;
1051
+ }
1052
+ if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false))
1053
+ return;
739
1054
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
740
1055
  const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
741
1056
  if (!conversationId || !toolCall)
@@ -900,18 +1215,131 @@ export class AgentWorkspaceProxy {
900
1215
  });
901
1216
  this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
902
1217
  }
1218
+ handleSemanticSystemItem(item, status, streaming) {
1219
+ const itemType = firstString(item, ["type"]);
1220
+ const normalized = normalizedIdentifier(itemType);
1221
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1222
+ if (!conversationId)
1223
+ return false;
1224
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
1225
+ const existing = this.findItem(conversationId, itemId);
1226
+ const base = {
1227
+ id: itemId,
1228
+ conversationId,
1229
+ type: "status",
1230
+ role: "system",
1231
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1232
+ itemId,
1233
+ createdAt: existing?.createdAt ?? Date.now(),
1234
+ updatedAt: Date.now(),
1235
+ isStreaming: streaming,
1236
+ };
1237
+ if (normalized === "reasoning" || normalized === "thinking") {
1238
+ const text = firstString(item, ["text", "content", "summary", "message"]) ??
1239
+ stringifyDefined(item.contentItems ?? item.summary);
1240
+ this.upsertItem(conversationId, {
1241
+ ...base,
1242
+ kind: "thinking",
1243
+ text: text ?? (streaming ? "正在思考" : "完成思考"),
1244
+ });
1245
+ return true;
1246
+ }
1247
+ if (normalized === "enteredreviewmode") {
1248
+ const target = firstString(item, ["review", "target", "label"]) ?? "changes";
1249
+ this.upsertItem(conversationId, {
1250
+ ...base,
1251
+ kind: "review",
1252
+ text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
1253
+ });
1254
+ return true;
1255
+ }
1256
+ if (normalized === "contextcompaction") {
1257
+ this.upsertItem(conversationId, {
1258
+ ...base,
1259
+ kind: "context_compaction",
1260
+ text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
1261
+ });
1262
+ return true;
1263
+ }
1264
+ return false;
1265
+ }
1266
+ handleSubagentItem(item, status, streaming) {
1267
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1268
+ if (!conversationId)
1269
+ return;
1270
+ const subagent = decodeSubagentAction(item, status);
1271
+ if (!subagent)
1272
+ return;
1273
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
1274
+ const text = summarizeSubagentAction(subagent);
1275
+ const existing = this.findItem(conversationId, itemId);
1276
+ this.upsertItem(conversationId, {
1277
+ id: itemId,
1278
+ conversationId,
1279
+ type: "status",
1280
+ kind: "subagent_action",
1281
+ role: "system",
1282
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1283
+ itemId,
1284
+ text,
1285
+ subagent,
1286
+ createdAt: existing?.createdAt ?? Date.now(),
1287
+ updatedAt: Date.now(),
1288
+ isStreaming: streaming,
1289
+ });
1290
+ this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
1291
+ }
1292
+ handleStructuredInput(params, waitForResponse = false) {
1293
+ const raw = asRecord(params) ?? {};
1294
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1295
+ if (!conversationId)
1296
+ return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1297
+ const structuredInput = decodeStructuredInput(raw);
1298
+ if (!structuredInput)
1299
+ return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1300
+ const text = structuredInput.questions.map((question) => question.question).join("\n");
1301
+ this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
1302
+ this.upsertItem(conversationId, {
1303
+ id: `input:${structuredInput.requestId}`,
1304
+ conversationId,
1305
+ type: "status",
1306
+ kind: "user_input_prompt",
1307
+ role: "system",
1308
+ text,
1309
+ structuredInput,
1310
+ metadata: { inputPending: true },
1311
+ createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
1312
+ updatedAt: Date.now(),
1313
+ });
1314
+ this.updateConversationPreview(conversationId, "需要用户输入", "running");
1315
+ if (!waitForResponse)
1316
+ return;
1317
+ return new Promise((resolve) => {
1318
+ const timer = setTimeout(() => {
1319
+ this.pendingStructuredInputs.delete(structuredInput.requestId);
1320
+ this.structuredInputWaiters.delete(structuredInput.requestId);
1321
+ resolve(formatStructuredInputResponse({}));
1322
+ this.markStructuredInput(conversationId, structuredInput.requestId, {
1323
+ inputPending: false,
1324
+ inputError: "等待用户输入超时",
1325
+ });
1326
+ }, PERMISSION_TIMEOUT_MS);
1327
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
1328
+ });
1329
+ }
903
1330
  toolCallFromItem(item, fallbackStatus) {
904
1331
  const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
905
1332
  if (!itemId)
906
1333
  return undefined;
907
1334
  const itemType = firstString(item, ["type"]);
1335
+ const normalizedItemType = normalizedIdentifier(itemType);
908
1336
  const name = toolNameFromItem(item);
909
1337
  if (!name && !isToolItemType(itemType))
910
1338
  return undefined;
911
1339
  const bufferedOutput = this.toolOutputBuffers.get(itemId);
912
1340
  const rawOutput = firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
913
1341
  stringifyDefined(item.result ?? item.error ?? item.contentItems);
914
- const output = itemType === "fileChange"
1342
+ const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
915
1343
  ? extractDiffText(item) ?? bufferedOutput ?? rawOutput
916
1344
  : rawOutput ?? bufferedOutput;
917
1345
  return {
@@ -989,6 +1417,32 @@ export class AgentWorkspaceProxy {
989
1417
  }
990
1418
  this.updateConversationStatus(payload.conversationId, "running");
991
1419
  }
1420
+ respondStructuredInput(payload) {
1421
+ const pending = this.pendingStructuredInputs.get(payload.requestId);
1422
+ this.pendingStructuredInputs.delete(payload.requestId);
1423
+ const waiter = this.structuredInputWaiters.get(payload.requestId);
1424
+ if (waiter) {
1425
+ clearTimeout(waiter.timer);
1426
+ this.structuredInputWaiters.delete(payload.requestId);
1427
+ waiter.resolve(formatStructuredInputResponse(payload.answers));
1428
+ }
1429
+ this.markStructuredInput(payload.conversationId, payload.requestId, {
1430
+ inputPending: false,
1431
+ inputSubmitted: true,
1432
+ answers: payload.answers,
1433
+ });
1434
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
1435
+ }
1436
+ markStructuredInput(conversationId, requestId, metadata) {
1437
+ const item = this.findItem(conversationId, `input:${requestId}`);
1438
+ if (!item)
1439
+ return;
1440
+ this.upsertItem(conversationId, {
1441
+ ...item,
1442
+ metadata: { ...(item.metadata ?? {}), ...metadata },
1443
+ updatedAt: Date.now(),
1444
+ });
1445
+ }
992
1446
  addItem(conversationId, item) {
993
1447
  const timeline = this.timelines.get(conversationId) ?? [];
994
1448
  timeline.push(item);
@@ -1014,21 +1468,55 @@ export class AgentWorkspaceProxy {
1014
1468
  this.emitItem(conversationId, item);
1015
1469
  }
1016
1470
  upsertTool(conversationId, toolCall) {
1017
- const existing = this.findTool(conversationId, toolCall.id);
1471
+ const duplicate = this.findDuplicateFileTool(conversationId, toolCall);
1472
+ if (duplicate && duplicate.id !== toolCall.id) {
1473
+ this.removeToolItem(conversationId, toolCall.id);
1474
+ }
1475
+ const targetToolId = duplicate?.id ?? toolCall.id;
1476
+ const existing = this.findTool(conversationId, targetToolId);
1018
1477
  const nextToolCall = {
1478
+ ...existing,
1019
1479
  ...toolCall,
1480
+ id: targetToolId,
1020
1481
  createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
1021
1482
  };
1483
+ this.toolConversationIds.set(toolCall.id, conversationId);
1022
1484
  this.toolConversationIds.set(nextToolCall.id, conversationId);
1485
+ const kind = nextToolCall.name.includes("文件")
1486
+ ? "file_change"
1487
+ : nextToolCall.name.includes("命令")
1488
+ ? "command_execution"
1489
+ : "tool_activity";
1023
1490
  this.upsertItem(conversationId, {
1024
1491
  id: `tool:${nextToolCall.id}`,
1025
1492
  conversationId,
1026
1493
  type: "tool_call",
1494
+ kind,
1495
+ itemId: nextToolCall.id,
1027
1496
  toolCall: nextToolCall,
1497
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
1498
+ fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
1028
1499
  createdAt: nextToolCall.createdAt ?? Date.now(),
1029
1500
  updatedAt: Date.now(),
1030
1501
  });
1031
1502
  }
1503
+ findDuplicateFileTool(conversationId, toolCall) {
1504
+ const output = toolCall.output?.trim();
1505
+ if (!toolCall.name.includes("文件") || !output)
1506
+ return undefined;
1507
+ return this.timelines.get(conversationId)?.find((entry) => entry.type === "tool_call" &&
1508
+ entry.toolCall?.id !== toolCall.id &&
1509
+ entry.toolCall?.name.includes("文件") &&
1510
+ entry.toolCall.output?.trim() === output)?.toolCall;
1511
+ }
1512
+ removeToolItem(conversationId, toolId) {
1513
+ const timeline = this.timelines.get(conversationId);
1514
+ if (!timeline)
1515
+ return;
1516
+ const index = timeline.findIndex((entry) => entry.id === `tool:${toolId}`);
1517
+ if (index >= 0)
1518
+ timeline.splice(index, 1);
1519
+ }
1032
1520
  findItem(conversationId, itemId) {
1033
1521
  return this.timelines.get(conversationId)?.find((item) => item.id === itemId);
1034
1522
  }
@@ -1180,6 +1668,19 @@ export class AgentWorkspaceProxy {
1180
1668
  this.permissionSources.delete(requestId);
1181
1669
  }
1182
1670
  this.permissionWaiters.clear();
1671
+ for (const [requestId, waiter] of this.structuredInputWaiters) {
1672
+ clearTimeout(waiter.timer);
1673
+ waiter.resolve(formatStructuredInputResponse({}));
1674
+ const pending = this.pendingStructuredInputs.get(requestId);
1675
+ if (pending) {
1676
+ this.markStructuredInput(pending.conversationId, requestId, {
1677
+ inputPending: false,
1678
+ inputError: "已停止",
1679
+ });
1680
+ }
1681
+ this.pendingStructuredInputs.delete(requestId);
1682
+ }
1683
+ this.structuredInputWaiters.clear();
1183
1684
  if (conversationId)
1184
1685
  this.updateConversationStatus(conversationId, "idle");
1185
1686
  }
@@ -1247,8 +1748,15 @@ function selectPermissionOption(permission, outcome) {
1247
1748
  function isPermissionRequestMethod(method) {
1248
1749
  return (method === "session/request_permission" ||
1249
1750
  method.endsWith("/requestApproval") ||
1250
- method === "mcpServer/elicitation/request" ||
1251
- method === "item/tool/requestUserInput");
1751
+ method === "mcpServer/elicitation/request");
1752
+ }
1753
+ function formatStructuredInputResponse(answers) {
1754
+ return {
1755
+ answers: Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
1756
+ questionId,
1757
+ { answers: values.map((value) => value.trim()).filter(Boolean) },
1758
+ ])),
1759
+ };
1252
1760
  }
1253
1761
  function formatPermissionResponse(source, outcome, optionId) {
1254
1762
  if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {