@esso0428/pi-subagents 0.15.15 → 0.15.17
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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/index.ts +47 -5
- package/src/ui/agent-widget.ts +85 -17
- package/src/ui/conversation-blocks.ts +13 -11
- package/src/ui/conversation-timeline.ts +141 -56
- package/src/ui/conversation-viewer.ts +52 -22
- package/src/ui/deferred-ui-refresh.ts +72 -0
- package/src/ui/fleet-list.ts +215 -46
- package/test/ui/agent-widget.test.ts +50 -1
- package/test/ui/deferred-ui-refresh.test.ts +55 -0
- package/test/ui/fleet-list.test.ts +264 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.17] - 2026-09-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **FleetView preserves the selected row while navigating** by maintaining a bounded viewport over the full roster; `j`/`k`, arrow keys, and normalized wheel input share one directional transition and consume handled wheel events without stealing input from other focused components.
|
|
14
|
+
- **Structural Fleet/widget changes refresh the text-input viewport after a short defer** instead of requiring a manual redraw; refreshes coalesce, respect a maximum wait, cancel on session switch, and dispose before shutdown cleanup.
|
|
15
|
+
|
|
16
|
+
## [0.15.16] - 2026-09-13
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Live panel rendering now coalesces refreshes and avoids rebuilding unchanged rows**, keeping FleetView, the conversation timeline, and tool hover interactions responsive during frequent agent updates.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Same-name tool results remain paired with their originating tool calls when a conversation is refreshed.
|
|
23
|
+
|
|
10
24
|
## [0.15.15] - 2026-09-13
|
|
11
25
|
|
|
12
26
|
### Added
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
15
15
|
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
16
16
|
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and smart group join (consolidated notifications)
|
|
17
17
|
- **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons. Configurable via `/agents → Settings → Widget`: `all` (every agent), `background` (default — hides foreground runs, which already render inline as the `Agent` tool result), or `off`
|
|
18
|
-
- **FleetView** — Claude Code-style navigable list of `main` + every running subagent rendered below the editor (earliest-launched first). Press `↓` (or `←`) at an empty prompt to jump in,
|
|
18
|
+
- **FleetView** — Claude Code-style navigable list of `main` + every running subagent rendered below the editor (earliest-launched first). Press `↓` (or `←`) at an empty prompt to jump in, `↑`/`↓`/wheel to move the selection, `Enter` to open the selected agent's live, auto-updating conversation, `Esc` to return. The selected row stays visible while the bounded list window moves through the full roster. Finished agents linger briefly before dropping out, and a viewer stays open through completion so you can read the final output. Toggle via `/agents → Settings → Fleet view`
|
|
19
19
|
- **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). Steer a running agent inline by pressing `Enter` to open a composer, typing, then `Enter` to send (`Esc` or an empty submit returns) — the message appears as a user message and redirects the agent after its current tool. Stop a still-running agent by pressing `x` (then `x` again to confirm) — both work for background agents too
|
|
20
20
|
- **Custom agent types** — define agents in `.pi/agents/<name>.md` or `.agents/agents/<name>.md` (project) or globally, with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
|
|
21
21
|
- **Mid-run steering** — inject messages into running agents to redirect their work without restarting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esso0428/pi-subagents",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.17",
|
|
4
4
|
"description": "A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.",
|
|
5
5
|
"author": "ESSO0428",
|
|
6
6
|
"repository": {
|
package/src/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
type UICtx,
|
|
51
51
|
} from "./ui/agent-widget.js";
|
|
52
52
|
import { FleetList, type FleetUICtx } from "./ui/fleet-list.js";
|
|
53
|
+
import { createDeferredUiRefresh } from "./ui/deferred-ui-refresh.js";
|
|
53
54
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
54
55
|
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
55
56
|
|
|
@@ -374,6 +375,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
374
375
|
fleet.onAgentFinished(record.id);
|
|
375
376
|
scheduleNudge(record.id, () => emitIndividualNudge(record));
|
|
376
377
|
widget.update();
|
|
378
|
+
scheduleUiRefresh();
|
|
377
379
|
}
|
|
378
380
|
|
|
379
381
|
// ---- Group join manager ----
|
|
@@ -385,7 +387,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
385
387
|
scheduleNudge(groupKey, () => {
|
|
386
388
|
// Re-check at send time
|
|
387
389
|
const unconsumed = records.filter(r => !r.resultConsumed);
|
|
388
|
-
if (unconsumed.length === 0) {
|
|
390
|
+
if (unconsumed.length === 0) {
|
|
391
|
+
widget.update();
|
|
392
|
+
scheduleUiRefresh();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
389
395
|
|
|
390
396
|
const notifications = unconsumed.map(r => formatTaskNotification(r, 300)).join('\n\n');
|
|
391
397
|
const label = partial
|
|
@@ -406,6 +412,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
412
|
}, { deliverAs: "followUp", triggerTurn: true });
|
|
407
413
|
});
|
|
408
414
|
widget.update();
|
|
415
|
+
scheduleUiRefresh();
|
|
409
416
|
},
|
|
410
417
|
30_000,
|
|
411
418
|
);
|
|
@@ -463,6 +470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
463
470
|
widget.markFinished(record.id);
|
|
464
471
|
fleet.onAgentFinished(record.id);
|
|
465
472
|
widget.update();
|
|
473
|
+
scheduleUiRefresh();
|
|
466
474
|
return;
|
|
467
475
|
}
|
|
468
476
|
|
|
@@ -470,6 +478,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
470
478
|
// don't send an individual nudge — finalizeBatch will pick it up retroactively.
|
|
471
479
|
if (currentBatchAgents.some(a => a.id === record.id)) {
|
|
472
480
|
widget.update();
|
|
481
|
+
scheduleUiRefresh();
|
|
473
482
|
return;
|
|
474
483
|
}
|
|
475
484
|
|
|
@@ -480,6 +489,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
480
489
|
// 'held' → do nothing, group will fire later
|
|
481
490
|
// 'delivered' → group callback already fired
|
|
482
491
|
widget.update();
|
|
492
|
+
scheduleUiRefresh();
|
|
483
493
|
}, undefined, (record) => {
|
|
484
494
|
// Emit started event when agent transitions to running (including from queue)
|
|
485
495
|
pi.events.emit("subagents:started", {
|
|
@@ -487,6 +497,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
487
497
|
type: record.type,
|
|
488
498
|
description: record.description,
|
|
489
499
|
});
|
|
500
|
+
widget.ensureTimer();
|
|
501
|
+
widget.update();
|
|
502
|
+
fleet.ensureTimer();
|
|
503
|
+
fleet.update();
|
|
504
|
+
scheduleUiRefresh();
|
|
490
505
|
}, (record, info) => {
|
|
491
506
|
// Emit compacted event when agent's session compacts (preserves count on record).
|
|
492
507
|
pi.events.emit("subagents:compacted", {
|
|
@@ -581,6 +596,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
581
596
|
});
|
|
582
597
|
|
|
583
598
|
pi.on("session_before_switch", () => {
|
|
599
|
+
deferredUiRefresh.cancel();
|
|
584
600
|
manager.clearCompleted(true);
|
|
585
601
|
scheduler.stop();
|
|
586
602
|
});
|
|
@@ -602,6 +618,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
602
618
|
manager.abortAll();
|
|
603
619
|
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
604
620
|
pendingNudges.clear();
|
|
621
|
+
deferredUiRefresh.dispose();
|
|
605
622
|
fleet.dispose();
|
|
606
623
|
manager.dispose();
|
|
607
624
|
});
|
|
@@ -614,13 +631,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
614
631
|
let widgetMode: WidgetMode = "background";
|
|
615
632
|
function getWidgetMode(): WidgetMode { return widgetMode; }
|
|
616
633
|
const widget = new AgentWidget(manager, agentActivity, getWidgetMode);
|
|
617
|
-
function setWidgetMode(m: WidgetMode): void {
|
|
634
|
+
function setWidgetMode(m: WidgetMode): void {
|
|
635
|
+
const changed = widgetMode !== m;
|
|
636
|
+
widgetMode = m;
|
|
637
|
+
widget.update();
|
|
638
|
+
if (changed) scheduleUiRefresh();
|
|
639
|
+
}
|
|
618
640
|
|
|
619
641
|
// Claude Code-style FleetView: navigable list of main + subagents below the editor.
|
|
620
642
|
const fleet = new FleetList(manager, agentActivity, () => currentCtx?.cwd, pi, () => currentCtx);
|
|
643
|
+
|
|
644
|
+
// One render-only scheduler is shared by both widgets. The callback resolves
|
|
645
|
+
// the live TUI target at fire time because session switches invalidate the
|
|
646
|
+
// previous widget context; both widgets use the same TUI, so one successful
|
|
647
|
+
// request is sufficient.
|
|
648
|
+
const deferredUiRefresh = createDeferredUiRefresh(() => {
|
|
649
|
+
if (widget.requestUiRefresh(true)) return;
|
|
650
|
+
fleet.requestUiRefresh(true);
|
|
651
|
+
});
|
|
652
|
+
const scheduleUiRefresh = (): void => deferredUiRefresh.schedule();
|
|
653
|
+
|
|
621
654
|
let fleetViewEnabled = true;
|
|
622
655
|
function isFleetViewEnabled(): boolean { return fleetViewEnabled; }
|
|
623
|
-
function setFleetViewEnabled(b: boolean): void {
|
|
656
|
+
function setFleetViewEnabled(b: boolean): void {
|
|
657
|
+
const changed = fleetViewEnabled !== b;
|
|
658
|
+
fleetViewEnabled = b;
|
|
659
|
+
fleet.setEnabled(b);
|
|
660
|
+
if (changed) scheduleUiRefresh();
|
|
661
|
+
}
|
|
624
662
|
|
|
625
663
|
// Project/global default for writing the subagent .output transcript. A custom
|
|
626
664
|
// agent's `output_transcript` frontmatter overrides this per spawn; when the
|
|
@@ -720,8 +758,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
720
758
|
|
|
721
759
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
722
760
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
723
|
-
widget.setUICtx(ctx.ui as UICtx);
|
|
724
|
-
fleet.setUICtx(ctx.ui as unknown as FleetUICtx);
|
|
761
|
+
const widgetContextChanged = widget.setUICtx(ctx.ui as UICtx);
|
|
762
|
+
const fleetContextChanged = fleet.setUICtx(ctx.ui as unknown as FleetUICtx);
|
|
763
|
+
if (widgetContextChanged || fleetContextChanged) scheduleUiRefresh();
|
|
725
764
|
widget.onTurnStart();
|
|
726
765
|
});
|
|
727
766
|
|
|
@@ -1319,6 +1358,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1319
1358
|
widget.update();
|
|
1320
1359
|
fleet.ensureTimer();
|
|
1321
1360
|
fleet.update();
|
|
1361
|
+
scheduleUiRefresh();
|
|
1322
1362
|
|
|
1323
1363
|
// Emit created event
|
|
1324
1364
|
pi.events.emit("subagents:created", {
|
|
@@ -1379,8 +1419,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1379
1419
|
fgId = a.id;
|
|
1380
1420
|
agentActivity.set(a.id, fgState);
|
|
1381
1421
|
widget.ensureTimer();
|
|
1422
|
+
widget.update();
|
|
1382
1423
|
fleet.ensureTimer();
|
|
1383
1424
|
fleet.update();
|
|
1425
|
+
scheduleUiRefresh();
|
|
1384
1426
|
break;
|
|
1385
1427
|
}
|
|
1386
1428
|
}
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -240,6 +240,8 @@ export class AgentWidget {
|
|
|
240
240
|
private tui: any | undefined;
|
|
241
241
|
/** Last status bar text, used to avoid redundant setStatus calls. */
|
|
242
242
|
private lastStatusText: string | undefined;
|
|
243
|
+
/** Snapshot of the state used for the last widget registration/render request. */
|
|
244
|
+
private lastRenderKey: string | undefined;
|
|
243
245
|
|
|
244
246
|
constructor(
|
|
245
247
|
private manager: AgentManager,
|
|
@@ -273,15 +275,24 @@ export class AgentWidget {
|
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
/** Set the UI context (grabbed from first tool execution). */
|
|
276
|
-
setUICtx(ctx: UICtx) {
|
|
277
|
-
if (ctx
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
278
|
+
setUICtx(ctx: UICtx): boolean {
|
|
279
|
+
if (ctx === this.uiCtx) return false;
|
|
280
|
+
|
|
281
|
+
// UICtx changed — the widget registered on the old context is gone.
|
|
282
|
+
// Force re-registration on next update().
|
|
283
|
+
this.uiCtx = ctx;
|
|
284
|
+
this.widgetRegistered = false;
|
|
285
|
+
this.tui = undefined;
|
|
286
|
+
this.lastStatusText = undefined;
|
|
287
|
+
this.lastRenderKey = undefined;
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Request a render on the currently registered TUI without touching input. */
|
|
292
|
+
requestUiRefresh(force = true): boolean {
|
|
293
|
+
if (!this.tui) return false;
|
|
294
|
+
this.tui.requestRender(force);
|
|
295
|
+
return true;
|
|
285
296
|
}
|
|
286
297
|
|
|
287
298
|
/**
|
|
@@ -297,10 +308,20 @@ export class AgentWidget {
|
|
|
297
308
|
this.update();
|
|
298
309
|
}
|
|
299
310
|
|
|
300
|
-
/**
|
|
311
|
+
/** Keep the spinner/elapsed-time timer alive only while a visible agent runs. */
|
|
301
312
|
ensureTimer() {
|
|
313
|
+
if (!this.uiCtx || !this.widgetAgents().some(a => a.status === "running")) return;
|
|
302
314
|
if (!this.widgetInterval) {
|
|
303
|
-
this.widgetInterval = setInterval(() => this.update(), 80);
|
|
315
|
+
this.widgetInterval = setInterval(() => this.update(true), 80);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private syncTimer(shouldRun: boolean): void {
|
|
320
|
+
if (shouldRun) {
|
|
321
|
+
this.ensureTimer();
|
|
322
|
+
} else if (this.widgetInterval) {
|
|
323
|
+
clearInterval(this.widgetInterval);
|
|
324
|
+
this.widgetInterval = undefined;
|
|
304
325
|
}
|
|
305
326
|
}
|
|
306
327
|
|
|
@@ -505,8 +526,43 @@ export class AgentWidget {
|
|
|
505
526
|
return lines;
|
|
506
527
|
}
|
|
507
528
|
|
|
508
|
-
/**
|
|
509
|
-
|
|
529
|
+
/** Build a render-relevant snapshot without capturing mutable records. */
|
|
530
|
+
private renderKey(allAgents: ReturnType<AgentManager["listAgents"]>): string {
|
|
531
|
+
const activities = allAgents.map(a => {
|
|
532
|
+
const activity = this.agentActivity.get(a.id);
|
|
533
|
+
return activity ? {
|
|
534
|
+
id: a.id,
|
|
535
|
+
activeTools: [...activity.activeTools.entries()],
|
|
536
|
+
toolUses: activity.toolUses,
|
|
537
|
+
responseText: activity.responseText,
|
|
538
|
+
turnCount: activity.turnCount,
|
|
539
|
+
maxTurns: activity.maxTurns,
|
|
540
|
+
lifetimeUsage: activity.lifetimeUsage,
|
|
541
|
+
contextPercent: getSessionContextPercent(activity.session),
|
|
542
|
+
} : undefined;
|
|
543
|
+
});
|
|
544
|
+
return JSON.stringify({
|
|
545
|
+
frame: this.widgetFrame,
|
|
546
|
+
agents: allAgents.map(a => ({
|
|
547
|
+
id: a.id,
|
|
548
|
+
type: a.type,
|
|
549
|
+
description: a.description,
|
|
550
|
+
status: a.status,
|
|
551
|
+
toolUses: a.toolUses,
|
|
552
|
+
startedAt: a.startedAt,
|
|
553
|
+
completedAt: a.completedAt,
|
|
554
|
+
error: a.error,
|
|
555
|
+
lifetimeUsage: a.lifetimeUsage,
|
|
556
|
+
compactionCount: a.compactionCount,
|
|
557
|
+
isBackground: a.isBackground,
|
|
558
|
+
})),
|
|
559
|
+
activities,
|
|
560
|
+
finishedTurnAge: [...this.finishedTurnAge.entries()],
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Force an immediate widget update. `advanceSpinner` is reserved for the timer. */
|
|
565
|
+
update(advanceSpinner = false) {
|
|
510
566
|
if (!this.uiCtx) return;
|
|
511
567
|
const allAgents = this.widgetAgents();
|
|
512
568
|
|
|
@@ -532,7 +588,8 @@ export class AgentWidget {
|
|
|
532
588
|
this.uiCtx.setStatus("subagents", undefined);
|
|
533
589
|
this.lastStatusText = undefined;
|
|
534
590
|
}
|
|
535
|
-
|
|
591
|
+
this.lastRenderKey = undefined;
|
|
592
|
+
this.syncTimer(false);
|
|
536
593
|
// Clean up stale entries
|
|
537
594
|
for (const [id] of this.finishedTurnAge) {
|
|
538
595
|
if (!allAgents.some(a => a.id === id)) this.finishedTurnAge.delete(id);
|
|
@@ -554,7 +611,12 @@ export class AgentWidget {
|
|
|
554
611
|
this.lastStatusText = newStatusText;
|
|
555
612
|
}
|
|
556
613
|
|
|
557
|
-
|
|
614
|
+
// Spinner animation is driven only by the timer while a visible running
|
|
615
|
+
// agent exists. Event-driven updates refresh changed stats without making
|
|
616
|
+
// an otherwise idle widget advance.
|
|
617
|
+
if (advanceSpinner && runningCount > 0) this.widgetFrame++;
|
|
618
|
+
this.syncTimer(runningCount > 0);
|
|
619
|
+
const renderKey = this.renderKey(allAgents);
|
|
558
620
|
|
|
559
621
|
// Register widget callback once; subsequent updates use requestRender()
|
|
560
622
|
// which re-invokes render() without replacing the component (avoids layout thrashing).
|
|
@@ -567,14 +629,17 @@ export class AgentWidget {
|
|
|
567
629
|
// Theme changed — force re-registration so factory captures fresh theme.
|
|
568
630
|
this.widgetRegistered = false;
|
|
569
631
|
this.tui = undefined;
|
|
632
|
+
this.lastRenderKey = undefined;
|
|
570
633
|
},
|
|
571
634
|
};
|
|
572
635
|
}, { placement: "aboveEditor" });
|
|
573
636
|
this.widgetRegistered = true;
|
|
574
|
-
} else {
|
|
575
|
-
// Widget already registered —
|
|
637
|
+
} else if (renderKey !== this.lastRenderKey) {
|
|
638
|
+
// Widget already registered — request a re-render only when visible state
|
|
639
|
+
// changed (or the spinner timer advanced).
|
|
576
640
|
this.tui?.requestRender();
|
|
577
641
|
}
|
|
642
|
+
this.lastRenderKey = renderKey;
|
|
578
643
|
}
|
|
579
644
|
|
|
580
645
|
dispose() {
|
|
@@ -589,5 +654,8 @@ export class AgentWidget {
|
|
|
589
654
|
this.widgetRegistered = false;
|
|
590
655
|
this.tui = undefined;
|
|
591
656
|
this.lastStatusText = undefined;
|
|
657
|
+
this.lastRenderKey = undefined;
|
|
658
|
+
this.uiCtx = undefined;
|
|
659
|
+
this.finishedTurnAge.clear();
|
|
592
660
|
}
|
|
593
661
|
}
|
|
@@ -300,23 +300,25 @@ export function formatConversationMessages(
|
|
|
300
300
|
const blocks: ConversationBlock[] = [];
|
|
301
301
|
const usedBlockIds = new Set<string>();
|
|
302
302
|
const callsById = new Map<string, ConversationBlock>();
|
|
303
|
-
const callsByName = new Map<string, ConversationBlock[]>();
|
|
303
|
+
const callsByName = new Map<string, { blocks: ConversationBlock[]; next: number }>();
|
|
304
304
|
|
|
305
305
|
const registerCall = (name: string, block: ConversationBlock) => {
|
|
306
306
|
callsById.set(block.toolCallId!, block);
|
|
307
307
|
const nameKey = name.toLowerCase();
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
callsByName.set(nameKey,
|
|
308
|
+
const queue = callsByName.get(nameKey) ?? { blocks: [], next: 0 };
|
|
309
|
+
queue.blocks.push(block);
|
|
310
|
+
callsByName.set(nameKey, queue);
|
|
311
311
|
};
|
|
312
312
|
const unregisterCall = (block: ConversationBlock) => {
|
|
313
|
+
// Calls remain in their name queue as tombstones. Advancing its cursor is
|
|
314
|
+
// amortized O(1), unlike removing from the middle of a same-name array.
|
|
313
315
|
if (block.toolCallId) callsById.delete(block.toolCallId);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
};
|
|
317
|
+
const nextCallByName = (name: string): ConversationBlock | undefined => {
|
|
318
|
+
const queue = callsByName.get(name.toLowerCase());
|
|
319
|
+
if (!queue) return undefined;
|
|
320
|
+
while (queue.next < queue.blocks.length && queue.blocks[queue.next]?.toolStatus !== "pending") queue.next++;
|
|
321
|
+
return queue.blocks[queue.next];
|
|
320
322
|
};
|
|
321
323
|
|
|
322
324
|
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
@@ -368,7 +370,7 @@ export function formatConversationMessages(
|
|
|
368
370
|
const direct = result.toolCallId ? callsById.get(result.toolCallId) : undefined;
|
|
369
371
|
let match = direct;
|
|
370
372
|
if (!match && result.toolName) {
|
|
371
|
-
match =
|
|
373
|
+
match = nextCallByName(result.toolName);
|
|
372
374
|
}
|
|
373
375
|
if (match) {
|
|
374
376
|
applyToolResult(match, result, maxToolResultChars);
|