linkshell-cli 0.2.96 → 0.2.98

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.
@@ -114,8 +114,10 @@ export class ClaudeStreamJsonClient {
114
114
  if (this.claudeSessionId) {
115
115
  args.push("--resume", this.claudeSessionId);
116
116
  }
117
- // Prevent autonomous multi-turn tool loops in headless mode
118
- args.push("--max-turns", "1");
117
+ // Allow up to 10 turns so Claude can execute tools and continue responding.
118
+ // With bypassPermissions, tools auto-execute — a generous limit prevents
119
+ // infinite loops while still allowing complex multi-step workflows.
120
+ args.push("--max-turns", "10");
119
121
 
120
122
  if (input.model) {
121
123
  args.push("--model", input.model);
@@ -180,6 +182,10 @@ export class ClaudeStreamJsonClient {
180
182
  const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
181
183
  let currentToolId: string | undefined;
182
184
  let currentToolName: string | undefined;
185
+ let currentMessageId: string | undefined;
186
+ // Map tool_use_id → tool_name so tool_result can look up the correct name
187
+ // even when multiple tools are in flight
188
+ const toolNames = new Map<string, string>();
183
189
 
184
190
  rl.on("line", (line: string) => {
185
191
  if (this.pendingCancel) {
@@ -225,11 +231,15 @@ export class ClaudeStreamJsonClient {
225
231
  const message = event.message;
226
232
  if (!message) break;
227
233
  const content = (message.content ?? []) as ClaudeContentBlock[];
234
+ // Reset per-message tracking — each assistant message starts fresh
235
+ currentMessageId = undefined;
228
236
 
229
237
  for (const block of content) {
230
238
  switch (block.type) {
231
239
  case "thinking":
232
- this.input.onNotification("item/started", {
240
+ // Use item/completed since thinking blocks arrive complete (not streaming deltas)
241
+ // item/started would leave isStreaming=true forever with no matching item/completed
242
+ this.input.onNotification("item/completed", {
233
243
  sessionId: this.claudeSessionId,
234
244
  item: {
235
245
  id: event.uuid ?? id("thinking"),
@@ -241,9 +251,10 @@ export class ClaudeStreamJsonClient {
241
251
  break;
242
252
 
243
253
  case "text":
254
+ currentMessageId = (typeof message.id === "string" ? message.id : undefined) ?? event.uuid ?? id("msg");
244
255
  this.input.onNotification("item/agentMessage/delta", {
245
256
  sessionId: this.claudeSessionId,
246
- itemId: message.id ?? event.uuid ?? id("msg"),
257
+ itemId: currentMessageId,
247
258
  delta: block.text,
248
259
  });
249
260
  break;
@@ -251,6 +262,7 @@ export class ClaudeStreamJsonClient {
251
262
  case "tool_use": {
252
263
  currentToolId = block.id;
253
264
  currentToolName = block.name ?? "tool";
265
+ if (block.id) toolNames.set(block.id, block.name ?? "tool");
254
266
  const toolName = block.name ?? "tool";
255
267
  this.input.onNotification("item/started", {
256
268
  sessionId: this.claudeSessionId,
@@ -273,6 +285,20 @@ export class ClaudeStreamJsonClient {
273
285
  break;
274
286
  }
275
287
  }
288
+
289
+ // Mark this assistant message as complete — the full message has arrived.
290
+ // Each text block was streamed via item/agentMessage/delta; now we signal
291
+ // that streaming is done so the UI stops showing "正在生成".
292
+ if (currentMessageId) {
293
+ this.input.onNotification("item/completed", {
294
+ sessionId: this.claudeSessionId,
295
+ item: {
296
+ id: currentMessageId,
297
+ type: "agentMessage",
298
+ status: "completed",
299
+ },
300
+ });
301
+ }
276
302
  break;
277
303
  }
278
304
 
@@ -285,14 +311,15 @@ export class ClaudeStreamJsonClient {
285
311
  for (const block of content) {
286
312
  if (block.type === "tool_result") {
287
313
  const toolId = block.tool_use_id ?? currentToolId;
314
+ const toolName = (toolId ? toolNames.get(toolId) : undefined) ?? currentToolName;
288
315
  const isError = block.is_error === true;
289
316
  this.input.onNotification("item/completed", {
290
317
  sessionId: this.claudeSessionId,
291
318
  item: {
292
319
  id: toolId ?? id("tool"),
293
320
  type: "toolCall",
294
- toolName: currentToolName,
295
- tool: currentToolName,
321
+ toolName,
322
+ tool: toolName,
296
323
  status: isError ? "failed" : "completed",
297
324
  output: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
298
325
  aggregatedOutput: typeof block.content === "string" ? block.content : undefined,
@@ -305,6 +332,17 @@ export class ClaudeStreamJsonClient {
305
332
  }
306
333
 
307
334
  case "result": {
335
+ // Mark the last agent message as complete so isStreaming flips to false
336
+ if (currentMessageId) {
337
+ this.input.onNotification("item/completed", {
338
+ sessionId: this.claudeSessionId,
339
+ item: {
340
+ id: currentMessageId,
341
+ type: "agentMessage",
342
+ status: "completed",
343
+ },
344
+ });
345
+ }
308
346
  // Turn complete
309
347
  const isError = event.subtype === "error" || event.is_error === true;
310
348
  this.input.onNotification("turn/completed", {