gentle-pi 3.7.0 → 4.0.0
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/README.md +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ScrollView, VStack, visibleWidth, type Component, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
2
2
|
import { sidebarState, type SidebarRail } from "./shell-sidebar.ts";
|
|
3
3
|
import type { ShellBarTheme } from "./shell-bar.ts";
|
|
4
|
+
import { CARD_STYLE, cardStyle, type CardStyle } from "./shell-card.ts";
|
|
4
5
|
import { renderSidebarBanner } from "./shell-sidebar-banner.ts";
|
|
6
|
+
import type { Density, HeaderPlacement, StatusPlacement } from "./visual-customization-policy.ts";
|
|
5
7
|
|
|
6
8
|
export const SIDEBAR_BREAKPOINT = 140;
|
|
7
9
|
const RAIL_WIDTH = 50;
|
|
@@ -12,6 +14,8 @@ const RAIL_PADDING = 1;
|
|
|
12
14
|
// the card instead of touching the terminal edge.
|
|
13
15
|
const HEADER_RIGHT_INSET = RAIL_PADDING + 1;
|
|
14
16
|
const GAP = 3;
|
|
17
|
+
/** Right-edge columns the painting rail takes from the editor column; published as `railColumns`. */
|
|
18
|
+
export const SIDEBAR_RAIL_COLUMNS = RAIL_WIDTH + GAP;
|
|
15
19
|
// Experimental Pi 0.85.1 internals. Only the fullscreen layout tree is adapted;
|
|
16
20
|
// regular mode keeps native scrollback and the original bottom components.
|
|
17
21
|
const NODE = Symbol.for("@earendil-works/pi-tui/layout-node");
|
|
@@ -27,6 +31,8 @@ type PreparedRail = {
|
|
|
27
31
|
revision: number;
|
|
28
32
|
width: number;
|
|
29
33
|
mode: string | undefined;
|
|
34
|
+
headerPlacement: HeaderPlacement;
|
|
35
|
+
cardStyle: CardStyle;
|
|
30
36
|
root: LayoutRoot;
|
|
31
37
|
theme: ShellBarTheme;
|
|
32
38
|
parts: Array<[string, SidebarRail]>;
|
|
@@ -68,8 +74,28 @@ function railDigest(rail: SidebarRail): string | undefined {
|
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
export const STATUS_OWNER = { HEADER: "header", BOTTOM: "bottom" } as const;
|
|
78
|
+
export type StatusOwner = (typeof STATUS_OWNER)[keyof typeof STATUS_OWNER];
|
|
79
|
+
export interface StatusOwnerInput {
|
|
80
|
+
mode: string | undefined;
|
|
81
|
+
columns: number;
|
|
82
|
+
statusPlacement: StatusPlacement;
|
|
83
|
+
headerPlacement: HeaderPlacement;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Which single status row owns a narrow fullscreen terminal, so the header and
|
|
88
|
+
* the bottom bar never both paint there. A configured top header wins;
|
|
89
|
+
* otherwise the bottom bar does, unless Status is hidden and the below-input
|
|
90
|
+
* header is all that is left. Wide and regular layouts keep their own rules.
|
|
91
|
+
*/
|
|
92
|
+
export function narrowStatusOwner(input: StatusOwnerInput): StatusOwner | undefined {
|
|
93
|
+
if (input.mode !== "fullscreen" || input.columns >= SIDEBAR_BREAKPOINT) return undefined;
|
|
94
|
+
return input.headerPlacement === "top" || input.statusPlacement === "hidden" ? STATUS_OWNER.HEADER : STATUS_OWNER.BOTTOM;
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
/** Installs the fullscreen rail: wraps the host layout root with the [rail, transcript] hstack and returns a disposer restoring the original layout. */
|
|
72
|
-
export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
98
|
+
export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () => StatusPlacement = () => "auto", headerPlacement: () => HeaderPlacement = () => "top", density: () => Density = () => "comfortable"): () => void {
|
|
73
99
|
if (!tui.terminal) return () => {};
|
|
74
100
|
const host = tui as Host;
|
|
75
101
|
const state = sidebarState(tui);
|
|
@@ -89,7 +115,17 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
89
115
|
// revision counter, exactly like the whole-rail memo already did.
|
|
90
116
|
const sectionCache = new Map<string, SectionCacheEntry>();
|
|
91
117
|
state.active = false;
|
|
92
|
-
|
|
118
|
+
// Hidden removes Status everywhere, including regular mode where the rail
|
|
119
|
+
// never mounts, so it is published independently of the fullscreen layout.
|
|
120
|
+
state.statusHidden = () => !stopped && placement() === "hidden";
|
|
121
|
+
// A narrow top header is the only status row there. Whether it paints comes
|
|
122
|
+
// from the last layout pass (a blank or failed header never swallows the
|
|
123
|
+
// bottom bar); the geometry is read live so a resize applies before the next pass.
|
|
124
|
+
const headerOwnsStatus = () => !stopped && !failed && headerLines.length > 0 && headerPlacement() === "top" &&
|
|
125
|
+
narrowStatusOwner({ mode: host.mode, columns: tui.terminal.columns, statusPlacement: placement(), headerPlacement: headerPlacement() }) === STATUS_OWNER.HEADER;
|
|
126
|
+
state.headerOwnsStatus = headerOwnsStatus;
|
|
127
|
+
state.railColumns = SIDEBAR_RAIL_COLUMNS;
|
|
128
|
+
state.ownsHost = () => !stopped && host.mode === "fullscreen" && tui.terminal.columns >= SIDEBAR_BREAKPOINT && (placement() === "auto" || placement() === "right") && !!host.layoutRoot && roots.has(host.layoutRoot);
|
|
93
129
|
const rail: Component = {
|
|
94
130
|
render: () => railLines,
|
|
95
131
|
invalidate() {
|
|
@@ -156,14 +192,30 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
156
192
|
};
|
|
157
193
|
const prepare = (width: number, root: LayoutRoot): boolean => {
|
|
158
194
|
state.active = false;
|
|
159
|
-
if (stopped || failed || host.mode !== "fullscreen"
|
|
195
|
+
if (stopped || failed || host.mode !== "fullscreen") {
|
|
160
196
|
prepared = undefined;
|
|
197
|
+
headerLines = [];
|
|
161
198
|
return false;
|
|
162
199
|
}
|
|
200
|
+
const railEligible = width >= SIDEBAR_BREAKPOINT && placement() !== "bottom" && placement() !== "hidden";
|
|
201
|
+
if (!railEligible) {
|
|
202
|
+
prepared = undefined;
|
|
203
|
+
try {
|
|
204
|
+
const headerPart = state.parts.get("header");
|
|
205
|
+
headerLines = headerPlacement() === "top" && headerPart
|
|
206
|
+
? [...(headerPart.render(Math.max(0, width)) ?? [])] : [];
|
|
207
|
+
if (!headerLines.some((line) => line.trim() !== "")) headerLines = [];
|
|
208
|
+
} catch {
|
|
209
|
+
failed = true;
|
|
210
|
+
headerLines = [];
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const style = cardStyle();
|
|
163
215
|
const parts = [...state.parts.entries()];
|
|
164
216
|
const digests = parts.map(([, rail]) => railDigest(rail));
|
|
165
217
|
const unchanged = prepared?.revision === cache.revision &&
|
|
166
|
-
prepared.width === width && prepared.mode === host.mode && prepared.root === root && prepared.theme === theme &&
|
|
218
|
+
prepared.width === width && prepared.mode === host.mode && prepared.headerPlacement === headerPlacement() && prepared.cardStyle === style && prepared.root === root && prepared.theme === theme &&
|
|
167
219
|
prepared.parts.length === parts.length && prepared.parts.every(([key, part], index) => parts[index]?.[0] === key && parts[index]?.[1] === part) &&
|
|
168
220
|
prepared.digests.length === digests.length && prepared.digests.every((digest, index) => digest === digests[index]);
|
|
169
221
|
if (unchanged) {
|
|
@@ -179,7 +231,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
179
231
|
const preparedHeaderLines = [...(headerPart?.render(Math.max(0, width - HEADER_RIGHT_INSET)) ?? [])];
|
|
180
232
|
const headerActive = headerPart !== undefined && preparedHeaderLines.some((line) => line.trim() !== "");
|
|
181
233
|
const contentWidth = scroll.getContentWidth(RAIL_WIDTH);
|
|
182
|
-
const sections = ["footer", "agents", "todo"].map((key) => {
|
|
234
|
+
const sections = ["footer", "agents", "todo"].filter((key) => key !== "todo" || state.visibility?.todo !== false).map((key) => {
|
|
183
235
|
const component = state.parts.get(key);
|
|
184
236
|
if (!component) {
|
|
185
237
|
sectionCache.delete(key);
|
|
@@ -204,23 +256,23 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
204
256
|
railLines = [];
|
|
205
257
|
if (sections.length && branding.length) {
|
|
206
258
|
railLines.push(...branding.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
|
|
207
|
-
} else if (sections.length && headerActive) {
|
|
208
|
-
//
|
|
209
|
-
//
|
|
259
|
+
} else if (sections.length && headerActive && density() === "comfortable" && style === CARD_STYLE.NEON) {
|
|
260
|
+
// Preserve neon's header gap. Float's painted top padding starts
|
|
261
|
+
// on the same body row as the transcript, without an external gap.
|
|
210
262
|
railLines.push("");
|
|
211
263
|
}
|
|
212
264
|
for (const section of sections) {
|
|
213
265
|
// One blank row separates a section from the banner or the
|
|
214
|
-
// previous section;
|
|
215
|
-
if (hits.length > 0 || branding.length > 0) railLines.push("");
|
|
266
|
+
// previous section; neon's header gap above is not a section.
|
|
267
|
+
if ((hits.length > 0 || branding.length > 0) && density() === "comfortable") railLines.push("");
|
|
216
268
|
const startY = railLines.length;
|
|
217
269
|
railLines.push(...section.lines.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
|
|
218
270
|
hits.push({ key: section.key, component: section.component, startY, height: section.lines.length, width: contentWidth - RAIL_PADDING * 2 });
|
|
219
271
|
}
|
|
220
272
|
// Height is owned by the native ScrollView, never by the transcript.
|
|
221
273
|
const active = railLines.length > 0 && railLines.every((line) => visibleWidth(line) <= contentWidth);
|
|
222
|
-
headerLines =
|
|
223
|
-
prepared = { revision: cache.revision, width, mode: host.mode, root, theme, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
|
|
274
|
+
headerLines = headerActive && headerPlacement() === "top" ? preparedHeaderLines : [];
|
|
275
|
+
prepared = { revision: cache.revision, width, mode: host.mode, headerPlacement: headerPlacement(), cardStyle: style, root, theme, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
|
|
224
276
|
state.active = active;
|
|
225
277
|
return active;
|
|
226
278
|
} catch {
|
|
@@ -264,6 +316,11 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
264
316
|
}
|
|
265
317
|
return { ...node, entries: entries.map((entry, index) => index === entries.length - 1 ? { ...entry, component: wrapped! } : entry) };
|
|
266
318
|
};
|
|
319
|
+
// Without a rail the native layout stays in place; a hidden Status, or a
|
|
320
|
+
// narrow top header owning it, only frees the footer's reserved dock row,
|
|
321
|
+
// exactly as the rail does.
|
|
322
|
+
const nativeLayout = () => placement() === "hidden" || headerOwnsStatus() ? reclaimFooterRow(original.call(root)) : original.call(root);
|
|
323
|
+
const nativeHost = { render: () => [], invalidate() {}, [NODE]: nativeLayout };
|
|
267
324
|
const left = { render: () => [], invalidate() {}, [NODE]: () => reclaimFooterRow(original.call(root)) };
|
|
268
325
|
// Stable component wrapping the [left, scroll] hstack behind its own
|
|
269
326
|
// NODE, exactly like `left` wraps the native transcript: the header
|
|
@@ -278,10 +335,17 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
278
335
|
] }),
|
|
279
336
|
};
|
|
280
337
|
const replacement = () => {
|
|
281
|
-
if (!prepare(tui.terminal.columns, root))
|
|
338
|
+
if (!prepare(tui.terminal.columns, root)) {
|
|
339
|
+
if (failed || stopped || host.mode !== "fullscreen") return original.call(root);
|
|
340
|
+
if (!headerLines.length) return nativeLayout();
|
|
341
|
+
return { type: "vstack", gap: 0, align: "stretch", entries: [
|
|
342
|
+
{ component: header, basis: "auto", grow: 0, shrink: 0, minSize: 1 },
|
|
343
|
+
{ component: nativeHost, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
|
344
|
+
] };
|
|
345
|
+
}
|
|
282
346
|
const current = prepared!;
|
|
283
347
|
if (current.presentation?.scrollTop === scroll.scrollTop) return current.presentation.output;
|
|
284
|
-
const output: LayoutNode = current.headerActive
|
|
348
|
+
const output: LayoutNode = current.headerActive && headerPlacement() !== "below-input"
|
|
285
349
|
? { type: "vstack", gap: 0, align: "stretch", entries: [
|
|
286
350
|
{ component: header, basis: "auto", grow: 0, shrink: 0, minSize: 1 },
|
|
287
351
|
{ component: hstackHost, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
package/lib/shell-sidebar.ts
CHANGED
|
@@ -3,9 +3,23 @@ import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
|
3
3
|
// Store on the terminal, not a module singleton: extension loaders may isolate
|
|
4
4
|
// modules, while Pi keeps this terminal across regular/fullscreen transitions.
|
|
5
5
|
const STATE = Symbol.for("gentle-pi.experimental-sidebar.state");
|
|
6
|
+
/** In-process notification after a visual preference is persisted. */
|
|
7
|
+
export const VISUAL_SETTINGS_CHANGED = "gentle-pi.visual-settings-changed";
|
|
6
8
|
export interface SidebarState {
|
|
7
9
|
active: boolean;
|
|
10
|
+
visibility?: { todo?: boolean };
|
|
8
11
|
ownsHost?: () => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Columns the fullscreen rail reserves at the right edge (rail plus gap)
|
|
14
|
+
* while `active && ownsHost()`. Overlays read it to stay inside the editor
|
|
15
|
+
* column (the prompt-history picker's right margin) without importing
|
|
16
|
+
* gentle-shell.
|
|
17
|
+
*/
|
|
18
|
+
railColumns?: number;
|
|
19
|
+
/** True while Status placement is "hidden": the bottom Status bar paints nothing at any width or mode. */
|
|
20
|
+
statusHidden?: () => boolean;
|
|
21
|
+
/** True while a painting top header is the only status row of a narrow fullscreen terminal: the bottom Status bar steps aside. */
|
|
22
|
+
headerOwnsStatus?: () => boolean;
|
|
9
23
|
parts: Map<string, SidebarRail>;
|
|
10
24
|
}
|
|
11
25
|
|
|
@@ -32,7 +46,7 @@ export function sidebarPart<T extends Component & { dispose?(): void }>(tui: TUI
|
|
|
32
46
|
state.parts.set(key, rail);
|
|
33
47
|
return {
|
|
34
48
|
...bottom,
|
|
35
|
-
render: (width: number) => state.active && state.ownsHost?.() ? [] : bottom.render(width),
|
|
49
|
+
render: (width: number) => (key === "todo" && state.visibility?.todo === false) || (key === "footer" && (state.statusHidden?.() || state.headerOwnsStatus?.())) || (state.active && state.ownsHost?.()) ? [] : bottom.render(width),
|
|
36
50
|
dispose() {
|
|
37
51
|
if (state.parts.get(key) === rail) state.parts.delete(key);
|
|
38
52
|
bottom.dispose?.();
|
package/lib/shell-todo.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
-
import { CARD_TONE,
|
|
2
|
+
import { CARD_TONE, panelHeaderRow, panelInnerWidth, renderCard, type CardTheme, type CardTone } from "./shell-card.ts";
|
|
3
3
|
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
4
4
|
import { paintHoverable } from "./shell-hover.ts";
|
|
5
5
|
|
|
@@ -244,11 +244,11 @@ export function todoPromptBlock(state: TodoState, stale: number): string | undef
|
|
|
244
244
|
].join("\n") + staleLine;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
function taskRow(task: TodoTask, theme: TodoTheme,
|
|
247
|
+
function taskRow(task: TodoTask, theme: TodoTheme, inner: number): string {
|
|
248
248
|
if (task.status === TODO_STATUS.DONE) {
|
|
249
249
|
// Wrap before applying SGR 9: every physical title closes its own style
|
|
250
250
|
// before renderCard adds padding and rails.
|
|
251
|
-
return wrapTextWithAnsi(task.title, Math.max(1,
|
|
251
|
+
return wrapTextWithAnsi(task.title, Math.max(1, inner - 2))
|
|
252
252
|
.map((line, index) => `${index === 0 ? theme.fg(GLYPH_ROLE[task.status], STATUS_GLYPH[task.status]) : " "} ${theme.fg(STATUS_ROLE[task.status], theme.strikethrough(line))}`)
|
|
253
253
|
.join("\n");
|
|
254
254
|
}
|
|
@@ -259,27 +259,32 @@ function taskRow(task: TodoTask, theme: TodoTheme, width: number): string {
|
|
|
259
259
|
|
|
260
260
|
// Long lists keep the card short: done tasks fold into "✓ N done" and the
|
|
261
261
|
// open ones fill the remaining rows, with a trailing count for the rest.
|
|
262
|
-
function bodyRows(state: TodoState, theme: TodoTheme,
|
|
263
|
-
if (state.tasks.length <= ROW_CAP) return state.tasks.map((task) => taskRow(task, theme,
|
|
262
|
+
function bodyRows(state: TodoState, theme: TodoTheme, inner: number): string[] {
|
|
263
|
+
if (state.tasks.length <= ROW_CAP) return state.tasks.map((task) => taskRow(task, theme, inner));
|
|
264
264
|
const { done } = todoSummary(state);
|
|
265
265
|
const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE);
|
|
266
266
|
const rows: string[] = [];
|
|
267
267
|
if (done > 0) rows.push(`${theme.fg(GLYPH_ROLE[TODO_STATUS.DONE], STATUS_GLYPH[TODO_STATUS.DONE])} ${theme.fg(NOTE_ROLE, `${done} done`)}`);
|
|
268
268
|
const room = ROW_CAP - rows.length - (open.length > ROW_CAP - rows.length ? 1 : 0);
|
|
269
|
-
for (const task of open.slice(0, room)) rows.push(taskRow(task, theme,
|
|
269
|
+
for (const task of open.slice(0, room)) rows.push(taskRow(task, theme, inner));
|
|
270
270
|
if (open.length > room) rows.push(theme.fg(NOTE_ROLE, `… ${open.length - room} more`));
|
|
271
271
|
return rows;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
function collapsedRow(state: TodoState, theme: TodoTheme,
|
|
274
|
+
function collapsedRow(state: TodoState, theme: TodoTheme, inner: number): string {
|
|
275
275
|
const active = state.tasks.find((task) => task.status === TODO_STATUS.IN_PROGRESS);
|
|
276
|
-
if (active) return taskRow(active, theme,
|
|
276
|
+
if (active) return taskRow(active, theme, inner);
|
|
277
277
|
const pending = state.tasks.find((task) => task.status === TODO_STATUS.PENDING);
|
|
278
|
-
if (pending) return taskRow(pending, theme,
|
|
278
|
+
if (pending) return taskRow(pending, theme, inner);
|
|
279
279
|
const { open } = todoSummary(state);
|
|
280
280
|
return `${theme.fg(GLYPH_ROLE[TODO_STATUS.PENDING], STATUS_GLYPH[TODO_STATUS.PENDING])} ${theme.fg(NOTE_ROLE, `${open} open`)}`;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/** The tone the Todos card renders in: warning once the list went stale, info otherwise. */
|
|
284
|
+
export function todoCardTone(staleTurns: number): CardTone {
|
|
285
|
+
return staleTurns >= STALE_AFTER_TURNS ? CARD_TONE.WARNING : CARD_TONE.INFO;
|
|
286
|
+
}
|
|
287
|
+
|
|
283
288
|
export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number, options: TodoRenderOptions): string[] {
|
|
284
289
|
if (state.tasks.length === 0) return [];
|
|
285
290
|
const { done, total } = todoSummary(state);
|
|
@@ -288,16 +293,21 @@ export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number
|
|
|
288
293
|
const actionLabel = action[0]!.toUpperCase() + action.slice(1);
|
|
289
294
|
const icon = options.collapsed ? "▸" : "▾";
|
|
290
295
|
const control = `${icon} ${width >= 28 ? actionLabel : ""}`.trimEnd();
|
|
291
|
-
const
|
|
292
|
-
const
|
|
296
|
+
const tone = todoCardTone(options.staleTurns);
|
|
297
|
+
const hint = options.collapseKey
|
|
298
|
+
? panelHeaderRow(theme, width, tone) ? options.collapseKey : `${options.collapseKey} ${action}`
|
|
299
|
+
: undefined;
|
|
300
|
+
// Content columns of the card body in the active style (float is narrower).
|
|
301
|
+
const inner = panelInnerWidth(theme, width, tone);
|
|
302
|
+
const rows = options.collapsed ? [collapsedRow(state, theme, inner)] : options.scrollable ? state.tasks.map((task) => taskRow(task, theme, inner)) : bodyRows(state, theme, inner);
|
|
293
303
|
const body = stale ? [theme.fg(NOTE_ROLE, `stale · ${options.staleTurns} turns`), ...rows] : rows;
|
|
294
304
|
// The clickable control paints the shared hover role while hovered --
|
|
295
305
|
// same treatment every other clickable surface uses -- instead of its
|
|
296
306
|
// ordinary accent role.
|
|
297
307
|
return renderCard(
|
|
298
|
-
{ title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${total}`, body, tone
|
|
308
|
+
{ title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${total}`, body, tone, glyph: TODO_GLYPH },
|
|
299
309
|
theme,
|
|
300
310
|
width,
|
|
301
|
-
{ expanded: true, hint },
|
|
311
|
+
{ expanded: true, hint, panel: true },
|
|
302
312
|
);
|
|
303
313
|
}
|
package/lib/shell-usage-view.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Key, matchesKey, truncateToWidth, visibleWidth, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
2
|
-
import { renderUsagePanel, type ActiveProvider, type UsageSourceRegistry, type UsageStore, type UsageTheme } from "./shell-usage.ts";
|
|
2
|
+
import { renderUsagePanel, type ActiveProvider, type UsagePanelScope, type UsageSourceRegistry, type UsageStore, type UsageTheme } from "./shell-usage.ts";
|
|
3
3
|
import { paintHoverable } from "./shell-hover.ts";
|
|
4
4
|
|
|
5
5
|
// Gentle Shell subscriptions overlay: a framed panel over the usage store.
|
|
@@ -12,6 +12,10 @@ export interface UsageViewDeps {
|
|
|
12
12
|
// Optional: lets the panel resolve the pending note of a provider whose
|
|
13
13
|
// usage source was registered at runtime instead of built in.
|
|
14
14
|
registry?(): UsageSourceRegistry | undefined;
|
|
15
|
+
// Optional: the targeted routing scope. Without it the panel draws every
|
|
16
|
+
// provider the store holds; with it, exactly the targeted providers, in
|
|
17
|
+
// scope order, explaining the ones without data.
|
|
18
|
+
scope?(): UsagePanelScope | undefined;
|
|
15
19
|
onRefresh(): Promise<void>;
|
|
16
20
|
onClose(): void;
|
|
17
21
|
requestRender(): void;
|
|
@@ -80,8 +84,9 @@ export class UsageView {
|
|
|
80
84
|
// A failing usage fetch is the store's problem to report (its rows already
|
|
81
85
|
// carry the last error); the panel only clears its "refreshing" state. The
|
|
82
86
|
// rejection must never leave this method: an unhandled rejection is fatal
|
|
83
|
-
// to the whole shell on current Node.
|
|
84
|
-
|
|
87
|
+
// to the whole shell on current Node. Public so the overlay can dispatch
|
|
88
|
+
// its opening refresh instead of making the open wait for it.
|
|
89
|
+
refresh(): void {
|
|
85
90
|
if (this.refreshing) return;
|
|
86
91
|
this.refreshing = true;
|
|
87
92
|
this.deps.requestRender();
|
|
@@ -101,7 +106,7 @@ export class UsageView {
|
|
|
101
106
|
const inner = width - 2;
|
|
102
107
|
const title = this.refreshing ? REFRESHING : TITLE;
|
|
103
108
|
const top = theme.fg(FRAME_ROLE, "╭─ ") + theme.fg(TITLE_ROLE, title) + theme.fg(FRAME_ROLE, ` ${rule(inner - visibleWidth(title) - 3)}╮`);
|
|
104
|
-
const body = renderUsagePanel(this.store.all(), theme, inner - 2, this.deps.now(), this.deps.active(), this.deps.registry?.()).map(
|
|
109
|
+
const body = renderUsagePanel(this.store.all(), theme, inner - 2, this.deps.now(), this.deps.active(), this.deps.registry?.(), this.deps.scope?.()).map(
|
|
105
110
|
(line) => `${theme.fg(FRAME_ROLE, "│")} ${fit(line, inner - 2)} ${theme.fg(FRAME_ROLE, "│")}`,
|
|
106
111
|
);
|
|
107
112
|
const hints = KEYS.map(([key, label]) => ({ key, label, text: `${key} ${label}`, action: (key === "r" ? "refresh" : "close") as HintAction }));
|
package/lib/shell-usage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { parseModelRef } from "./agents-config.ts";
|
|
2
3
|
import { paintGauge } from "./shell-gauge.ts";
|
|
3
4
|
|
|
4
5
|
// Gentle Shell subscription usage: the rate-limit windows each connected
|
|
@@ -125,6 +126,10 @@ const PENDING_NOTE: Record<string, string> = {
|
|
|
125
126
|
};
|
|
126
127
|
const UNSUPPORTED_NOTE = "no subscription usage for this provider";
|
|
127
128
|
const ACTIVE_MARK = "✿";
|
|
129
|
+
// What a targeted provider with no snapshot says after a refresh actually ran
|
|
130
|
+
// and answered nothing. Generic on purpose: it names no endpoint, key, or
|
|
131
|
+
// account detail, exactly like the other notes here.
|
|
132
|
+
export const USAGE_FETCH_FAILED_NOTE = "fetch failed · r to retry";
|
|
128
133
|
|
|
129
134
|
export interface ActiveProvider {
|
|
130
135
|
provider: string;
|
|
@@ -289,7 +294,7 @@ export function parseCodexUsage(payload: unknown, now: number): ProviderUsage {
|
|
|
289
294
|
return { provider: CODEX_PROVIDER, plan: typeof raw.plan_type === "string" ? raw.plan_type : undefined, limits, fetchedAt: now };
|
|
290
295
|
}
|
|
291
296
|
|
|
292
|
-
function headerWindow(headers: Record<string, string>, kind: "primary" | "secondary"
|
|
297
|
+
function headerWindow(headers: Record<string, string>, kind: "primary" | "secondary"): UsageWindow | undefined {
|
|
293
298
|
const used = Number.parseFloat(headers[`${HEADER_PREFIX}${kind}-used-percent`] ?? "");
|
|
294
299
|
if (!Number.isFinite(used)) return undefined;
|
|
295
300
|
const minutes = Number.parseInt(headers[`${HEADER_PREFIX}${kind}-window-minutes`] ?? "", 10);
|
|
@@ -299,7 +304,7 @@ function headerWindow(headers: Record<string, string>, kind: "primary" | "second
|
|
|
299
304
|
}
|
|
300
305
|
|
|
301
306
|
export function parseCodexHeaders(headers: Record<string, string>, now: number): ProviderUsage | undefined {
|
|
302
|
-
const windows = [headerWindow(headers, "primary"
|
|
307
|
+
const windows = [headerWindow(headers, "primary"), headerWindow(headers, "secondary")].filter((window): window is UsageWindow => window !== undefined);
|
|
303
308
|
if (windows.length === 0) return undefined;
|
|
304
309
|
const reached = headers[`${HEADER_PREFIX}rate-limit-reached-type`];
|
|
305
310
|
return { provider: CODEX_PROVIDER, plan: undefined, limits: [{ name: CODEX_MAIN_LIMIT, windows, limitReached: Boolean(reached) }], fetchedAt: now };
|
|
@@ -527,18 +532,66 @@ function updatedAgo(fetchedAt: number, now: number): string {
|
|
|
527
532
|
return minutes < 1 ? "updated just now" : `updated ${minutes}m ago`;
|
|
528
533
|
}
|
|
529
534
|
|
|
535
|
+
// The providers one refresh targets: the session's own provider first, then
|
|
536
|
+
// every provider the active profile's subagent routing names, deduplicated in
|
|
537
|
+
// first-seen order. A qualified ref ("provider/id") resolves directly; a bare
|
|
538
|
+
// model id resolves through the registry callback only when that callback can
|
|
539
|
+
// name exactly one provider, so an ambiguous or unknown ref never guesses one
|
|
540
|
+
// and is dropped instead.
|
|
541
|
+
export function usageScopeProviders(
|
|
542
|
+
mainProvider: string | undefined,
|
|
543
|
+
routingModels: ReadonlyArray<string | undefined> | undefined,
|
|
544
|
+
resolveBareModel?: (modelId: string) => string | undefined,
|
|
545
|
+
): string[] {
|
|
546
|
+
const providers = new Set<string>();
|
|
547
|
+
if (mainProvider) providers.add(mainProvider);
|
|
548
|
+
for (const model of routingModels ?? []) {
|
|
549
|
+
const ref = parseModelRef(model);
|
|
550
|
+
if (!ref) continue;
|
|
551
|
+
const provider = ref.provider ?? resolveBareModel?.(ref.id);
|
|
552
|
+
if (provider) providers.add(provider);
|
|
553
|
+
}
|
|
554
|
+
return [...providers];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// The current routing scope the panel draws: which providers are targeted, and
|
|
558
|
+
// which of them failed their latest settled refresh. A provider recorded
|
|
559
|
+
// earlier but no longer targeted is simply absent — it is not current scope.
|
|
560
|
+
export interface UsagePanelScope {
|
|
561
|
+
providers: readonly string[];
|
|
562
|
+
// Actual failures only: a refresh that settled without a snapshot. A
|
|
563
|
+
// provider still fetching is never in here, so an in-flight refresh cannot
|
|
564
|
+
// read as failed, and a successful refresh removes the provider again.
|
|
565
|
+
failed: ReadonlySet<string>;
|
|
566
|
+
}
|
|
567
|
+
|
|
530
568
|
// The active provider comes first, marked with the petal, and explains
|
|
531
569
|
// itself when it has no data yet. Other providers seen this session follow.
|
|
532
|
-
|
|
570
|
+
// With a scope, the panel draws exactly the targeted providers in scope order:
|
|
571
|
+
// one with no snapshot explains itself (its latest refresh failed, or its
|
|
572
|
+
// pending note otherwise), one whose latest refresh failed after a good
|
|
573
|
+
// snapshot keeps that snapshot and says so beside it, and a provider recorded
|
|
574
|
+
// under a previous profile's routing is never presented as current scope after
|
|
575
|
+
// a profile switch.
|
|
576
|
+
export function renderUsagePanel(usages: ProviderUsage[], theme: UsageTheme, width: number, now: number, active?: ActiveProvider, registry?: UsageSourceRegistry, scope?: UsagePanelScope): string[] {
|
|
533
577
|
const activeUsage = active ? usages.find((usage) => usage.provider === active.provider) : undefined;
|
|
534
|
-
const
|
|
535
|
-
|
|
578
|
+
const byProvider = new Map(usages.map((usage) => [usage.provider, usage] as const));
|
|
579
|
+
const rows = scope
|
|
580
|
+
? scope.providers.map((provider) => ({ provider, usage: byProvider.get(provider) }))
|
|
581
|
+
: [...(active ? [{ provider: active.provider, usage: activeUsage }] : []), ...usages.filter((usage) => usage !== activeUsage).map((usage) => ({ provider: usage.provider, usage }))];
|
|
582
|
+
if (rows.length === 0) return [truncateToWidth(USAGE_EMPTY_MESSAGE, width, "…")];
|
|
536
583
|
const lines: string[] = [];
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
584
|
+
for (const row of rows) {
|
|
585
|
+
const mark = active?.provider === row.provider ? `${theme.fg(ROLE.LIMIT, ACTIVE_MARK)} ` : "";
|
|
586
|
+
if (!row.usage) {
|
|
587
|
+
// A settled refresh that answered nothing is a failure; a provider
|
|
588
|
+
// whose usage arrives with responses (Anthropic) or that has not
|
|
589
|
+
// failed keeps its pending note.
|
|
590
|
+
const note = scope?.failed.has(row.provider) ? USAGE_FETCH_FAILED_NOTE : providerNote(row.provider, registry);
|
|
591
|
+
lines.push(`${mark}${theme.fg(ROLE.PROVIDER, row.provider)} ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.RESET, note)}`);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const usage = row.usage;
|
|
542
595
|
const plan = usage.plan ? ` ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.PLAN, usage.plan)}` : "";
|
|
543
596
|
lines.push(`${mark}${theme.fg(ROLE.PROVIDER, usage.provider)}${plan} ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.RESET, updatedAgo(usage.fetchedAt, now))}`);
|
|
544
597
|
// One row per window: the limit name, its meter, its percentage and the reset
|
|
@@ -555,6 +608,9 @@ export function renderUsagePanel(usages: ProviderUsage[], theme: UsageTheme, wid
|
|
|
555
608
|
const tail = reset.length > 0 ? ` ${theme.fg(ROLE.SEPARATOR, "·")} ${theme.fg(ROLE.RESET, reset)}` : "";
|
|
556
609
|
lines.push(` ${theme.fg(ROLE.LABEL, row.name.padEnd(nameWidth))} ${paintMeter(row.window.usedPercent, PANEL_METER_CELLS, theme)} ${theme.fg(ROLE.PERCENT, percent)}${tail}`);
|
|
557
610
|
}
|
|
611
|
+
// A failure after a good snapshot never hides either fact: the last good
|
|
612
|
+
// snapshot stays on its rows and the failed refresh says so beside it.
|
|
613
|
+
if (scope?.failed.has(row.provider)) lines.push(` ${theme.fg(ROLE.RESET, USAGE_FETCH_FAILED_NOTE)}`);
|
|
558
614
|
}
|
|
559
615
|
return lines.map((line) => truncateToWidth(line, width, "…"));
|
|
560
616
|
}
|