@oh-my-pi/pi-coding-agent 16.3.8 → 16.3.10
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 +37 -0
- package/dist/cli.js +3050 -3048
- package/dist/types/commands/say.d.ts +6 -1
- package/dist/types/commit/agentic/lock-files.d.ts +36 -0
- package/dist/types/config/model-discovery.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +5 -3
- package/dist/types/modes/session-observer-registry.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +11 -7
- package/dist/types/task/renderer.d.ts +0 -1
- package/dist/types/tools/browser/run-cancellation.d.ts +11 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +1 -0
- package/dist/types/tools/tool-errors.d.ts +1 -1
- package/package.json +12 -12
- package/src/commands/say.ts +73 -30
- package/src/commit/agentic/index.ts +3 -1
- package/src/commit/agentic/lock-files.ts +107 -0
- package/src/commit/agentic/tools/git-overview.ts +1 -20
- package/src/config/model-discovery.ts +6 -3
- package/src/config/model-registry.ts +18 -7
- package/src/discovery/claude.ts +12 -8
- package/src/extensibility/skills.ts +8 -5
- package/src/modes/components/__tests__/skill-message.test.ts +2 -0
- package/src/modes/components/agent-hub.ts +17 -2
- package/src/modes/components/skill-message.ts +1 -1
- package/src/modes/index.ts +0 -7
- package/src/modes/interactive-mode.ts +54 -18
- package/src/modes/session-observer-registry.ts +11 -8
- package/src/sdk.ts +18 -0
- package/src/session/agent-session.ts +25 -13
- package/src/task/render.test.ts +134 -1
- package/src/task/render.ts +24 -6
- package/src/task/renderer.ts +0 -1
- package/src/tools/browser/cmux/cmux-tab.ts +7 -3
- package/src/tools/browser/run-cancellation.ts +28 -8
- package/src/tools/browser/tab-protocol.ts +1 -1
- package/src/tools/browser/tab-supervisor.ts +4 -4
- package/src/tools/browser/tab-worker.ts +19 -7
- package/src/tools/irc.ts +18 -2
- package/src/tools/tool-errors.ts +3 -3
- package/src/tts/speakable.ts +19 -9
package/src/discovery/claude.ts
CHANGED
|
@@ -167,17 +167,21 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
|
|
|
167
167
|
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
168
168
|
const userSkillsDir = path.join(getUserClaude(ctx), "skills");
|
|
169
169
|
|
|
170
|
-
// Walk up from cwd finding .claude/skills/ in ancestors
|
|
170
|
+
// Walk up from cwd finding .claude/skills/ in ancestors. Skip $HOME:
|
|
171
|
+
// that path is already scanned as the Claude user source below, and scanning
|
|
172
|
+
// it again as project would bypass enableClaudeUser when project skills stay enabled.
|
|
171
173
|
const projectScans: Promise<LoadResult<Skill>>[] = [];
|
|
172
174
|
let current = ctx.cwd;
|
|
173
175
|
while (true) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
if (current !== ctx.home) {
|
|
177
|
+
projectScans.push(
|
|
178
|
+
scanSkillsFromDir(ctx, {
|
|
179
|
+
dir: path.join(current, CONFIG_DIR, "skills"),
|
|
180
|
+
providerId: PROVIDER_ID,
|
|
181
|
+
level: "project",
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
181
185
|
if (current === (ctx.repoRoot ?? ctx.home)) break;
|
|
182
186
|
const parent = path.dirname(current);
|
|
183
187
|
if (parent === current) break; // filesystem root
|
|
@@ -190,12 +190,18 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
190
190
|
const disabledSkillNames = new Set(
|
|
191
191
|
(disabledExtensions ?? []).filter(id => id.startsWith("skill:")).map(id => id.slice(6)),
|
|
192
192
|
);
|
|
193
|
-
//
|
|
194
|
-
|
|
193
|
+
// Select authored skills from the pre-dedup superset. `loadCapability`
|
|
194
|
+
// dedupes before source toggles, so a disabled high-priority provider must
|
|
195
|
+
// not hide an enabled lower-priority provider with the same skill name.
|
|
196
|
+
const seenAuthoredSkillNames = new Set<string>();
|
|
197
|
+
const filteredSkills = result.all.filter(capSkill => {
|
|
198
|
+
if (capSkill._source.provider === MANAGED_SKILLS_PROVIDER_ID) return false;
|
|
195
199
|
if (disabledSkillNames.has(capSkill.name)) return false;
|
|
196
200
|
if (!isSourceEnabled(capSkill._source)) return false;
|
|
197
201
|
if (matchesIgnorePatterns(capSkill.name)) return false;
|
|
198
202
|
if (!matchesIncludePatterns(capSkill.name)) return false;
|
|
203
|
+
if (seenAuthoredSkillNames.has(capSkill.name)) return false;
|
|
204
|
+
seenAuthoredSkillNames.add(capSkill.name);
|
|
199
205
|
return true;
|
|
200
206
|
});
|
|
201
207
|
|
|
@@ -213,9 +219,6 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
213
219
|
// Process skills with resolved paths
|
|
214
220
|
for (let i = 0; i < filteredSkills.length; i++) {
|
|
215
221
|
const capSkill = filteredSkills[i];
|
|
216
|
-
// Managed (auto-learn) skills are resolved dead-last (below) so any
|
|
217
|
-
// authored skill of the same name — from ANY provider or custom dir — wins.
|
|
218
|
-
if (capSkill._source.provider === MANAGED_SKILLS_PROVIDER_ID) continue;
|
|
219
222
|
const resolvedPath = realPaths[i];
|
|
220
223
|
|
|
221
224
|
// Skip silently if we've already loaded this exact file (via symlink)
|
|
@@ -42,6 +42,8 @@ describe("SkillMessageComponent", () => {
|
|
|
42
42
|
// New look: an icon-tagged "skill" header with the name and a single meta line.
|
|
43
43
|
expect(text).toContain("skill");
|
|
44
44
|
expect(text).toContain("atomic-commit");
|
|
45
|
+
expect(text).toContain("skill atomic-commit");
|
|
46
|
+
expect(text).not.toContain("skill atomic-commit");
|
|
45
47
|
expect(text).toContain("88 lines");
|
|
46
48
|
|
|
47
49
|
// The card is drawn with an outline.
|
|
@@ -34,6 +34,7 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
34
34
|
|
|
35
35
|
/** Refresh cadence for the relative-time column */
|
|
36
36
|
const AGE_TICK_MS = 5_000;
|
|
37
|
+
const DATA_CHANGE_RENDER_COALESCE_MS = 100;
|
|
37
38
|
/** Double-tap window for the table's left-left "close hub" gesture. */
|
|
38
39
|
const LEFT_TAP_WINDOW_MS = 500;
|
|
39
40
|
|
|
@@ -198,6 +199,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
198
199
|
#hubKeys: KeyId[];
|
|
199
200
|
#unsubscribers: Array<() => void> = [];
|
|
200
201
|
#ageTimer: NodeJS.Timeout | undefined;
|
|
202
|
+
#dataChangeTimer?: NodeJS.Timeout;
|
|
201
203
|
#remote: AgentHubRemote | undefined;
|
|
202
204
|
/** Resolves after persisted historical subagents have been registered and rows refreshed. */
|
|
203
205
|
readonly persistedSubagentsReady: Promise<void>;
|
|
@@ -251,8 +253,8 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
251
253
|
this.#expandKeys = deps.expandKeys ?? ["ctrl+o"];
|
|
252
254
|
this.#focusAgent = deps.focusAgent;
|
|
253
255
|
|
|
254
|
-
this.#unsubscribers.push(this.#registry.onChange(() => this.#
|
|
255
|
-
this.#unsubscribers.push(this.#observers.onChange(() => this.#
|
|
256
|
+
this.#unsubscribers.push(this.#registry.onChange(() => this.#scheduleDataChange()));
|
|
257
|
+
this.#unsubscribers.push(this.#observers.onChange(() => this.#scheduleDataChange()));
|
|
256
258
|
this.#ageTimer = setInterval(() => this.#requestRender(), AGE_TICK_MS);
|
|
257
259
|
this.#ageTimer.unref?.();
|
|
258
260
|
|
|
@@ -285,6 +287,10 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
285
287
|
clearInterval(this.#ageTimer);
|
|
286
288
|
this.#ageTimer = undefined;
|
|
287
289
|
}
|
|
290
|
+
if (this.#dataChangeTimer) {
|
|
291
|
+
clearTimeout(this.#dataChangeTimer);
|
|
292
|
+
this.#dataChangeTimer = undefined;
|
|
293
|
+
}
|
|
288
294
|
this.#closeTranscriptOverlay();
|
|
289
295
|
}
|
|
290
296
|
|
|
@@ -355,6 +361,15 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
355
361
|
// Live data plumbing
|
|
356
362
|
// ========================================================================
|
|
357
363
|
|
|
364
|
+
#scheduleDataChange(): void {
|
|
365
|
+
if (this.#dataChangeTimer) return;
|
|
366
|
+
this.#dataChangeTimer = setTimeout(() => {
|
|
367
|
+
this.#dataChangeTimer = undefined;
|
|
368
|
+
this.#onDataChange();
|
|
369
|
+
}, DATA_CHANGE_RENDER_COALESCE_MS);
|
|
370
|
+
this.#dataChangeTimer.unref?.();
|
|
371
|
+
}
|
|
372
|
+
|
|
358
373
|
#onDataChange(): void {
|
|
359
374
|
this.#refreshRows();
|
|
360
375
|
this.#requestRender();
|
|
@@ -50,7 +50,7 @@ export class SkillMessageComponent extends Container {
|
|
|
50
50
|
|
|
51
51
|
// Header: icon-tag + skill name, with the invocation args trailing dimmed.
|
|
52
52
|
const tag = theme.fg("customMessageLabel", theme.bold(`${theme.icon.extensionSkill} skill`));
|
|
53
|
-
let header = `${tag}
|
|
53
|
+
let header = `${tag} ${theme.fg("customMessageText", theme.bold(name))}`;
|
|
54
54
|
if (args) {
|
|
55
55
|
header += ` ${theme.fg("dim", args)}`;
|
|
56
56
|
}
|
package/src/modes/index.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { emergencyTerminalRestore } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Interactive mode and embeddable RPC client exports for the coding agent.
|
|
6
3
|
*
|
|
@@ -11,7 +8,3 @@ import { postmortem } from "@oh-my-pi/pi-utils";
|
|
|
11
8
|
export * from "./interactive-mode";
|
|
12
9
|
export * from "./rpc/rpc-client";
|
|
13
10
|
export * from "./rpc/rpc-types";
|
|
14
|
-
|
|
15
|
-
postmortem.register("terminal-restore", () => {
|
|
16
|
-
emergencyTerminalRestore();
|
|
17
|
-
});
|
|
@@ -161,8 +161,11 @@ import {
|
|
|
161
161
|
} from "./loop-limit";
|
|
162
162
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
163
163
|
import { countRunningSubagentBadgeAgents, getRunningSubagentBadgeRegistry } from "./running-subagent-badge";
|
|
164
|
-
import
|
|
165
|
-
|
|
164
|
+
import {
|
|
165
|
+
type ObservableSession,
|
|
166
|
+
type SessionObserverChangeKind,
|
|
167
|
+
SessionObserverRegistry,
|
|
168
|
+
} from "./session-observer-registry";
|
|
166
169
|
import { createSessionTeardown, type SessionTeardown } from "./session-teardown";
|
|
167
170
|
import { runProviderSetupWizard } from "./setup-wizard/lazy";
|
|
168
171
|
import { interruptHint } from "./shared";
|
|
@@ -343,9 +346,12 @@ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveReg
|
|
|
343
346
|
* before it auto-clears, mirroring the todo HUD's auto-clear timer. */
|
|
344
347
|
const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
|
|
345
348
|
|
|
349
|
+
const SUBAGENT_HUD_VISIBLE_LIMIT = 8;
|
|
350
|
+
const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
|
|
351
|
+
|
|
346
352
|
/**
|
|
347
353
|
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
348
|
-
*
|
|
354
|
+
* a bounded set of running-agent rows in the same `Id: description` shape the
|
|
349
355
|
* inline task rows use (muted task preview when no description was given).
|
|
350
356
|
* Layout mirrors the Todos HUD exactly: unindented header, then
|
|
351
357
|
* `renderTreeList` rows (dim connectors) shifted right by one space.
|
|
@@ -361,9 +367,11 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
|
|
|
361
367
|
if (running.length === 0) return [];
|
|
362
368
|
|
|
363
369
|
const dot = theme.styledSymbol("status.done", "accent");
|
|
370
|
+
const visible = running.slice(0, SUBAGENT_HUD_VISIBLE_LIMIT);
|
|
371
|
+
const hiddenCount = running.length - visible.length;
|
|
364
372
|
const rows = renderTreeList(
|
|
365
373
|
{
|
|
366
|
-
items:
|
|
374
|
+
items: visible,
|
|
367
375
|
expanded: true,
|
|
368
376
|
renderItem: session => {
|
|
369
377
|
const displayId = formatTaskId(session.id);
|
|
@@ -385,6 +393,9 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
|
|
|
385
393
|
},
|
|
386
394
|
theme,
|
|
387
395
|
);
|
|
396
|
+
if (hiddenCount > 0) {
|
|
397
|
+
rows.push(theme.fg("dim", `… ${hiddenCount} more running — open Agent Hub for full list`));
|
|
398
|
+
}
|
|
388
399
|
return ["", theme.bold(theme.fg("accent", "Subagents")), ...rows.map(line => ` ${line}`)];
|
|
389
400
|
}
|
|
390
401
|
|
|
@@ -588,6 +599,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
588
599
|
#observerRegistry: SessionObserverRegistry;
|
|
589
600
|
#eventBus?: EventBus;
|
|
590
601
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
602
|
+
#observerUiSyncTimer?: NodeJS.Timeout;
|
|
603
|
+
#observerUiSyncNeedsTodoReconcile = false;
|
|
591
604
|
#agentRegistryUnsubscribe?: () => void;
|
|
592
605
|
#agentRegistrySubscriptionTarget?: AgentRegistry;
|
|
593
606
|
#mcpStatusOrder: string[] = [];
|
|
@@ -917,17 +930,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
917
930
|
}
|
|
918
931
|
this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
|
|
919
932
|
this.syncRunningSubagentBadge();
|
|
920
|
-
this.#observerRegistry.onChange(
|
|
921
|
-
this
|
|
922
|
-
// Auto-checkmark todos whose matching subagent just succeeded, then
|
|
923
|
-
// re-render so the running override (the static "live" glyph when a
|
|
924
|
-
// subagent is doing the work for a still-pending todo) updates as
|
|
925
|
-
// subagents start, finish, or fail.
|
|
926
|
-
this.#reconcileTodosWithSubagents();
|
|
927
|
-
this.#syncTodoAutoClearTimer();
|
|
928
|
-
this.#renderTodoList();
|
|
929
|
-
this.#renderSubagentList();
|
|
930
|
-
this.ui.requestRender();
|
|
933
|
+
this.#observerRegistry.onChange(kind => {
|
|
934
|
+
this.#scheduleObserverUiSync(kind);
|
|
931
935
|
});
|
|
932
936
|
|
|
933
937
|
// Load initial todos
|
|
@@ -1521,19 +1525,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1521
1525
|
}
|
|
1522
1526
|
|
|
1523
1527
|
/** Refresh the running-subagents status badge from the active local or collab registry. */
|
|
1524
|
-
syncRunningSubagentBadge(): void {
|
|
1528
|
+
syncRunningSubagentBadge(options: { requestRender?: boolean } = {}): void {
|
|
1525
1529
|
const registry = getRunningSubagentBadgeRegistry(this.collabGuest);
|
|
1526
1530
|
if (this.#agentRegistrySubscriptionTarget !== registry) {
|
|
1527
1531
|
this.#agentRegistryUnsubscribe?.();
|
|
1528
1532
|
this.#agentRegistrySubscriptionTarget = registry;
|
|
1529
1533
|
this.#agentRegistryUnsubscribe = registry.onChange(() => {
|
|
1530
1534
|
this.syncRunningSubagentBadge();
|
|
1531
|
-
this.ui.requestRender();
|
|
1532
1535
|
});
|
|
1533
1536
|
}
|
|
1534
1537
|
const count = countRunningSubagentBadgeAgents(registry);
|
|
1535
1538
|
this.statusLine.setSubagentCount(count);
|
|
1536
|
-
this.ui.requestRender();
|
|
1539
|
+
if (options.requestRender !== false) this.ui.requestRender();
|
|
1537
1540
|
}
|
|
1538
1541
|
|
|
1539
1542
|
rebuildChatFromMessages(): void {
|
|
@@ -1754,6 +1757,38 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1754
1757
|
return active ?? nonEmpty[nonEmpty.length - 1];
|
|
1755
1758
|
}
|
|
1756
1759
|
|
|
1760
|
+
#scheduleObserverUiSync(kind: SessionObserverChangeKind): void {
|
|
1761
|
+
if (kind !== "progress") {
|
|
1762
|
+
this.#observerUiSyncNeedsTodoReconcile = true;
|
|
1763
|
+
}
|
|
1764
|
+
if (this.#observerUiSyncTimer) return;
|
|
1765
|
+
this.#observerUiSyncTimer = setTimeout(() => {
|
|
1766
|
+
this.#observerUiSyncTimer = undefined;
|
|
1767
|
+
this.#flushObserverUiSync();
|
|
1768
|
+
}, SUBAGENT_OBSERVER_UI_COALESCE_MS);
|
|
1769
|
+
this.#observerUiSyncTimer.unref?.();
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
#flushObserverUiSync(): void {
|
|
1773
|
+
this.syncRunningSubagentBadge({ requestRender: false });
|
|
1774
|
+
if (this.#observerUiSyncNeedsTodoReconcile) {
|
|
1775
|
+
this.#observerUiSyncNeedsTodoReconcile = false;
|
|
1776
|
+
this.#reconcileTodosWithSubagents();
|
|
1777
|
+
}
|
|
1778
|
+
this.#syncTodoAutoClearTimer();
|
|
1779
|
+
this.#renderTodoList();
|
|
1780
|
+
this.#renderSubagentList();
|
|
1781
|
+
this.ui.requestRender();
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
#cancelObserverUiSyncTimer(): void {
|
|
1785
|
+
if (this.#observerUiSyncTimer) {
|
|
1786
|
+
clearTimeout(this.#observerUiSyncTimer);
|
|
1787
|
+
this.#observerUiSyncTimer = undefined;
|
|
1788
|
+
}
|
|
1789
|
+
this.#observerUiSyncNeedsTodoReconcile = false;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1757
1792
|
#renderTodoList(): void {
|
|
1758
1793
|
this.todoContainer.clear();
|
|
1759
1794
|
const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
|
|
@@ -3306,6 +3341,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3306
3341
|
}
|
|
3307
3342
|
this.#cleanupMicAnimation();
|
|
3308
3343
|
this.#cancelTodoAutoClearTimer();
|
|
3344
|
+
this.#cancelObserverUiSyncTimer();
|
|
3309
3345
|
this.#cancelGoalContinuation();
|
|
3310
3346
|
if (this.#sttController) {
|
|
3311
3347
|
this.#sttController.dispose();
|
|
@@ -24,6 +24,9 @@ export interface ObservableSession {
|
|
|
24
24
|
progress?: AgentProgress;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
|
|
28
|
+
export type SessionObserverChangeKind = "main" | "reset" | "lifecycle" | "progress";
|
|
29
|
+
|
|
27
30
|
const STATUS_MAP: Record<string, ObservableSession["status"]> = {
|
|
28
31
|
started: "active",
|
|
29
32
|
completed: "completed",
|
|
@@ -33,20 +36,20 @@ const STATUS_MAP: Record<string, ObservableSession["status"]> = {
|
|
|
33
36
|
|
|
34
37
|
export class SessionObserverRegistry {
|
|
35
38
|
#sessions = new Map<string, ObservableSession>();
|
|
36
|
-
#listeners = new Set<() => void>();
|
|
39
|
+
#listeners = new Set<(kind: SessionObserverChangeKind) => void>();
|
|
37
40
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
38
41
|
#sortOrderById = new Map<string, number>();
|
|
39
42
|
#parentSortOrderById = new Map<string, number>();
|
|
40
43
|
#nextSortOrder = 0;
|
|
41
44
|
|
|
42
45
|
/** Add a change listener. Returns unsubscribe function. */
|
|
43
|
-
onChange(cb: () => void): () => void {
|
|
46
|
+
onChange(cb: (kind: SessionObserverChangeKind) => void): () => void {
|
|
44
47
|
this.#listeners.add(cb);
|
|
45
48
|
return () => this.#listeners.delete(cb);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
#notifyListeners(): void {
|
|
49
|
-
for (const cb of this.#listeners) cb();
|
|
51
|
+
#notifyListeners(kind: SessionObserverChangeKind): void {
|
|
52
|
+
for (const cb of this.#listeners) cb(kind);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
#ensureSortOrder(id: string): number {
|
|
@@ -85,7 +88,7 @@ export class SessionObserverRegistry {
|
|
|
85
88
|
sessionFile: sessionFile ?? existing?.sessionFile,
|
|
86
89
|
lastUpdate: Date.now(),
|
|
87
90
|
});
|
|
88
|
-
this.#notifyListeners();
|
|
91
|
+
this.#notifyListeners("main");
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
getSessions(): ObservableSession[] {
|
|
@@ -121,7 +124,7 @@ export class SessionObserverRegistry {
|
|
|
121
124
|
this.#sortOrderById.clear();
|
|
122
125
|
this.#parentSortOrderById.clear();
|
|
123
126
|
this.#nextSortOrder = 0;
|
|
124
|
-
this.#notifyListeners();
|
|
127
|
+
this.#notifyListeners("reset");
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
dispose(): void {
|
|
@@ -171,7 +174,7 @@ export class SessionObserverRegistry {
|
|
|
171
174
|
lastUpdate: Date.now(),
|
|
172
175
|
});
|
|
173
176
|
}
|
|
174
|
-
this.#notifyListeners();
|
|
177
|
+
this.#notifyListeners("lifecycle");
|
|
175
178
|
}),
|
|
176
179
|
);
|
|
177
180
|
|
|
@@ -208,7 +211,7 @@ export class SessionObserverRegistry {
|
|
|
208
211
|
progress,
|
|
209
212
|
});
|
|
210
213
|
}
|
|
211
|
-
this.#notifyListeners();
|
|
214
|
+
this.#notifyListeners("progress");
|
|
212
215
|
}),
|
|
213
216
|
);
|
|
214
217
|
}
|
package/src/sdk.ts
CHANGED
|
@@ -2132,6 +2132,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2132
2132
|
}
|
|
2133
2133
|
}
|
|
2134
2134
|
|
|
2135
|
+
if (model) {
|
|
2136
|
+
const selectedModel = model;
|
|
2137
|
+
const refreshedModel = await logger.time("refreshInitialModelMetadata", () =>
|
|
2138
|
+
modelRegistry.refreshSelectedModelMetadata(selectedModel),
|
|
2139
|
+
);
|
|
2140
|
+
if (refreshedModel !== selectedModel) {
|
|
2141
|
+
model = refreshedModel;
|
|
2142
|
+
thinkingLevel = pickInitialThinkingLevel(refreshedModel);
|
|
2143
|
+
autoThinking = thinkingLevel === AUTO_THINKING;
|
|
2144
|
+
effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
|
|
2145
|
+
effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
|
|
2146
|
+
autoThinking
|
|
2147
|
+
? resolveProvisionalAutoLevel(refreshedModel)
|
|
2148
|
+
: resolveThinkingLevelForModel(refreshedModel, effectiveThinkingLevel),
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2135
2153
|
// Discover custom commands (TypeScript slash commands)
|
|
2136
2154
|
const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
|
|
2137
2155
|
? { commands: [], errors: [] }
|
|
@@ -933,6 +933,7 @@ export interface SessionStats {
|
|
|
933
933
|
};
|
|
934
934
|
premiumRequests: number;
|
|
935
935
|
cost: number;
|
|
936
|
+
contextUsage?: ContextUsage;
|
|
936
937
|
}
|
|
937
938
|
|
|
938
939
|
/** Advisor statistics for /advisor status command. */
|
|
@@ -1824,13 +1825,19 @@ export class AgentSession {
|
|
|
1824
1825
|
* Sticky across an in-flight prompt run: a successful `yield` makes the run
|
|
1825
1826
|
* terminal for execution purposes, so any trailing empty/aborted assistant
|
|
1826
1827
|
* stop must NOT trigger empty-stop/unexpected-stop/compaction continuations.
|
|
1827
|
-
* Cleared
|
|
1828
|
+
* Cleared before every new prompt turn so the next turn evaluates cleanly.
|
|
1828
1829
|
*/
|
|
1829
1830
|
#yieldTerminationPending = false;
|
|
1830
1831
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
1831
1832
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
1832
1833
|
readonly rawSseDebugBuffer: RawSseDebugBuffer;
|
|
1833
1834
|
|
|
1835
|
+
#resetPromptMaintenanceState(): void {
|
|
1836
|
+
this.#emptyStopRetryCount = 0;
|
|
1837
|
+
this.#unexpectedStopRetryCount = 0;
|
|
1838
|
+
this.#yieldTerminationPending = false;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1834
1841
|
#acquirePowerAssertion(): void {
|
|
1835
1842
|
if (process.platform !== "darwin") return;
|
|
1836
1843
|
if (isBunTestRuntime()) return;
|
|
@@ -1949,6 +1956,7 @@ export class AgentSession {
|
|
|
1949
1956
|
if (parkedFollowUps.length > 0) {
|
|
1950
1957
|
this.agent.replaceQueues([...this.agent.peekSteeringQueue()], []);
|
|
1951
1958
|
}
|
|
1959
|
+
this.#resetPromptMaintenanceState();
|
|
1952
1960
|
this.#beginInFlight();
|
|
1953
1961
|
void this.agent
|
|
1954
1962
|
.prompt(records)
|
|
@@ -7566,12 +7574,7 @@ export class AgentSession {
|
|
|
7566
7574
|
this.#todoReminderAwaitingProgress = false;
|
|
7567
7575
|
this.#mutationsSinceLastTodoTouch = 0;
|
|
7568
7576
|
this.#midRunNudgeCount = 0;
|
|
7569
|
-
this.#
|
|
7570
|
-
this.#unexpectedStopRetryCount = 0;
|
|
7571
|
-
// A new prompt cycle starts: drop any sticky yield-termination from the
|
|
7572
|
-
// previous run so empty-stop / unexpected-stop / compaction maintenance
|
|
7573
|
-
// can evaluate this turn normally.
|
|
7574
|
-
this.#yieldTerminationPending = false;
|
|
7577
|
+
this.#resetPromptMaintenanceState();
|
|
7575
7578
|
|
|
7576
7579
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
7577
7580
|
|
|
@@ -14120,15 +14123,15 @@ export class AgentSession {
|
|
|
14120
14123
|
// =========================================================================
|
|
14121
14124
|
|
|
14122
14125
|
/**
|
|
14123
|
-
* Surfaces
|
|
14126
|
+
* Surfaces and consumes pending IRC incoming records before the next model
|
|
14124
14127
|
* step can inject them automatically.
|
|
14125
14128
|
*
|
|
14126
|
-
*
|
|
14127
|
-
*
|
|
14128
|
-
*
|
|
14129
|
-
*
|
|
14129
|
+
* Tool results already expose the formatted body to the model. Leaving the
|
|
14130
|
+
* same record in either pending IRC queue would deliver it a second time at
|
|
14131
|
+
* the next step boundary — including on `peek`, which is why inbox peeks
|
|
14132
|
+
* also drain here.
|
|
14130
14133
|
*/
|
|
14131
|
-
drainPendingIrcInboxMessages(agentId: string): IrcMessage[] {
|
|
14134
|
+
drainPendingIrcInboxMessages(agentId: string, opts?: { from?: string; limit?: number }): IrcMessage[] {
|
|
14132
14135
|
const messages: IrcMessage[] = [];
|
|
14133
14136
|
const remainingInterrupts: CustomMessage[] = [];
|
|
14134
14137
|
const remainingAsides: CustomMessage[] = [];
|
|
@@ -14155,6 +14158,14 @@ export class AgentSession {
|
|
|
14155
14158
|
queue.remaining.push(record);
|
|
14156
14159
|
continue;
|
|
14157
14160
|
}
|
|
14161
|
+
if (opts?.from !== undefined && from !== opts.from) {
|
|
14162
|
+
queue.remaining.push(record);
|
|
14163
|
+
continue;
|
|
14164
|
+
}
|
|
14165
|
+
if (opts?.limit !== undefined && messages.length >= opts.limit) {
|
|
14166
|
+
queue.remaining.push(record);
|
|
14167
|
+
continue;
|
|
14168
|
+
}
|
|
14158
14169
|
messages.push({
|
|
14159
14170
|
id,
|
|
14160
14171
|
from,
|
|
@@ -15212,6 +15223,7 @@ export class AgentSession {
|
|
|
15212
15223
|
},
|
|
15213
15224
|
cost: totalCost,
|
|
15214
15225
|
premiumRequests: totalPremiumRequests,
|
|
15226
|
+
contextUsage: this.getContextUsage(),
|
|
15215
15227
|
};
|
|
15216
15228
|
}
|
|
15217
15229
|
|
package/src/task/render.test.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { Settings } from "../config/settings";
|
|
3
3
|
import { getThemeByName, setThemeInstance, type Theme } from "../modes/theme/theme";
|
|
4
4
|
import { renderResult } from "./render";
|
|
5
|
-
import
|
|
5
|
+
import { taskToolRenderer } from "./renderer";
|
|
6
|
+
import type { AgentProgress, SingleResult, TaskToolDetails } from "./types";
|
|
6
7
|
|
|
7
8
|
const strip = (lines: readonly string[]): string =>
|
|
8
9
|
lines
|
|
@@ -42,6 +43,71 @@ function makeProgress(recentOutput: string[]): AgentProgress {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function makeParentWithNestedProgress(childCount: number): AgentProgress {
|
|
47
|
+
const children = Array.from(
|
|
48
|
+
{ length: childCount },
|
|
49
|
+
(_, index): AgentProgress => ({
|
|
50
|
+
...makeProgress([]),
|
|
51
|
+
index,
|
|
52
|
+
id: `Nested${index + 1}`,
|
|
53
|
+
task: `nested child ${index + 1}`,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
...makeProgress([]),
|
|
58
|
+
id: "Parent",
|
|
59
|
+
task: "parent task",
|
|
60
|
+
inflightTaskDetails: {
|
|
61
|
+
projectAgentsDir: null,
|
|
62
|
+
results: [],
|
|
63
|
+
totalDurationMs: 1,
|
|
64
|
+
progress: children,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeSingleResult(index: number, overrides?: Partial<SingleResult>): SingleResult {
|
|
70
|
+
return {
|
|
71
|
+
index,
|
|
72
|
+
id: `Done${index + 1}`,
|
|
73
|
+
agent: "task",
|
|
74
|
+
agentSource: "bundled",
|
|
75
|
+
task: `finished child ${index + 1}`,
|
|
76
|
+
exitCode: 0,
|
|
77
|
+
output: "ok",
|
|
78
|
+
stderr: "",
|
|
79
|
+
truncated: false,
|
|
80
|
+
durationMs: 5,
|
|
81
|
+
tokens: 0,
|
|
82
|
+
requests: 1,
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeParentWithNestedResults(childCount: number): TaskToolDetails {
|
|
88
|
+
const nested: TaskToolDetails = {
|
|
89
|
+
projectAgentsDir: null,
|
|
90
|
+
// Failure on the LAST child in display order (equal durations, index tiebreak):
|
|
91
|
+
// a plain head-of-list pick would elide it, so its visibility proves the
|
|
92
|
+
// failure-first selection of `selectCollapsedResults`.
|
|
93
|
+
results: Array.from({ length: childCount }, (_, index) =>
|
|
94
|
+
makeSingleResult(index, index === childCount - 1 ? { exitCode: 1, error: "boom" } : undefined),
|
|
95
|
+
),
|
|
96
|
+
totalDurationMs: 1,
|
|
97
|
+
};
|
|
98
|
+
const parent = makeSingleResult(0, { id: "Parent", extractedToolData: { task: [nested] } });
|
|
99
|
+
return { projectAgentsDir: null, results: [parent], totalDurationMs: 1 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderResultText(details: TaskToolDetails, expanded: boolean, uiTheme: Theme): string {
|
|
103
|
+
const component = renderResult(
|
|
104
|
+
{ content: [{ type: "text", text: "Ran 1 agent" }], details },
|
|
105
|
+
{ expanded, isPartial: false },
|
|
106
|
+
uiTheme,
|
|
107
|
+
);
|
|
108
|
+
return strip(component.render(120));
|
|
109
|
+
}
|
|
110
|
+
|
|
45
111
|
function renderProgressText(progress: AgentProgress, expanded: boolean, uiTheme: Theme): string {
|
|
46
112
|
const details: TaskToolDetails = {
|
|
47
113
|
projectAgentsDir: null,
|
|
@@ -118,4 +184,71 @@ describe("task live progress rendering", () => {
|
|
|
118
184
|
expect(collapsedText).not.toContain("line 1");
|
|
119
185
|
expect(collapsedText).not.toContain("raw output:");
|
|
120
186
|
});
|
|
187
|
+
|
|
188
|
+
it("caps collapsed nested task progress at four rows plus an elision line", () => {
|
|
189
|
+
setViewportRows(40);
|
|
190
|
+
const text = renderProgressText(makeParentWithNestedProgress(6), false, uiTheme);
|
|
191
|
+
|
|
192
|
+
expect(text).not.toContain("Nested1");
|
|
193
|
+
expect(text).not.toContain("Nested2");
|
|
194
|
+
expect(text).toContain("Nested3");
|
|
195
|
+
expect(text).toContain("Nested4");
|
|
196
|
+
expect(text).toContain("Nested5");
|
|
197
|
+
expect(text).toContain("Nested6");
|
|
198
|
+
expect(text).toContain("2 more agents");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("shows every nested task progress row when expanded", () => {
|
|
202
|
+
setViewportRows(40);
|
|
203
|
+
const text = renderProgressText(makeParentWithNestedProgress(6), true, uiTheme);
|
|
204
|
+
|
|
205
|
+
for (let index = 1; index <= 6; index++) {
|
|
206
|
+
expect(text).toContain(`Nested${index}`);
|
|
207
|
+
}
|
|
208
|
+
expect(text).not.toContain("more agents");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("caps collapsed finalized nested task results and keeps the failed child visible", () => {
|
|
212
|
+
setViewportRows(40);
|
|
213
|
+
const text = renderResultText(makeParentWithNestedResults(6), false, uiTheme);
|
|
214
|
+
|
|
215
|
+
expect(text).toContain("Done6"); // failed child wins a slot despite sorting last
|
|
216
|
+
expect(text).toContain("2 more agents");
|
|
217
|
+
const visibleChildren = [1, 2, 3, 4, 5, 6].filter(index => text.includes(`Done${index}`));
|
|
218
|
+
expect(visibleChildren).toHaveLength(4);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("shows every finalized nested task result when expanded", () => {
|
|
222
|
+
setViewportRows(40);
|
|
223
|
+
const text = renderResultText(makeParentWithNestedResults(6), true, uiTheme);
|
|
224
|
+
|
|
225
|
+
for (let index = 1; index <= 6; index++) {
|
|
226
|
+
expect(text).toContain(`Done${index}`);
|
|
227
|
+
}
|
|
228
|
+
expect(text).not.toContain("more agents");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not request spinner ticks for static partial progress", () => {
|
|
232
|
+
expect("animatedPartialResult" in taskToolRenderer).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("renders running progress identically across spinner frames", () => {
|
|
236
|
+
const progress = makeProgress([]);
|
|
237
|
+
const details: TaskToolDetails = {
|
|
238
|
+
projectAgentsDir: null,
|
|
239
|
+
results: [],
|
|
240
|
+
totalDurationMs: 1,
|
|
241
|
+
progress: [progress],
|
|
242
|
+
};
|
|
243
|
+
const render = (spinnerFrame: number) =>
|
|
244
|
+
strip(
|
|
245
|
+
renderResult(
|
|
246
|
+
{ content: [{ type: "text", text: "Running 1 agent..." }], details },
|
|
247
|
+
{ expanded: false, isPartial: true, spinnerFrame },
|
|
248
|
+
uiTheme,
|
|
249
|
+
).render(120),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(render(0)).toBe(render(1));
|
|
253
|
+
});
|
|
121
254
|
});
|