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.
@@ -15,6 +15,8 @@ export declare class AgentWorkspaceProxy {
15
15
  private pendingPermissions;
16
16
  private permissionWaiters;
17
17
  private permissionSources;
18
+ private pendingStructuredInputs;
19
+ private structuredInputWaiters;
18
20
  private toolConversationIds;
19
21
  private agentProtocol;
20
22
  constructor(input: {
@@ -46,9 +48,14 @@ export declare class AgentWorkspaceProxy {
46
48
  private handleCommandExecDelta;
47
49
  private handleCompletedMessageItem;
48
50
  private handleSessionUpdate;
51
+ private handleSemanticSystemItem;
52
+ private handleSubagentItem;
53
+ private handleStructuredInput;
49
54
  private toolCallFromItem;
50
55
  private handlePermission;
51
56
  private respondPermission;
57
+ private respondStructuredInput;
58
+ private markStructuredInput;
52
59
  private addItem;
53
60
  private upsertItem;
54
61
  private upsertTool;
@@ -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);
@@ -1028,11 +1482,20 @@ export class AgentWorkspaceProxy {
1028
1482
  };
1029
1483
  this.toolConversationIds.set(toolCall.id, conversationId);
1030
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";
1031
1490
  this.upsertItem(conversationId, {
1032
1491
  id: `tool:${nextToolCall.id}`,
1033
1492
  conversationId,
1034
1493
  type: "tool_call",
1494
+ kind,
1495
+ itemId: nextToolCall.id,
1035
1496
  toolCall: nextToolCall,
1497
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
1498
+ fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
1036
1499
  createdAt: nextToolCall.createdAt ?? Date.now(),
1037
1500
  updatedAt: Date.now(),
1038
1501
  });
@@ -1205,6 +1668,19 @@ export class AgentWorkspaceProxy {
1205
1668
  this.permissionSources.delete(requestId);
1206
1669
  }
1207
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();
1208
1684
  if (conversationId)
1209
1685
  this.updateConversationStatus(conversationId, "idle");
1210
1686
  }
@@ -1272,8 +1748,15 @@ function selectPermissionOption(permission, outcome) {
1272
1748
  function isPermissionRequestMethod(method) {
1273
1749
  return (method === "session/request_permission" ||
1274
1750
  method.endsWith("/requestApproval") ||
1275
- method === "mcpServer/elicitation/request" ||
1276
- 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
+ };
1277
1760
  }
1278
1761
  function formatPermissionResponse(source, outcome, optionId) {
1279
1762
  if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {