linkshell-cli 0.2.121 → 0.2.123

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.
@@ -529,6 +529,49 @@ function textFromBlocks(blocks: AgentContentBlock[]): string {
529
529
  .join("\n");
530
530
  }
531
531
 
532
+ function contentBlocksFromValue(value: unknown): AgentContentBlock[] {
533
+ if (typeof value === "string") {
534
+ return value.trim() ? [{ type: "text", text: value }] : [];
535
+ }
536
+ if (Array.isArray(value)) {
537
+ return value.flatMap((entry) => contentBlocksFromValue(entry));
538
+ }
539
+ const raw = asRecord(value);
540
+ if (!raw) return [];
541
+ const rawType = firstString(raw, ["type", "kind"]);
542
+ const normalizedType = normalizedIdentifier(rawType);
543
+ if (normalizedType === "image" || normalizedType === "inputimage" || normalizedType === "outputimage") {
544
+ const data = firstString(raw, [
545
+ "data",
546
+ "url",
547
+ "uri",
548
+ "imageUrl",
549
+ "image_url",
550
+ "base64",
551
+ ]);
552
+ const mimeType = firstString(raw, ["mimeType", "mime_type", "mediaType", "media_type"]);
553
+ const text = firstString(raw, ["text", "alt", "caption", "name"]);
554
+ return [{ type: "image", data, mimeType, text }];
555
+ }
556
+ if (normalizedType === "text" || normalizedType === "outputtext" || normalizedType === "inputtext") {
557
+ const text = firstString(raw, ["text", "content", "message"]);
558
+ return text ? [{ type: "text", text }] : [];
559
+ }
560
+ const nested = raw.content ?? raw.contentItems ?? raw.parts;
561
+ if (Array.isArray(nested)) return contentBlocksFromValue(nested);
562
+ const text = firstString(raw, ["text", "message", "content"]);
563
+ return text ? [{ type: "text", text }] : [];
564
+ }
565
+
566
+ function contentBlocksFromItem(item: Record<string, unknown>): AgentContentBlock[] {
567
+ for (const key of ["content", "contentItems", "parts", "message"]) {
568
+ const blocks = contentBlocksFromValue(item[key]);
569
+ if (blocks.length > 0) return blocks;
570
+ }
571
+ const text = firstString(item, ["text", "message"]);
572
+ return text ? [{ type: "text", text }] : [];
573
+ }
574
+
532
575
  function protocolSupportsImages(protocol: AgentProtocol | undefined): boolean {
533
576
  return protocol === "codex-app-server" ||
534
577
  protocol === "claude-agent-sdk" ||
@@ -2053,7 +2096,11 @@ export class AgentWorkspaceProxy {
2053
2096
  if (agentSessionId && conversationId) {
2054
2097
  this.conversationByAgentSessionId.set(agentSessionId, conversationId);
2055
2098
  const conversation = this.conversations.get(conversationId);
2056
- if (conversation) conversation.agentSessionId = agentSessionId;
2099
+ if (conversation) {
2100
+ conversation.agentSessionId = agentSessionId;
2101
+ conversation.lastActivityAt = Date.now();
2102
+ this.emitConversation(conversation);
2103
+ }
2057
2104
  }
2058
2105
  return;
2059
2106
  }
@@ -2387,20 +2434,24 @@ export class AgentWorkspaceProxy {
2387
2434
  if (!conversationId) return;
2388
2435
  const itemId = firstString(item, ["id"]) ?? id("msg");
2389
2436
  const existing = this.findItem(conversationId, itemId);
2390
- const content = firstString(item, ["text", "content", "message"]) ?? existing?.text;
2391
- if (!content) return;
2437
+ const content = contentBlocksFromItem(item);
2438
+ const nextContent = content.length > 0
2439
+ ? content
2440
+ : existing?.content ?? (existing?.text ? [{ type: "text", text: existing.text }] : []);
2441
+ const text = textFromBlocks(nextContent);
2442
+ if (!nextContent.length && !text) return;
2392
2443
  this.upsertItem(conversationId, {
2393
2444
  id: itemId,
2394
2445
  conversationId,
2395
2446
  type: "message",
2396
2447
  role: "assistant",
2397
- content: [{ type: "text", text: content }],
2398
- text: content,
2448
+ content: nextContent,
2449
+ text,
2399
2450
  createdAt: existing?.createdAt ?? Date.now(),
2400
2451
  updatedAt: Date.now(),
2401
2452
  isStreaming: streaming,
2402
2453
  });
2403
- this.updateConversationPreview(conversationId, content, streaming ? "running" : "idle");
2454
+ this.updateConversationPreview(conversationId, text || "图片附件", streaming ? "running" : "idle");
2404
2455
  }
2405
2456
 
2406
2457
  private handleSessionUpdate(params: unknown): void {
@@ -2409,7 +2460,8 @@ export class AgentWorkspaceProxy {
2409
2460
  const text =
2410
2461
  firstString(raw, ["delta", "text", "content", "message"]) ??
2411
2462
  firstString(nested, ["delta", "text", "content", "message"]);
2412
- if (!text) return;
2463
+ const content = contentBlocksFromItem(raw);
2464
+ if (!text && content.length === 0) return;
2413
2465
  const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
2414
2466
  if (!conversationId) return;
2415
2467
  if (firstString(raw, ["toolName", "tool", "name"])) {
@@ -2426,18 +2478,20 @@ export class AgentWorkspaceProxy {
2426
2478
  return;
2427
2479
  }
2428
2480
  const role = raw.role === "user" || raw.role === "system" ? raw.role : "assistant";
2481
+ const blocks = content.length > 0 ? content : [{ type: "text" as const, text }];
2482
+ const preview = textFromBlocks(blocks);
2429
2483
  this.upsertItem(conversationId, {
2430
2484
  id: firstString(raw, ["messageId", "id"]) ?? id("msg"),
2431
2485
  conversationId,
2432
2486
  type: "message",
2433
2487
  role,
2434
- content: [{ type: "text", text }],
2435
- text,
2488
+ content: blocks,
2489
+ text: preview,
2436
2490
  createdAt: Date.now(),
2437
2491
  updatedAt: Date.now(),
2438
2492
  isStreaming: raw.done === false || raw.isStreaming === true,
2439
2493
  });
2440
- this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
2494
+ this.updateConversationPreview(conversationId, preview || "图片附件", raw.done === true ? "idle" : "running");
2441
2495
  }
2442
2496
 
2443
2497
  private handleSemanticSystemItem(
@@ -271,8 +271,18 @@ export class ClaudeSdkClient {
271
271
  let currentToolId: string | undefined;
272
272
  let currentToolName: string | undefined;
273
273
  let currentMessageId: string | undefined;
274
+ const progressItemId = `claude-progress:${input.clientMessageId}`;
274
275
 
275
276
  try {
277
+ this.input.onNotification("item/started", {
278
+ sessionId: input.sessionId ?? this.claudeSessionId,
279
+ item: {
280
+ id: progressItemId,
281
+ type: "thinking",
282
+ text: "Claude 正在处理请求",
283
+ status: "running",
284
+ },
285
+ });
276
286
  const queryPrompt = hasImages ? singleUserMessage(toClaudeMessageContent(inputBlocks)) : prompt;
277
287
  for await (const message of this.query({ prompt: queryPrompt, options: sdkOptions })) {
278
288
  if (abortController.signal.aborted) break;
@@ -295,6 +305,15 @@ export class ClaudeSdkClient {
295
305
  }
296
306
  return { sessionId: this.claudeSessionId, status: abortController.signal.aborted ? "cancelled" : "completed" };
297
307
  } finally {
308
+ this.input.onNotification("item/completed", {
309
+ sessionId: this.claudeSessionId ?? input.sessionId,
310
+ item: {
311
+ id: progressItemId,
312
+ type: "thinking",
313
+ text: abortController.signal.aborted ? "Claude 已停止" : "Claude 已完成",
314
+ status: abortController.signal.aborted ? "failed" : "completed",
315
+ },
316
+ });
298
317
  if (this.abortController === abortController) this.abortController = undefined;
299
318
  }
300
319
  }
@@ -222,10 +222,21 @@ export class ClaudeStreamJsonClient {
222
222
  let currentToolId: string | undefined;
223
223
  let currentToolName: string | undefined;
224
224
  let currentMessageId: string | undefined;
225
+ const progressItemId = `claude-progress:${input.clientMessageId}`;
225
226
  // Map tool_use_id → tool_name so tool_result can look up the correct name
226
227
  // even when multiple tools are in flight
227
228
  const toolNames = new Map<string, string>();
228
229
 
230
+ this.input.onNotification("item/started", {
231
+ sessionId: input.sessionId ?? this.claudeSessionId,
232
+ item: {
233
+ id: progressItemId,
234
+ type: "thinking",
235
+ text: "Claude 正在处理请求",
236
+ status: "running",
237
+ },
238
+ });
239
+
229
240
  rl.on("line", (line: string) => {
230
241
  if (this.pendingCancel) {
231
242
  child.kill("SIGTERM");
@@ -375,6 +386,16 @@ export class ClaudeStreamJsonClient {
375
386
  }
376
387
 
377
388
  case "result": {
389
+ const isError = event.subtype === "error" || event.is_error === true;
390
+ this.input.onNotification("item/completed", {
391
+ sessionId: this.claudeSessionId ?? input.sessionId,
392
+ item: {
393
+ id: progressItemId,
394
+ type: "thinking",
395
+ text: isError ? "Claude 运行出错" : "Claude 已完成",
396
+ status: isError ? "failed" : "completed",
397
+ },
398
+ });
378
399
  // Mark the last agent message as complete so isStreaming flips to false
379
400
  if (currentMessageId) {
380
401
  this.input.onNotification("item/completed", {
@@ -387,7 +408,6 @@ export class ClaudeStreamJsonClient {
387
408
  });
388
409
  }
389
410
  // Turn complete
390
- const isError = event.subtype === "error" || event.is_error === true;
391
411
  this.input.onNotification("turn/completed", {
392
412
  sessionId: this.claudeSessionId,
393
413
  stopReason: event.stop_reason ?? (isError ? "error" : "end_turn"),
@@ -408,11 +428,29 @@ export class ClaudeStreamJsonClient {
408
428
  });
409
429
 
410
430
  child.on("error", (err) => {
431
+ this.input.onNotification("item/completed", {
432
+ sessionId: this.claudeSessionId ?? input.sessionId,
433
+ item: {
434
+ id: progressItemId,
435
+ type: "thinking",
436
+ text: "Claude 运行出错",
437
+ status: "failed",
438
+ },
439
+ });
411
440
  finish(err, undefined);
412
441
  });
413
442
 
414
443
  child.on("exit", (code, signal) => {
415
444
  if (!settled) {
445
+ this.input.onNotification("item/completed", {
446
+ sessionId: this.claudeSessionId ?? input.sessionId,
447
+ item: {
448
+ id: progressItemId,
449
+ type: "thinking",
450
+ text: this.pendingCancel ? "Claude 已停止" : "Claude 意外退出",
451
+ status: "failed",
452
+ },
453
+ });
416
454
  finish(
417
455
  new Error(`Claude exited unexpectedly (code=${code ?? "null"}, signal=${signal ?? "null"})`),
418
456
  undefined,