@oh-my-pi/pi-coding-agent 15.11.4 → 15.11.6
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 +53 -0
- package/dist/cli.js +450 -424
- package/dist/types/cli/usage-cli.d.ts +10 -1
- package/dist/types/commands/usage.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +53 -3
- package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +107 -0
- package/dist/types/session/snapcompact-inline.d.ts +105 -4
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +0 -11
- package/package.json +11 -11
- package/src/cli/usage-cli.ts +187 -16
- package/src/commands/usage.ts +8 -0
- package/src/config/settings-schema.ts +56 -3
- package/src/config/settings.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/session-selector.ts +8 -2
- package/src/modes/components/settings-selector.ts +62 -47
- package/src/modes/components/tool-execution.ts +18 -0
- package/src/modes/components/transcript-container.ts +23 -1
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/selector-controller.ts +68 -0
- package/src/modes/interactive-mode.ts +59 -0
- package/src/modes/session-observer-registry.ts +61 -3
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/context-usage.ts +75 -1
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
- package/src/prompts/tools/browser.md +33 -43
- package/src/prompts/tools/eval.md +27 -50
- package/src/prompts/tools/irc.md +29 -31
- package/src/prompts/tools/read.md +31 -37
- package/src/prompts/tools/todo.md +1 -2
- package/src/sdk.ts +3 -2
- package/src/session/agent-session.ts +131 -6
- package/src/session/auth-storage.ts +3 -0
- package/src/session/codex-auto-reset.ts +190 -0
- package/src/session/snapcompact-inline.ts +396 -59
- package/src/slash-commands/builtin-registry.ts +145 -8
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/usage-report.ts +12 -0
- package/src/task/index.ts +30 -7
- package/src/task/render.ts +34 -19
- package/src/tools/todo.ts +8 -128
|
@@ -4,12 +4,11 @@ import {
|
|
|
4
4
|
type Component,
|
|
5
5
|
Container,
|
|
6
6
|
extractPrintableText,
|
|
7
|
-
|
|
7
|
+
fuzzyRank,
|
|
8
8
|
getKeybindings,
|
|
9
9
|
getSettingItemFilterText,
|
|
10
10
|
Input,
|
|
11
11
|
matchesKey,
|
|
12
|
-
padding,
|
|
13
12
|
parseSgrMouse,
|
|
14
13
|
type SelectItem,
|
|
15
14
|
SelectList,
|
|
@@ -68,8 +67,6 @@ class TextInputSubmenu extends Container {
|
|
|
68
67
|
this.#input = new Input();
|
|
69
68
|
if (currentValue) {
|
|
70
69
|
this.#input.setValue(currentValue);
|
|
71
|
-
// Move cursor to end of pre-filled value (ctrl+e = cursorLineEnd).
|
|
72
|
-
this.#input.handleInput("\x05");
|
|
73
70
|
}
|
|
74
71
|
this.#input.onSubmit = value => {
|
|
75
72
|
this.onSubmit(value); // empty string clears the setting
|
|
@@ -291,6 +288,8 @@ export class SettingsSelectorComponent implements Component {
|
|
|
291
288
|
#currentTabId: SettingTab | "plugins" = "appearance";
|
|
292
289
|
#preSearchTabId: SettingTab | "plugins" = "appearance";
|
|
293
290
|
#searchQuery = "";
|
|
291
|
+
/** Single-line editor backing the search banner (cursor, word ops, paste). */
|
|
292
|
+
#searchInput = new Input();
|
|
294
293
|
#searchMatchCount = 0;
|
|
295
294
|
/** First matching item id per tab id, for Tab-key jumps while searching. */
|
|
296
295
|
#searchFirstMatch = new Map<string, string>();
|
|
@@ -354,7 +353,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
354
353
|
|
|
355
354
|
#footerHintText(): string {
|
|
356
355
|
if (this.#searchList) {
|
|
357
|
-
return "Enter
|
|
356
|
+
return "Enter to change · Tab to jump tabs · Esc to exit search";
|
|
358
357
|
}
|
|
359
358
|
if (this.#currentTabId === "plugins") {
|
|
360
359
|
return "Tab to switch tabs · Esc to close";
|
|
@@ -366,28 +365,17 @@ export class SettingsSelectorComponent implements Component {
|
|
|
366
365
|
return `Enter/Space to change · ${nav} · Type to search · Esc to close`;
|
|
367
366
|
}
|
|
368
367
|
|
|
369
|
-
/** Single-line search banner: accent icon,
|
|
368
|
+
/** Single-line search banner: accent icon, editable query with live cursor, right-aligned match count. */
|
|
370
369
|
#renderSearchBanner(width: number): string {
|
|
371
370
|
const icon = theme.symbol("icon.search");
|
|
372
371
|
const countText = this.#searchMatchCount === 1 ? "1 match" : `${this.#searchMatchCount} matches`;
|
|
373
372
|
const rightWidth = visibleWidth(countText) + 1; // trailing margin
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
let display = this.#searchQuery;
|
|
379
|
-
if (visibleWidth(display) > queryBudget) {
|
|
380
|
-
const chars = [...display];
|
|
381
|
-
while (chars.length > 1 && visibleWidth(chars.join("")) > queryBudget - 1) {
|
|
382
|
-
chars.shift();
|
|
383
|
-
}
|
|
384
|
-
display = `…${chars.join("")}`;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const left = ` ${theme.fg("accent", icon)} ${theme.bold(display)}${theme.fg("accent", "▌")}`;
|
|
373
|
+
const prefix = ` ${theme.fg("accent", icon)} `;
|
|
374
|
+
// The input pads itself to exactly this width and keeps the cursor in view.
|
|
375
|
+
const inputWidth = Math.max(4, width - visibleWidth(prefix) - rightWidth - 1);
|
|
376
|
+
const inputLine = this.#searchInput.render(inputWidth)[0] ?? "";
|
|
388
377
|
const count = theme.fg(this.#searchMatchCount > 0 ? "dim" : "warning", countText);
|
|
389
|
-
|
|
390
|
-
return truncateToWidth(`${left}${padding(gap)}${count} `, width);
|
|
378
|
+
return truncateToWidth(`${prefix}${theme.bold(inputLine)} ${count} `, width);
|
|
391
379
|
}
|
|
392
380
|
|
|
393
381
|
/**
|
|
@@ -511,6 +499,9 @@ export class SettingsSelectorComponent implements Component {
|
|
|
511
499
|
/** Swap the tab content for the global search result list. */
|
|
512
500
|
#startSearch(initialQuery: string): void {
|
|
513
501
|
this.#preSearchTabId = this.#currentTabId;
|
|
502
|
+
this.#searchInput = new Input();
|
|
503
|
+
this.#searchInput.prompt = "";
|
|
504
|
+
this.#searchInput.setValue(initialQuery);
|
|
514
505
|
const list = new SettingsList(
|
|
515
506
|
[],
|
|
516
507
|
10,
|
|
@@ -547,6 +538,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
547
538
|
|
|
548
539
|
const counts = new Map<SettingTab, number>();
|
|
549
540
|
const items: SettingItem[] = [];
|
|
541
|
+
const tabResults: { tab: SettingTab; matched: SettingItem[]; bestScore: number; order: number }[] = [];
|
|
550
542
|
this.#searchFirstMatch.clear();
|
|
551
543
|
let total = 0;
|
|
552
544
|
for (const tab of SETTING_TABS) {
|
|
@@ -555,24 +547,40 @@ export class SettingsSelectorComponent implements Component {
|
|
|
555
547
|
const item = this.#defToItem(def);
|
|
556
548
|
if (item) candidates.push(item);
|
|
557
549
|
}
|
|
558
|
-
const
|
|
550
|
+
const ranked = fuzzyRank(candidates, query, getSettingItemFilterText);
|
|
551
|
+
const matched = ranked.map(result => result.item);
|
|
559
552
|
counts.set(tab, matched.length);
|
|
560
553
|
if (matched.length === 0) continue;
|
|
561
554
|
total += matched.length;
|
|
562
|
-
|
|
555
|
+
tabResults.push({
|
|
556
|
+
tab,
|
|
557
|
+
matched,
|
|
558
|
+
bestScore: ranked[0]?.score ?? 0,
|
|
559
|
+
order: SETTING_TABS.indexOf(tab),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
tabResults.sort((a, b) => a.bestScore - b.bestScore || a.order - b.order);
|
|
564
|
+
for (const result of tabResults) {
|
|
565
|
+
const meta = TAB_METADATA[result.tab];
|
|
563
566
|
items.push({
|
|
564
|
-
id: `__tab:${tab}`,
|
|
567
|
+
id: `__tab:${result.tab}`,
|
|
565
568
|
label: `${theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0])} ${meta.label}`,
|
|
566
569
|
currentValue: "",
|
|
567
570
|
heading: true,
|
|
568
571
|
});
|
|
569
|
-
this.#searchFirstMatch.set(tab, matched[0]
|
|
570
|
-
items.push(...matched);
|
|
572
|
+
this.#searchFirstMatch.set(result.tab, result.matched[0]?.id ?? "");
|
|
573
|
+
items.push(...result.matched);
|
|
571
574
|
}
|
|
572
575
|
|
|
573
576
|
this.#searchList.setItems(items);
|
|
574
577
|
this.#searchMatchCount = total;
|
|
575
|
-
this.#tabBar.setTabs(
|
|
578
|
+
this.#tabBar.setTabs(
|
|
579
|
+
this.#buildSearchTabs(
|
|
580
|
+
counts,
|
|
581
|
+
tabResults.map(result => result.tab),
|
|
582
|
+
),
|
|
583
|
+
);
|
|
576
584
|
this.#syncTabBarToSelection(this.#searchList.getSelectedItem());
|
|
577
585
|
}
|
|
578
586
|
|
|
@@ -597,20 +605,25 @@ export class SettingsSelectorComponent implements Component {
|
|
|
597
605
|
}
|
|
598
606
|
}
|
|
599
607
|
|
|
600
|
-
/** Matching tabs first (counts attached), the rest muted at the end. */
|
|
601
|
-
#buildSearchTabs(counts: Map<SettingTab, number
|
|
608
|
+
/** Matching tabs first (counts attached), ordered by best result score; the rest stay muted at the end. */
|
|
609
|
+
#buildSearchTabs(counts: Map<SettingTab, number>, matchedTabOrder: readonly SettingTab[]): Tab[] {
|
|
602
610
|
const matched: Tab[] = [];
|
|
603
611
|
const empty: Tab[] = [];
|
|
604
|
-
|
|
612
|
+
const matchedIds = new Set<SettingTab>(matchedTabOrder);
|
|
613
|
+
for (const id of matchedTabOrder) {
|
|
605
614
|
const meta = TAB_METADATA[id];
|
|
606
615
|
const icon = theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0]);
|
|
607
616
|
const count = counts.get(id) ?? 0;
|
|
608
617
|
if (count > 0) {
|
|
609
618
|
matched.push({ id, label: `${icon} ${meta.label} (${count})`, short: `${icon} ${count}` });
|
|
610
|
-
} else {
|
|
611
|
-
empty.push({ id, label: `${icon} ${meta.label}`, short: icon, muted: true });
|
|
612
619
|
}
|
|
613
620
|
}
|
|
621
|
+
for (const id of SETTING_TABS) {
|
|
622
|
+
if (matchedIds.has(id)) continue;
|
|
623
|
+
const meta = TAB_METADATA[id];
|
|
624
|
+
const icon = theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0]);
|
|
625
|
+
empty.push({ id, label: `${icon} ${meta.label}`, short: icon, muted: true });
|
|
626
|
+
}
|
|
614
627
|
// Plugins hosts its own UI; it is not part of the schema-backed search.
|
|
615
628
|
empty.push({ id: "plugins", label: `${theme.icon.package} Plugins`, short: theme.icon.package, muted: true });
|
|
616
629
|
return [...matched, ...empty];
|
|
@@ -1029,25 +1042,27 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1029
1042
|
this.#endSearch(true);
|
|
1030
1043
|
return;
|
|
1031
1044
|
}
|
|
1032
|
-
if (
|
|
1033
|
-
this.#setSearchQuery([...this.#searchQuery].slice(0, -1).join(""));
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
if (
|
|
1037
|
-
matchesKey(data, "tab") ||
|
|
1038
|
-
matchesKey(data, "shift+tab") ||
|
|
1039
|
-
matchesKey(data, "left") ||
|
|
1040
|
-
matchesKey(data, "right")
|
|
1041
|
-
) {
|
|
1045
|
+
if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
|
|
1042
1046
|
// Jump between tabs that have matches (muted tabs are skipped).
|
|
1043
1047
|
this.#tabBar.handleInput(data);
|
|
1044
1048
|
return;
|
|
1045
1049
|
}
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1050
|
+
// Selection, paging, and activation stay with the result list.
|
|
1051
|
+
if (
|
|
1052
|
+
kb.matches(data, "tui.select.up") ||
|
|
1053
|
+
kb.matches(data, "tui.select.down") ||
|
|
1054
|
+
kb.matches(data, "tui.select.pageUp") ||
|
|
1055
|
+
kb.matches(data, "tui.select.pageDown") ||
|
|
1056
|
+
kb.matches(data, "tui.select.confirm") ||
|
|
1057
|
+
data === "\n"
|
|
1058
|
+
) {
|
|
1059
|
+
list.handleInput(data);
|
|
1049
1060
|
return;
|
|
1050
1061
|
}
|
|
1051
|
-
|
|
1062
|
+
// Everything else edits the query like a regular single-line editor:
|
|
1063
|
+
// cursor movement, word ops, kill ring, undo, paste.
|
|
1064
|
+
this.#searchInput.handleInput(data);
|
|
1065
|
+
const value = this.#searchInput.getValue();
|
|
1066
|
+
if (value !== this.#searchQuery) this.#setSearchQuery(value);
|
|
1052
1067
|
}
|
|
1053
1068
|
}
|
|
@@ -589,6 +589,24 @@ export class ToolExecutionComponent extends Container {
|
|
|
589
589
|
return (this.#result.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Whether this still-live block's settled rows may enter native scrollback
|
|
594
|
+
* (see `FinalizableBlock.isTranscriptBlockCommitStable`). A pending
|
|
595
|
+
* collapsed preview is provisional: the tail-window streaming views
|
|
596
|
+
* (edit/bash/eval caps) are re-anchored top-first by the result render, so
|
|
597
|
+
* promoting their visually static head — e.g. an edit preview idling on
|
|
598
|
+
* its last frame while the apply + LSP pass runs — would strand a stale
|
|
599
|
+
* copy of the call box above the final block the moment the result lands.
|
|
600
|
+
* Expanded pending blocks stream top-anchored append-shaped content whose
|
|
601
|
+
* rows the result render preserves byte-stable (the over-tall write/eval
|
|
602
|
+
* scrollback contract), so they stay commit-eligible. Displaceable waiting
|
|
603
|
+
* polls are removed wholesale by the next poll and must never commit.
|
|
604
|
+
*/
|
|
605
|
+
isTranscriptBlockCommitStable(): boolean {
|
|
606
|
+
if (this.#displaceable) return false;
|
|
607
|
+
return this.#expanded || this.isTranscriptBlockFinalized();
|
|
608
|
+
}
|
|
609
|
+
|
|
592
610
|
/**
|
|
593
611
|
* Mark the tool terminal even though no result arrived (the turn aborted or
|
|
594
612
|
* abandoned it) and stop animating, so it can freeze and stops pinning the
|
|
@@ -63,6 +63,19 @@ interface FinalizableBlock {
|
|
|
63
63
|
* never mutate post-finalize simply omit the method.
|
|
64
64
|
*/
|
|
65
65
|
getTranscriptBlockVersion?(): number;
|
|
66
|
+
/**
|
|
67
|
+
* Whether a still-live block's visually settled leading rows are durable —
|
|
68
|
+
* guaranteed to survive the block's remaining transitions (finalize,
|
|
69
|
+
* displacement) byte-stable — and may therefore be promoted as commit-safe
|
|
70
|
+
* by {@link deriveLiveCommitState}. Blocks whose pending render is
|
|
71
|
+
* provisional (a tool call's tail-window streaming preview, replaced
|
|
72
|
+
* wholesale by the result render) return `false`: committing such rows
|
|
73
|
+
* strands a stale copy in immutable terminal history the moment the real
|
|
74
|
+
* content re-lays-out the block (the engine audit recommits below it —
|
|
75
|
+
* "duplication, never loss"). Absent = `true`, the default for blocks
|
|
76
|
+
* whose live rows persist (a streaming assistant message).
|
|
77
|
+
*/
|
|
78
|
+
isTranscriptBlockCommitStable?(): boolean;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
function isBlockFinalized(child: Component): boolean {
|
|
@@ -75,6 +88,11 @@ function getBlockVersion(child: Component): number | undefined {
|
|
|
75
88
|
return fn ? fn.call(child) : undefined;
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
function isBlockCommitStable(child: Component): boolean {
|
|
92
|
+
const fn = (child as Component & FinalizableBlock).isTranscriptBlockCommitStable;
|
|
93
|
+
return fn ? fn.call(child) : true;
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
// A "plain blank" row is empty or whitespace-only with no ANSI bytes. It marks
|
|
79
97
|
// separation padding (a `Spacer`, or a no-background `paddingY` row) as opposed
|
|
80
98
|
// to a background-colored padding row, whose escape sequences contain `\S` and
|
|
@@ -598,7 +616,11 @@ export class TranscriptContainer
|
|
|
598
616
|
previous.generation === this.#generation);
|
|
599
617
|
const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
|
|
600
618
|
let liveCommitState: LiveCommitState | undefined;
|
|
601
|
-
|
|
619
|
+
// Provisional live renders (commit-unstable blocks) never feed the
|
|
620
|
+
// promotion machinery: their settled-looking rows are replaced
|
|
621
|
+
// wholesale on finalize, so offering them would commit a stale
|
|
622
|
+
// preview the result render can only duplicate, never erase.
|
|
623
|
+
if (i >= liveStartIndex && !finalized && isBlockCommitStable(child)) {
|
|
602
624
|
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
603
625
|
}
|
|
604
626
|
// Cache the latest contribution as the next frame's diff input.
|
|
@@ -456,7 +456,7 @@ export class CommandController {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
handleContextCommand(): void {
|
|
459
|
-
const breakdown = computeContextBreakdown(this.ctx.session);
|
|
459
|
+
const breakdown = computeContextBreakdown(this.ctx.session, { snapcompactSavings: true });
|
|
460
460
|
if (breakdown.contextWindow <= 0) {
|
|
461
461
|
this.ctx.showWarning("Context usage is unavailable: no model is selected for this session.");
|
|
462
462
|
return;
|
|
@@ -1528,6 +1528,29 @@ function renderUsageReports(
|
|
|
1528
1528
|
lines.push(` ${uiTheme.fg("accent", "in use by this session:")} ${activeAccountLabel}`);
|
|
1529
1529
|
}
|
|
1530
1530
|
|
|
1531
|
+
const resetAccountLines: string[] = [];
|
|
1532
|
+
for (const report of providerReports) {
|
|
1533
|
+
const count = report.resetCredits?.availableCount ?? 0;
|
|
1534
|
+
if (count <= 0) continue;
|
|
1535
|
+
const label =
|
|
1536
|
+
(report.metadata?.email as string | undefined) ??
|
|
1537
|
+
(report.metadata?.accountId as string | undefined) ??
|
|
1538
|
+
"account";
|
|
1539
|
+
const isActive =
|
|
1540
|
+
!!activeAccount &&
|
|
1541
|
+
((!!activeAccount.accountId && activeAccount.accountId === report.metadata?.accountId) ||
|
|
1542
|
+
(!!activeAccount.email && activeAccount.email === report.metadata?.email));
|
|
1543
|
+
resetAccountLines.push(
|
|
1544
|
+
` • ${label}: ${count} saved reset${count === 1 ? "" : "s"}${isActive ? " (active)" : ""}`,
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
if (resetAccountLines.length > 0) {
|
|
1548
|
+
lines.push(
|
|
1549
|
+
` ${uiTheme.fg("accent", "Saved rate-limit resets")} ${uiTheme.fg("dim", "(/usage reset to spend)")}`,
|
|
1550
|
+
);
|
|
1551
|
+
for (const line of resetAccountLines) lines.push(uiTheme.fg("dim", line));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1531
1554
|
const renderableGroups = Array.from(limitGroups.values()).map(group => {
|
|
1532
1555
|
const entries = group.limits.map((limit, index) => ({
|
|
1533
1556
|
limit,
|
|
@@ -27,8 +27,14 @@ import {
|
|
|
27
27
|
theme,
|
|
28
28
|
} from "../../modes/theme/theme";
|
|
29
29
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
30
|
+
import type { ResetCreditRedeemOutcome } from "../../session/auth-storage";
|
|
30
31
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
31
32
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
33
|
+
import {
|
|
34
|
+
describeRedeemOutcome,
|
|
35
|
+
type ResetUsageAccount,
|
|
36
|
+
toResetUsageAccounts,
|
|
37
|
+
} from "../../slash-commands/helpers/reset-usage";
|
|
32
38
|
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
|
|
33
39
|
import {
|
|
34
40
|
isImageProviderPreference,
|
|
@@ -48,6 +54,7 @@ import { HistorySearchComponent } from "../components/history-search";
|
|
|
48
54
|
import { ModelSelectorComponent } from "../components/model-selector";
|
|
49
55
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
50
56
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
57
|
+
import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
|
|
51
58
|
import { SessionSelectorComponent } from "../components/session-selector";
|
|
52
59
|
import { SettingsSelectorComponent } from "../components/settings-selector";
|
|
53
60
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
@@ -1091,6 +1098,67 @@ export class SelectorController {
|
|
|
1091
1098
|
});
|
|
1092
1099
|
}
|
|
1093
1100
|
|
|
1101
|
+
async showResetUsageSelector(): Promise<void> {
|
|
1102
|
+
const session = this.ctx.session;
|
|
1103
|
+
this.ctx.showStatus("Checking saved rate-limit resets…", { dim: true });
|
|
1104
|
+
let statuses: Awaited<ReturnType<typeof session.listResetCredits>>;
|
|
1105
|
+
try {
|
|
1106
|
+
statuses = await session.listResetCredits();
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
this.ctx.showError(`Could not load saved resets: ${error instanceof Error ? error.message : String(error)}`);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const accounts = toResetUsageAccounts(statuses);
|
|
1112
|
+
if (accounts.length === 0) {
|
|
1113
|
+
this.ctx.showStatus("No Codex accounts found. Use /login to add one.");
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (!accounts.some(account => account.availableCount > 0)) {
|
|
1117
|
+
this.ctx.showStatus(
|
|
1118
|
+
accounts.some(account => account.error)
|
|
1119
|
+
? "No saved resets available — some accounts couldn't be reached (try /login)."
|
|
1120
|
+
: "No saved rate-limit resets available to spend right now.",
|
|
1121
|
+
);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
this.showSelector(done => {
|
|
1125
|
+
const selector = new ResetUsageSelectorComponent(
|
|
1126
|
+
accounts,
|
|
1127
|
+
account => {
|
|
1128
|
+
done();
|
|
1129
|
+
void this.#redeemReset(account);
|
|
1130
|
+
},
|
|
1131
|
+
() => {
|
|
1132
|
+
done();
|
|
1133
|
+
this.ctx.ui.requestRender();
|
|
1134
|
+
},
|
|
1135
|
+
);
|
|
1136
|
+
return { component: selector, focus: selector };
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async #redeemReset(account: ResetUsageAccount): Promise<void> {
|
|
1141
|
+
this.ctx.showStatus(`Spending 1 saved reset for ${account.label}…`, { dim: true });
|
|
1142
|
+
let outcome: ResetCreditRedeemOutcome;
|
|
1143
|
+
try {
|
|
1144
|
+
outcome = await this.ctx.session.redeemResetCredit(account.target);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
this.ctx.showError(
|
|
1147
|
+
`Reset failed for ${account.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1148
|
+
);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const message = describeRedeemOutcome(outcome, account.label);
|
|
1152
|
+
if (outcome.ok) {
|
|
1153
|
+
this.ctx.showStatus(message);
|
|
1154
|
+
// Refresh the status-line usage so the freshly-reset window shows.
|
|
1155
|
+
this.ctx.statusLine.invalidate();
|
|
1156
|
+
this.ctx.ui.requestRender();
|
|
1157
|
+
} else {
|
|
1158
|
+
this.ctx.showWarning(message);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1094
1162
|
async showDebugSelector(): Promise<void> {
|
|
1095
1163
|
const { DebugSelectorComponent } = await import("../../debug");
|
|
1096
1164
|
this.showSelector(done => {
|
|
@@ -86,8 +86,10 @@ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../slash-commands/builtin-
|
|
|
86
86
|
import { formatDuration } from "../slash-commands/helpers/format";
|
|
87
87
|
import { STTController, type SttState } from "../stt";
|
|
88
88
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
89
|
+
import { formatTaskId } from "../task/render";
|
|
89
90
|
import type { LspStartupServerInfo } from "../tools";
|
|
90
91
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
92
|
+
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
|
91
93
|
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
|
92
94
|
import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
93
95
|
import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
|
|
@@ -132,6 +134,7 @@ import {
|
|
|
132
134
|
parseLoopLimitArgs,
|
|
133
135
|
} from "./loop-limit";
|
|
134
136
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
137
|
+
import type { ObservableSession } from "./session-observer-registry";
|
|
135
138
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
136
139
|
import { runProviderSetupWizard } from "./setup-wizard/lazy";
|
|
137
140
|
import { interruptHint } from "./shared";
|
|
@@ -277,6 +280,41 @@ class StatusContainer extends Container implements NativeScrollbackLiveRegion {
|
|
|
277
280
|
}
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
285
|
+
* one hooked row per running agent in the same `Id: description` shape the
|
|
286
|
+
* inline task rows use (muted task preview when no description was given).
|
|
287
|
+
* Returns an empty array when nothing is running so the container can clear.
|
|
288
|
+
*/
|
|
289
|
+
export function renderSubagentHudLines(sessions: ObservableSession[], columns: number): string[] {
|
|
290
|
+
const running = sessions.filter(session => session.kind === "subagent" && session.status === "active");
|
|
291
|
+
if (running.length === 0) return [];
|
|
292
|
+
|
|
293
|
+
const indent = " ";
|
|
294
|
+
const hook = theme.tree.hook;
|
|
295
|
+
const dot = theme.styledSymbol("status.done", "accent");
|
|
296
|
+
const lines = ["", indent + theme.bold(theme.fg("accent", "Subagents"))];
|
|
297
|
+
running.forEach((session, index) => {
|
|
298
|
+
const prefix = `${indent}${index === 0 ? hook : " "} `;
|
|
299
|
+
const displayId = formatTaskId(session.id);
|
|
300
|
+
let line = `${prefix}${dot} ${theme.fg("accent", theme.bold(displayId))}`;
|
|
301
|
+
const description = session.description?.trim() || session.progress?.description?.trim();
|
|
302
|
+
if (description) {
|
|
303
|
+
const budget = Math.max(TRUNCATE_LENGTHS.SHORT, columns - visibleWidth(prefix) - visibleWidth(displayId) - 6);
|
|
304
|
+
line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(description), budget))}`;
|
|
305
|
+
} else {
|
|
306
|
+
// No spawn description: fall back to a muted task preview, same as
|
|
307
|
+
// the inline task rows when a row has no label.
|
|
308
|
+
const taskPreview = session.progress?.task?.trim();
|
|
309
|
+
if (taskPreview) {
|
|
310
|
+
line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
lines.push(line);
|
|
314
|
+
});
|
|
315
|
+
return lines;
|
|
316
|
+
}
|
|
317
|
+
|
|
280
318
|
export class InteractiveMode implements InteractiveModeContext {
|
|
281
319
|
session: AgentSession;
|
|
282
320
|
sessionManager: SessionManager;
|
|
@@ -291,6 +329,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
291
329
|
pendingMessagesContainer: Container;
|
|
292
330
|
statusContainer: Container;
|
|
293
331
|
todoContainer: Container;
|
|
332
|
+
subagentContainer: Container;
|
|
294
333
|
btwContainer: Container;
|
|
295
334
|
omfgContainer: Container;
|
|
296
335
|
errorBannerContainer: Container;
|
|
@@ -440,6 +479,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
440
479
|
this.pendingMessagesContainer = new Container();
|
|
441
480
|
this.statusContainer = new StatusContainer();
|
|
442
481
|
this.todoContainer = new Container();
|
|
482
|
+
this.subagentContainer = new Container();
|
|
443
483
|
this.btwContainer = new Container();
|
|
444
484
|
this.omfgContainer = new Container();
|
|
445
485
|
this.errorBannerContainer = new Container();
|
|
@@ -606,6 +646,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
606
646
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
607
647
|
this.ui.addChild(this.statusContainer);
|
|
608
648
|
this.ui.addChild(this.todoContainer);
|
|
649
|
+
this.ui.addChild(this.subagentContainer);
|
|
609
650
|
this.ui.addChild(this.btwContainer);
|
|
610
651
|
this.ui.addChild(this.omfgContainer);
|
|
611
652
|
this.ui.addChild(this.errorBannerContainer);
|
|
@@ -632,6 +673,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
632
673
|
this.#reconcileTodosWithSubagents();
|
|
633
674
|
this.#syncTodoAutoClearTimer();
|
|
634
675
|
this.#renderTodoList();
|
|
676
|
+
this.#renderSubagentList();
|
|
635
677
|
this.ui.requestRender();
|
|
636
678
|
});
|
|
637
679
|
|
|
@@ -1282,6 +1324,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1282
1324
|
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
1283
1325
|
}
|
|
1284
1326
|
|
|
1327
|
+
/**
|
|
1328
|
+
* Anchored HUD of in-flight subagents, mirroring the Todos block above the
|
|
1329
|
+
* editor. Driven entirely by observer-registry change events, so rows appear
|
|
1330
|
+
* on spawn and the whole block clears itself once the last subagent leaves
|
|
1331
|
+
* the "active" state.
|
|
1332
|
+
*/
|
|
1333
|
+
#renderSubagentList(): void {
|
|
1334
|
+
this.subagentContainer.clear();
|
|
1335
|
+
const lines = renderSubagentHudLines(this.#observerRegistry.getSessions(), this.ui.terminal.columns);
|
|
1336
|
+
if (lines.length === 0) return;
|
|
1337
|
+
this.subagentContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1285
1340
|
async #loadTodoList(): Promise<void> {
|
|
1286
1341
|
this.todoPhases = this.session.getTodoPhases();
|
|
1287
1342
|
this.#syncTodoAutoClearTimer();
|
|
@@ -3235,6 +3290,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3235
3290
|
return this.#selectorController.showOAuthSelector(mode, providerId);
|
|
3236
3291
|
}
|
|
3237
3292
|
|
|
3293
|
+
showResetUsageSelector(): Promise<void> {
|
|
3294
|
+
return this.#selectorController.showResetUsageSelector();
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3238
3297
|
showProviderSetup(): Promise<void> {
|
|
3239
3298
|
return runProviderSetupWizard(this);
|
|
3240
3299
|
}
|
|
@@ -10,6 +10,8 @@ export interface ObservableSession {
|
|
|
10
10
|
description?: string;
|
|
11
11
|
status: "active" | "completed" | "failed" | "aborted";
|
|
12
12
|
sessionFile?: string;
|
|
13
|
+
parentToolCallId?: string;
|
|
14
|
+
index?: number;
|
|
13
15
|
lastUpdate: number;
|
|
14
16
|
/** Latest progress snapshot from the subagent executor */
|
|
15
17
|
progress?: AgentProgress;
|
|
@@ -26,6 +28,9 @@ export class SessionObserverRegistry {
|
|
|
26
28
|
#sessions = new Map<string, ObservableSession>();
|
|
27
29
|
#listeners = new Set<() => void>();
|
|
28
30
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
31
|
+
#sortOrderById = new Map<string, number>();
|
|
32
|
+
#parentSortOrderById = new Map<string, number>();
|
|
33
|
+
#nextSortOrder = 0;
|
|
29
34
|
|
|
30
35
|
/** Add a change listener. Returns unsubscribe function. */
|
|
31
36
|
onChange(cb: () => void): () => void {
|
|
@@ -37,8 +42,34 @@ export class SessionObserverRegistry {
|
|
|
37
42
|
for (const cb of this.#listeners) cb();
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
#ensureSortOrder(id: string): number {
|
|
46
|
+
const existing = this.#sortOrderById.get(id);
|
|
47
|
+
if (existing !== undefined) return existing;
|
|
48
|
+
const order = this.#nextSortOrder++;
|
|
49
|
+
this.#sortOrderById.set(id, order);
|
|
50
|
+
return order;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#ensureParentSortOrder(parentToolCallId: string | undefined, order: number): void {
|
|
54
|
+
if (!parentToolCallId) return;
|
|
55
|
+
if (this.#parentSortOrderById.has(parentToolCallId)) return;
|
|
56
|
+
this.#parentSortOrderById.set(parentToolCallId, order);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#getStableOrder(session: ObservableSession): number {
|
|
60
|
+
return this.#sortOrderById.get(session.id) ?? Number.MAX_SAFE_INTEGER;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#getGroupOrder(session: ObservableSession): number {
|
|
64
|
+
const parentOrder = session.parentToolCallId
|
|
65
|
+
? this.#parentSortOrderById.get(session.parentToolCallId)
|
|
66
|
+
: undefined;
|
|
67
|
+
return parentOrder ?? this.#getStableOrder(session);
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
setMainSession(sessionFile?: string): void {
|
|
41
71
|
const existing = this.#sessions.get("main");
|
|
72
|
+
this.#ensureSortOrder("main");
|
|
42
73
|
this.#sessions.set("main", {
|
|
43
74
|
id: "main",
|
|
44
75
|
kind: "main",
|
|
@@ -53,9 +84,18 @@ export class SessionObserverRegistry {
|
|
|
53
84
|
getSessions(): ObservableSession[] {
|
|
54
85
|
const sessions = [...this.#sessions.values()];
|
|
55
86
|
sessions.sort((a, b) => {
|
|
56
|
-
if (a.kind === "main") return -1;
|
|
57
|
-
if (b.kind === "main") return 1;
|
|
58
|
-
|
|
87
|
+
if (a.kind === "main" && b.kind !== "main") return -1;
|
|
88
|
+
if (b.kind === "main" && a.kind !== "main") return 1;
|
|
89
|
+
if (a.kind === "main" || b.kind === "main") return 0;
|
|
90
|
+
|
|
91
|
+
const groupDiff = this.#getGroupOrder(a) - this.#getGroupOrder(b);
|
|
92
|
+
if (groupDiff !== 0) return groupDiff;
|
|
93
|
+
|
|
94
|
+
const aIndex = a.index ?? Number.MAX_SAFE_INTEGER;
|
|
95
|
+
const bIndex = b.index ?? Number.MAX_SAFE_INTEGER;
|
|
96
|
+
if (aIndex !== bIndex) return aIndex - bIndex;
|
|
97
|
+
|
|
98
|
+
return this.#getStableOrder(a) - this.#getStableOrder(b);
|
|
59
99
|
});
|
|
60
100
|
return sessions;
|
|
61
101
|
}
|
|
@@ -71,6 +111,9 @@ export class SessionObserverRegistry {
|
|
|
71
111
|
/** Clear all tracked sessions (e.g. on session switch). Keeps EventBus subscriptions and listeners. */
|
|
72
112
|
resetSessions(): void {
|
|
73
113
|
this.#sessions.clear();
|
|
114
|
+
this.#sortOrderById.clear();
|
|
115
|
+
this.#parentSortOrderById.clear();
|
|
116
|
+
this.#nextSortOrder = 0;
|
|
74
117
|
this.#notifyListeners();
|
|
75
118
|
}
|
|
76
119
|
|
|
@@ -78,6 +121,9 @@ export class SessionObserverRegistry {
|
|
|
78
121
|
for (const unsub of this.#eventBusUnsubscribers) unsub();
|
|
79
122
|
this.#eventBusUnsubscribers = [];
|
|
80
123
|
this.#sessions.clear();
|
|
124
|
+
this.#sortOrderById.clear();
|
|
125
|
+
this.#parentSortOrderById.clear();
|
|
126
|
+
this.#nextSortOrder = 0;
|
|
81
127
|
this.#listeners.clear();
|
|
82
128
|
}
|
|
83
129
|
|
|
@@ -92,10 +138,14 @@ export class SessionObserverRegistry {
|
|
|
92
138
|
const status = STATUS_MAP[payload.status];
|
|
93
139
|
if (!status) return;
|
|
94
140
|
|
|
141
|
+
const sortOrder = this.#ensureSortOrder(payload.id);
|
|
142
|
+
this.#ensureParentSortOrder(payload.parentToolCallId, sortOrder);
|
|
95
143
|
const existing = this.#sessions.get(payload.id);
|
|
96
144
|
if (existing) {
|
|
97
145
|
existing.status = status;
|
|
98
146
|
existing.lastUpdate = Date.now();
|
|
147
|
+
existing.index = payload.index;
|
|
148
|
+
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
99
149
|
if (payload.description) existing.description = payload.description;
|
|
100
150
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
101
151
|
} else {
|
|
@@ -107,6 +157,8 @@ export class SessionObserverRegistry {
|
|
|
107
157
|
description: payload.description,
|
|
108
158
|
status,
|
|
109
159
|
sessionFile: payload.sessionFile,
|
|
160
|
+
parentToolCallId: payload.parentToolCallId,
|
|
161
|
+
index: payload.index,
|
|
110
162
|
lastUpdate: Date.now(),
|
|
111
163
|
});
|
|
112
164
|
}
|
|
@@ -121,8 +173,12 @@ export class SessionObserverRegistry {
|
|
|
121
173
|
const id = progress.id;
|
|
122
174
|
const existing = this.#sessions.get(id);
|
|
123
175
|
|
|
176
|
+
const sortOrder = this.#ensureSortOrder(id);
|
|
177
|
+
this.#ensureParentSortOrder(payload.parentToolCallId, sortOrder);
|
|
124
178
|
if (existing) {
|
|
125
179
|
existing.lastUpdate = Date.now();
|
|
180
|
+
existing.index = payload.index;
|
|
181
|
+
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
126
182
|
existing.progress = progress;
|
|
127
183
|
if (progress.description) existing.description = progress.description;
|
|
128
184
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
@@ -135,6 +191,8 @@ export class SessionObserverRegistry {
|
|
|
135
191
|
description: progress.description,
|
|
136
192
|
status: "active",
|
|
137
193
|
sessionFile: payload.sessionFile,
|
|
194
|
+
parentToolCallId: payload.parentToolCallId,
|
|
195
|
+
index: payload.index,
|
|
138
196
|
lastUpdate: Date.now(),
|
|
139
197
|
progress,
|
|
140
198
|
});
|