@oh-my-pi/pi-coding-agent 15.5.13 → 15.6.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/CHANGELOG.md +77 -0
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/tiny-models-cli.d.ts +9 -0
- package/dist/types/commands/tiny-models.d.ts +22 -0
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/config/model-id-affixes.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +233 -17
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/eval/__tests__/llm-bridge.test.d.ts +1 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
- package/dist/types/eval/llm-bridge.d.ts +25 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +15 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
- package/dist/types/internal-urls/local-protocol.d.ts +2 -1
- package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
- package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
- package/dist/types/internal-urls/router.d.ts +8 -1
- package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
- package/dist/types/internal-urls/types.d.ts +26 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/resolve.d.ts +2 -1
- package/dist/types/memory-backend/types.d.ts +7 -1
- package/dist/types/mnemosyne/backend.d.ts +4 -0
- package/dist/types/mnemosyne/config.d.ts +29 -0
- package/dist/types/mnemosyne/index.d.ts +3 -0
- package/dist/types/mnemosyne/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -3
- package/dist/types/modes/components/hook-selector.d.ts +27 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
- package/dist/types/modes/gradient-highlight.d.ts +23 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
- package/dist/types/modes/orchestrate.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/ultrathink.d.ts +3 -3
- package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +35 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/render.d.ts +5 -1
- package/dist/types/tiny/models.d.ts +185 -0
- package/dist/types/tiny/text.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +24 -0
- package/dist/types/tiny/title-protocol.d.ts +74 -0
- package/dist/types/tiny/worker.d.ts +2 -0
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/index.d.ts +7 -4
- package/dist/types/tools/memory-edit.d.ts +40 -0
- package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
- package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
- package/dist/types/tools/memory-render.d.ts +60 -0
- package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
- package/dist/types/tools/todo-write.d.ts +8 -0
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +3 -0
- package/package.json +18 -14
- package/scripts/build-binary.ts +1 -0
- package/src/cli/tiny-models-cli.ts +127 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +8 -8
- package/src/commands/tiny-models.ts +36 -0
- package/src/config/model-equivalence.ts +43 -2
- package/src/config/model-id-affixes.ts +64 -0
- package/src/config/model-registry.ts +166 -8
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +206 -14
- package/src/edit/hashline/diff.ts +5 -7
- package/src/eval/__tests__/llm-bridge.test.ts +297 -0
- package/src/eval/__tests__/shared-executors.test.ts +36 -0
- package/src/eval/js/shared/local-module-loader.ts +13 -1
- package/src/eval/js/shared/prelude.txt +8 -0
- package/src/eval/js/shared/rewrite-imports.ts +31 -26
- package/src/eval/js/tool-bridge.ts +4 -0
- package/src/eval/llm-bridge.ts +181 -0
- package/src/eval/py/prelude.py +52 -31
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -13
- package/src/extensibility/plugins/legacy-pi-compat.ts +60 -23
- package/src/internal-urls/agent-protocol.ts +18 -1
- package/src/internal-urls/artifact-protocol.ts +19 -1
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/internal-urls/local-protocol.ts +14 -1
- package/src/internal-urls/memory-protocol.ts +6 -1
- package/src/internal-urls/omp-protocol.ts +5 -1
- package/src/internal-urls/router.ts +20 -1
- package/src/internal-urls/rule-protocol.ts +8 -1
- package/src/internal-urls/skill-protocol.ts +8 -1
- package/src/internal-urls/types.ts +27 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +4 -0
- package/src/mcp/oauth-flow.ts +2 -2
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/resolve.ts +4 -1
- package/src/memory-backend/types.ts +8 -1
- package/src/mnemosyne/backend.ts +374 -0
- package/src/mnemosyne/config.ts +160 -0
- package/src/mnemosyne/index.ts +3 -0
- package/src/mnemosyne/state.ts +548 -0
- package/src/modes/acp/acp-agent.ts +11 -6
- package/src/modes/components/agent-dashboard.ts +4 -4
- package/src/modes/components/custom-editor.ts +3 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/extensions/extension-list.ts +3 -2
- package/src/modes/components/footer.ts +5 -6
- package/src/modes/components/history-search.ts +3 -3
- package/src/modes/components/hook-selector.ts +94 -8
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/mcp-add-wizard.ts +3 -3
- package/src/modes/components/model-selector.ts +124 -26
- package/src/modes/components/oauth-selector.ts +3 -3
- package/src/modes/components/session-observer-overlay.ts +19 -13
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/context-thresholds.ts +11 -0
- package/src/modes/components/status-line/presets.ts +1 -0
- package/src/modes/components/status-line/segments.ts +25 -2
- package/src/modes/components/tiny-title-download-progress.ts +90 -0
- package/src/modes/components/tips.txt +12 -0
- package/src/modes/components/tool-execution.ts +67 -3
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +3 -3
- package/src/modes/components/welcome.ts +55 -1
- package/src/modes/controllers/command-controller.ts +16 -1
- package/src/modes/controllers/extension-ui-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +57 -0
- package/src/modes/gradient-highlight.ts +70 -0
- package/src/modes/interactive-mode.ts +80 -196
- package/src/modes/internal-url-autocomplete.ts +143 -0
- package/src/modes/orchestrate.ts +36 -0
- package/src/modes/prompt-action-autocomplete.ts +12 -0
- package/src/modes/theme/theme.ts +7 -0
- package/src/modes/ultrathink.ts +9 -53
- package/src/modes/utils/keybinding-matchers.ts +11 -0
- package/src/prompts/system/memory-consolidation-system.md +8 -0
- package/src/prompts/system/memory-extraction-system.md +26 -0
- package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +5 -16
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/prompts/system/tiny-title-system.md +8 -0
- package/src/prompts/tools/eval.md +2 -0
- package/src/prompts/tools/memory-edit.md +8 -0
- package/src/prompts/tools/task.md +4 -7
- package/src/sdk.ts +8 -6
- package/src/session/agent-session.ts +147 -44
- package/src/session/session-manager.ts +47 -0
- package/src/slash-commands/builtin-registry.ts +10 -1
- package/src/system-prompt.ts +4 -0
- package/src/task/commands.ts +1 -5
- package/src/task/executor.ts +8 -0
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +69 -26
- package/src/tiny/models.ts +217 -0
- package/src/tiny/text.ts +19 -0
- package/src/tiny/title-client.ts +340 -0
- package/src/tiny/title-protocol.ts +51 -0
- package/src/tiny/worker.ts +523 -0
- package/src/tools/bash.ts +58 -16
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval.ts +24 -48
- package/src/tools/index.ts +17 -15
- package/src/tools/memory-edit.ts +59 -0
- package/src/tools/memory-recall.ts +100 -0
- package/src/tools/memory-reflect.ts +88 -0
- package/src/tools/memory-render.ts +185 -0
- package/src/tools/memory-retain.ts +91 -0
- package/src/tools/renderers.ts +4 -2
- package/src/tools/todo-write.ts +128 -29
- package/src/tools/tool-result.ts +8 -0
- package/src/utils/title-generator.ts +115 -13
- package/dist/types/tools/calculator.d.ts +0 -77
- package/src/prompts/tools/calculator.md +0 -10
- package/src/tools/calculator.ts +0 -541
- package/src/tools/hindsight-recall.ts +0 -69
- package/src/tools/hindsight-reflect.ts +0 -58
- package/src/tools/hindsight-retain.ts +0 -57
|
@@ -7,7 +7,7 @@ import type { AgentSession } from "../../session/agent-session";
|
|
|
7
7
|
import { shortenPath } from "../../tools/render-utils";
|
|
8
8
|
import * as git from "../../utils/git";
|
|
9
9
|
import { sanitizeStatusText } from "../shared";
|
|
10
|
-
import { getContextUsageLevel, getContextUsageThemeColor } from "./status-line/context-thresholds";
|
|
10
|
+
import { formatContextUsage, getContextUsageLevel, getContextUsageThemeColor } from "./status-line/context-thresholds";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Footer component that shows pwd, token stats, and context usage
|
|
@@ -136,7 +136,6 @@ export class FooterComponent implements Component {
|
|
|
136
136
|
const contextUsage = this.session.getContextUsage();
|
|
137
137
|
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
138
138
|
const contextPercentValue = contextUsage?.percent ?? 0;
|
|
139
|
-
const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
140
139
|
|
|
141
140
|
// Replace home directory with ~
|
|
142
141
|
let pwd = shortenPath(getProjectDir());
|
|
@@ -180,10 +179,10 @@ export class FooterComponent implements Component {
|
|
|
180
179
|
// Colorize context percentage based on usage
|
|
181
180
|
let contextPercentStr: string;
|
|
182
181
|
const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
|
|
183
|
-
const contextPercentDisplay =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
const contextPercentDisplay = `${formatContextUsage(
|
|
183
|
+
contextUsage?.percent === null ? null : contextPercentValue,
|
|
184
|
+
contextWindow,
|
|
185
|
+
)}${autoIndicator}`;
|
|
187
186
|
if (contextUsage?.percent !== null && contextUsage?.percent !== undefined) {
|
|
188
187
|
const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
|
|
189
188
|
contextPercentStr =
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
visibleWidth,
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { theme } from "../../modes/theme/theme";
|
|
14
|
-
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
14
|
+
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
15
15
|
import type { HistoryEntry, HistoryStorage } from "../../session/history-storage";
|
|
16
16
|
import { DynamicBorder } from "./dynamic-border";
|
|
17
17
|
|
|
@@ -116,14 +116,14 @@ export class HistorySearchComponent extends Container {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
handleInput(keyData: string): void {
|
|
119
|
-
if (
|
|
119
|
+
if (matchesSelectUp(keyData)) {
|
|
120
120
|
if (this.#results.length === 0) return;
|
|
121
121
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
122
122
|
this.#resultsList.setSelectedIndex(this.#selectedIndex);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
if (
|
|
126
|
+
if (matchesSelectDown(keyData)) {
|
|
127
127
|
if (this.#results.length === 0) return;
|
|
128
128
|
this.#selectedIndex = Math.min(this.#results.length - 1, this.#selectedIndex + 1);
|
|
129
129
|
this.#resultsList.setSelectedIndex(this.#selectedIndex);
|
|
@@ -15,11 +15,43 @@ import {
|
|
|
15
15
|
truncateToWidth,
|
|
16
16
|
visibleWidth,
|
|
17
17
|
} from "@oh-my-pi/pi-tui";
|
|
18
|
-
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
19
|
-
import {
|
|
18
|
+
import { getMarkdownTheme, type ThemeColor, theme } from "../../modes/theme/theme";
|
|
19
|
+
import {
|
|
20
|
+
matchesAppExternalEditor,
|
|
21
|
+
matchesSelectCancel,
|
|
22
|
+
matchesSelectDown,
|
|
23
|
+
matchesSelectUp,
|
|
24
|
+
} from "../../modes/utils/keybinding-matchers";
|
|
20
25
|
import { CountdownTimer } from "./countdown-timer";
|
|
21
26
|
import { DynamicBorder } from "./dynamic-border";
|
|
22
27
|
|
|
28
|
+
/** One segment of a {@link HookSelectorSlider} — a label, its accent color, and
|
|
29
|
+
* an optional detail line (e.g. the resolved model name) shown beneath the
|
|
30
|
+
* track while the segment is active. */
|
|
31
|
+
export interface HookSelectorSliderSegment {
|
|
32
|
+
label: string;
|
|
33
|
+
/** Theme color for the segment label; defaults to `accent`. */
|
|
34
|
+
color?: ThemeColor;
|
|
35
|
+
/** Secondary line rendered under the track when this segment is selected. */
|
|
36
|
+
detail?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A horizontal left/right selector rendered above the option list. Unlike the
|
|
41
|
+
* up/down option cursor, the slider is moved with the left/right arrows from
|
|
42
|
+
* any list position, letting the caller capture an orthogonal choice (e.g. the
|
|
43
|
+
* model tier to continue execution with) alongside the selected option.
|
|
44
|
+
*/
|
|
45
|
+
export interface HookSelectorSlider {
|
|
46
|
+
/** Dim caption rendered before the slider track (e.g. "continue with"). */
|
|
47
|
+
caption?: string;
|
|
48
|
+
segments: HookSelectorSliderSegment[];
|
|
49
|
+
/** Initially highlighted segment index. */
|
|
50
|
+
index: number;
|
|
51
|
+
/** Invoked with the new index whenever the slider moves. */
|
|
52
|
+
onChange?: (index: number) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
export interface HookSelectorOptions {
|
|
24
56
|
tui?: TUI;
|
|
25
57
|
timeout?: number;
|
|
@@ -31,6 +63,7 @@ export interface HookSelectorOptions {
|
|
|
31
63
|
onRight?: () => void;
|
|
32
64
|
onExternalEditor?: () => void;
|
|
33
65
|
helpText?: string;
|
|
66
|
+
slider?: HookSelectorSlider;
|
|
34
67
|
}
|
|
35
68
|
|
|
36
69
|
class OutlinedList extends Container {
|
|
@@ -69,6 +102,9 @@ export class HookSelectorComponent extends Container {
|
|
|
69
102
|
#onLeftCallback: (() => void) | undefined;
|
|
70
103
|
#onRightCallback: (() => void) | undefined;
|
|
71
104
|
#onExternalEditorCallback: (() => void) | undefined;
|
|
105
|
+
#slider: HookSelectorSlider | undefined;
|
|
106
|
+
#sliderIndex: number = 0;
|
|
107
|
+
#sliderComponent: Text | undefined;
|
|
72
108
|
constructor(
|
|
73
109
|
title: string,
|
|
74
110
|
options: string[],
|
|
@@ -87,6 +123,10 @@ export class HookSelectorComponent extends Container {
|
|
|
87
123
|
this.#onLeftCallback = opts?.onLeft;
|
|
88
124
|
this.#onRightCallback = opts?.onRight;
|
|
89
125
|
this.#onExternalEditorCallback = opts?.onExternalEditor;
|
|
126
|
+
if (opts?.slider && opts.slider.segments.length > 0) {
|
|
127
|
+
this.#slider = opts.slider;
|
|
128
|
+
this.#sliderIndex = Math.max(0, Math.min(opts.slider.index, opts.slider.segments.length - 1));
|
|
129
|
+
}
|
|
90
130
|
|
|
91
131
|
this.addChild(new DynamicBorder());
|
|
92
132
|
this.addChild(new Spacer(1));
|
|
@@ -95,6 +135,12 @@ export class HookSelectorComponent extends Container {
|
|
|
95
135
|
this.addChild(this.#titleComponent);
|
|
96
136
|
this.addChild(new Spacer(1));
|
|
97
137
|
|
|
138
|
+
if (this.#slider) {
|
|
139
|
+
this.#sliderComponent = new Text(this.#renderSliderLine(), 1, 0);
|
|
140
|
+
this.addChild(this.#sliderComponent);
|
|
141
|
+
this.addChild(new Spacer(1));
|
|
142
|
+
}
|
|
143
|
+
|
|
98
144
|
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
|
99
145
|
this.#countdown = new CountdownTimer(
|
|
100
146
|
opts.timeout,
|
|
@@ -160,23 +206,63 @@ export class HookSelectorComponent extends Container {
|
|
|
160
206
|
}
|
|
161
207
|
}
|
|
162
208
|
|
|
209
|
+
/** Render the slider block: the track (dim caption, edge arrows that brighten
|
|
210
|
+
* while there is room to move, one styled segment per option — active = bold
|
|
211
|
+
* in its color, the rest dim, joined by `›`) plus, when the active segment
|
|
212
|
+
* carries a `detail`, a muted second line beneath it (e.g. the resolved model
|
|
213
|
+
* name). Returns one or two `\n`-joined lines. */
|
|
214
|
+
#renderSliderLine(): string {
|
|
215
|
+
const slider = this.#slider;
|
|
216
|
+
if (!slider) return "";
|
|
217
|
+
const segments = slider.segments;
|
|
218
|
+
const sep = theme.fg("dim", " › ");
|
|
219
|
+
const track = segments
|
|
220
|
+
.map((segment, i) =>
|
|
221
|
+
i === this.#sliderIndex
|
|
222
|
+
? theme.bold(theme.fg(segment.color ?? "accent", segment.label))
|
|
223
|
+
: theme.fg("dim", segment.label),
|
|
224
|
+
)
|
|
225
|
+
.join(sep);
|
|
226
|
+
const leftArrow = theme.fg(this.#sliderIndex > 0 ? "accent" : "dim", "◂");
|
|
227
|
+
const rightArrow = theme.fg(this.#sliderIndex < segments.length - 1 ? "accent" : "dim", "▸");
|
|
228
|
+
const caption = slider.caption ? `${theme.fg("dim", slider.caption)} ` : "";
|
|
229
|
+
const trackLine = `${caption}${leftArrow} ${theme.fg("dim", "[")} ${track} ${theme.fg("dim", "]")} ${rightArrow}`;
|
|
230
|
+
const detail = segments[this.#sliderIndex]?.detail;
|
|
231
|
+
if (!detail) return trackLine;
|
|
232
|
+
return `${trackLine}\n ${theme.fg("dim", "↳")} ${theme.fg("muted", detail)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Move the slider by `delta`, clamped to the segment range, refresh the
|
|
236
|
+
* rendered track, and notify the caller only when the index actually moves. */
|
|
237
|
+
#moveSlider(delta: number): void {
|
|
238
|
+
const slider = this.#slider;
|
|
239
|
+
if (!slider) return;
|
|
240
|
+
const next = Math.max(0, Math.min(slider.segments.length - 1, this.#sliderIndex + delta));
|
|
241
|
+
if (next === this.#sliderIndex) return;
|
|
242
|
+
this.#sliderIndex = next;
|
|
243
|
+
this.#sliderComponent?.setText(this.#renderSliderLine());
|
|
244
|
+
slider.onChange?.(next);
|
|
245
|
+
}
|
|
246
|
+
|
|
163
247
|
handleInput(keyData: string): void {
|
|
164
248
|
// Reset countdown on any interaction
|
|
165
249
|
this.#countdown?.reset();
|
|
166
250
|
|
|
167
|
-
if (
|
|
251
|
+
if (matchesSelectUp(keyData) || keyData === "k") {
|
|
168
252
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
169
253
|
this.#updateList();
|
|
170
|
-
} else if (
|
|
254
|
+
} else if (matchesSelectDown(keyData) || keyData === "j") {
|
|
171
255
|
this.#selectedIndex = Math.min(this.#options.length - 1, this.#selectedIndex + 1);
|
|
172
256
|
this.#updateList();
|
|
173
257
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
174
258
|
const selected = this.#options[this.#selectedIndex];
|
|
175
259
|
if (selected) this.#onSelectCallback(selected);
|
|
176
|
-
} else if (matchesKey(keyData, "left")) {
|
|
177
|
-
this.#
|
|
178
|
-
|
|
179
|
-
|
|
260
|
+
} else if (matchesKey(keyData, "left") || (this.#slider && keyData === "h")) {
|
|
261
|
+
if (this.#slider) this.#moveSlider(-1);
|
|
262
|
+
else this.#onLeftCallback?.();
|
|
263
|
+
} else if (matchesKey(keyData, "right") || (this.#slider && keyData === "l")) {
|
|
264
|
+
if (this.#slider) this.#moveSlider(1);
|
|
265
|
+
else this.#onRightCallback?.();
|
|
180
266
|
} else if (this.#onExternalEditorCallback && matchesAppExternalEditor(keyData)) {
|
|
181
267
|
this.#onExternalEditorCallback();
|
|
182
268
|
} else if (matchesSelectCancel(keyData)) {
|
|
@@ -26,6 +26,7 @@ export * from "./show-images-selector";
|
|
|
26
26
|
export * from "./status-line";
|
|
27
27
|
export * from "./theme-selector";
|
|
28
28
|
export * from "./thinking-selector";
|
|
29
|
+
export * from "./tiny-title-download-progress";
|
|
29
30
|
export * from "./todo-reminder";
|
|
30
31
|
export * from "./tool-execution";
|
|
31
32
|
export * from "./tree-selector";
|
|
@@ -19,7 +19,7 @@ import { analyzeAuthError, discoverOAuthEndpoints } from "../../mcp/oauth-discov
|
|
|
19
19
|
import type { MCPHttpServerConfig, MCPServerConfig, MCPSseServerConfig, MCPStdioServerConfig } from "../../mcp/types";
|
|
20
20
|
import { shortenPath } from "../../tools/render-utils";
|
|
21
21
|
import { theme } from "../theme/theme";
|
|
22
|
-
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
22
|
+
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
23
23
|
import { DynamicBorder } from "./dynamic-border";
|
|
24
24
|
|
|
25
25
|
type TransportType = "stdio" | "http" | "sse";
|
|
@@ -501,11 +501,11 @@ export class MCPAddWizard extends Container {
|
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
// Handle up/down arrows for selectors
|
|
504
|
-
if (
|
|
504
|
+
if (matchesSelectUp(keyData)) {
|
|
505
505
|
this.#moveSelection(-1);
|
|
506
506
|
return;
|
|
507
507
|
}
|
|
508
|
-
if (
|
|
508
|
+
if (matchesSelectDown(keyData)) {
|
|
509
509
|
this.#moveSelection(1);
|
|
510
510
|
return;
|
|
511
511
|
}
|
|
@@ -18,6 +18,7 @@ import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../..
|
|
|
18
18
|
import { resolveModelRoleValue } from "../../config/model-resolver";
|
|
19
19
|
import type { Settings } from "../../config/settings";
|
|
20
20
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
21
|
+
import { matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
21
22
|
import { getThinkingLevelMetadata } from "../../thinking";
|
|
22
23
|
import { getTabBarTheme } from "../shared";
|
|
23
24
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -105,6 +106,8 @@ const STATIC_PROVIDER_TABS: ProviderTabState[] = [
|
|
|
105
106
|
{ id: CANONICAL_TAB, label: CANONICAL_TAB },
|
|
106
107
|
];
|
|
107
108
|
|
|
109
|
+
const MODEL_TAB_REFRESH_DEBOUNCE_MS = 120;
|
|
110
|
+
|
|
108
111
|
function formatProviderTabLabel(providerId: string): string {
|
|
109
112
|
return providerId.replace(/[-_]+/g, " ").toUpperCase();
|
|
110
113
|
}
|
|
@@ -145,6 +148,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
145
148
|
// Tab state
|
|
146
149
|
#providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
|
|
147
150
|
#activeTabIndex: number = 0;
|
|
151
|
+
#refreshingProviders: Set<string> = new Set();
|
|
152
|
+
#scheduledProviderRefreshes: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
153
|
+
#refreshSpinnerFrame: number = 0;
|
|
154
|
+
#refreshSpinnerInterval?: NodeJS.Timeout;
|
|
148
155
|
|
|
149
156
|
// Context menu state
|
|
150
157
|
#isMenuOpen: boolean = false;
|
|
@@ -371,10 +378,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
371
378
|
});
|
|
372
379
|
}
|
|
373
380
|
|
|
374
|
-
|
|
381
|
+
#loadModelsFromCurrentRegistryState(): void {
|
|
375
382
|
let models: ModelItem[];
|
|
376
|
-
|
|
377
|
-
// Use scoped models if provided via --models flag
|
|
378
383
|
if (this.#scopedModels.length > 0) {
|
|
379
384
|
models = this.#scopedModels.map(scoped => ({
|
|
380
385
|
kind: "provider",
|
|
@@ -384,10 +389,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
384
389
|
selector: `${scoped.model.provider}/${scoped.model.id}`,
|
|
385
390
|
}));
|
|
386
391
|
} else {
|
|
387
|
-
// Reload config and cached discovery state without blocking on live provider refresh
|
|
388
|
-
await this.#modelRegistry.refresh("offline");
|
|
389
|
-
|
|
390
|
-
// Check for models.json errors
|
|
391
392
|
const loadError = this.#modelRegistry.getError();
|
|
392
393
|
if (loadError) {
|
|
393
394
|
this.#errorMessage = loadError;
|
|
@@ -395,7 +396,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
395
396
|
this.#errorMessage = undefined;
|
|
396
397
|
}
|
|
397
398
|
|
|
398
|
-
// Load available models (built-in models still work even if models.json failed)
|
|
399
399
|
try {
|
|
400
400
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
401
401
|
models = availableModels.map((model: Model) => ({
|
|
@@ -415,15 +415,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
const candidates = models.map(item => item.model);
|
|
418
419
|
const canonicalRecords = this.#modelRegistry.getCanonicalModels({
|
|
419
420
|
availableOnly: this.#scopedModels.length === 0,
|
|
420
|
-
candidates
|
|
421
|
+
candidates,
|
|
421
422
|
});
|
|
422
423
|
const canonicalModels = canonicalRecords
|
|
423
424
|
.map(record => {
|
|
424
425
|
const selectedModel = this.#modelRegistry.resolveCanonicalModel(record.id, {
|
|
425
426
|
availableOnly: this.#scopedModels.length === 0,
|
|
426
|
-
candidates
|
|
427
|
+
candidates,
|
|
427
428
|
});
|
|
428
429
|
if (!selectedModel) return undefined;
|
|
429
430
|
const searchText = [
|
|
@@ -457,6 +458,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
457
458
|
this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, models.length - 1));
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
async #loadModels(): Promise<void> {
|
|
462
|
+
if (this.#scopedModels.length === 0) {
|
|
463
|
+
// Reload config and cached discovery state without blocking on live provider refresh
|
|
464
|
+
await this.#modelRegistry.refresh("offline");
|
|
465
|
+
}
|
|
466
|
+
this.#loadModelsFromCurrentRegistryState();
|
|
467
|
+
}
|
|
468
|
+
|
|
460
469
|
#buildProviderTabs(): void {
|
|
461
470
|
const activeTabId = this.#getActiveTab().id;
|
|
462
471
|
const providerSet = new Set<string>();
|
|
@@ -475,17 +484,100 @@ export class ModelSelectorComponent extends Container {
|
|
|
475
484
|
activeIndex >= 0 ? activeIndex : Math.min(this.#activeTabIndex, this.#providers.length - 1);
|
|
476
485
|
}
|
|
477
486
|
|
|
478
|
-
|
|
487
|
+
#getActiveProviderRefreshStatusText(): string | undefined {
|
|
488
|
+
const providerId = this.#getActiveProviderId();
|
|
489
|
+
if (!providerId || !this.#refreshingProviders.has(providerId)) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
const spinnerFrames = theme.spinnerFrames;
|
|
493
|
+
const spinner =
|
|
494
|
+
spinnerFrames.length > 0
|
|
495
|
+
? spinnerFrames[this.#refreshSpinnerFrame % spinnerFrames.length]
|
|
496
|
+
: theme.status.pending;
|
|
497
|
+
return theme.fg("warning", ` ${spinner} Refreshing ${formatProviderTabLabel(providerId)} in background...`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#startRefreshSpinner(): void {
|
|
501
|
+
if (this.#refreshSpinnerInterval) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
this.#refreshSpinnerInterval = setInterval(() => {
|
|
505
|
+
const frameCount = theme.spinnerFrames.length;
|
|
506
|
+
if (frameCount > 0) {
|
|
507
|
+
this.#refreshSpinnerFrame = (this.#refreshSpinnerFrame + 1) % frameCount;
|
|
508
|
+
}
|
|
509
|
+
this.#updateTabBar();
|
|
510
|
+
this.#tui.requestRender();
|
|
511
|
+
}, 80);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#stopRefreshSpinner(): void {
|
|
515
|
+
if (this.#refreshingProviders.size > 0) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (this.#refreshSpinnerInterval) {
|
|
519
|
+
clearInterval(this.#refreshSpinnerInterval);
|
|
520
|
+
this.#refreshSpinnerInterval = undefined;
|
|
521
|
+
}
|
|
522
|
+
this.#refreshSpinnerFrame = 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#setProviderRefreshing(providerId: string, refreshing: boolean): void {
|
|
526
|
+
if (refreshing) {
|
|
527
|
+
this.#refreshingProviders.add(providerId);
|
|
528
|
+
this.#startRefreshSpinner();
|
|
529
|
+
} else {
|
|
530
|
+
this.#refreshingProviders.delete(providerId);
|
|
531
|
+
this.#stopRefreshSpinner();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#cancelScheduledProviderRefreshesExcept(keepProviderId?: string): void {
|
|
536
|
+
for (const [providerId, timer] of this.#scheduledProviderRefreshes) {
|
|
537
|
+
if (providerId === keepProviderId) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
this.#scheduledProviderRefreshes.delete(providerId);
|
|
542
|
+
this.#setProviderRefreshing(providerId, false);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#scheduleSelectedProviderRefresh(): void {
|
|
479
547
|
const providerId = this.#getActiveProviderId();
|
|
480
548
|
if (this.#scopedModels.length > 0 || !providerId) {
|
|
481
549
|
return;
|
|
482
550
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
this.#
|
|
487
|
-
|
|
488
|
-
|
|
551
|
+
if (this.#scheduledProviderRefreshes.has(providerId) || this.#refreshingProviders.has(providerId)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.#setProviderRefreshing(providerId, true);
|
|
555
|
+
const timer = setTimeout(() => {
|
|
556
|
+
this.#scheduledProviderRefreshes.delete(providerId);
|
|
557
|
+
void this.#refreshProviderInBackground(providerId);
|
|
558
|
+
}, MODEL_TAB_REFRESH_DEBOUNCE_MS);
|
|
559
|
+
this.#scheduledProviderRefreshes.set(providerId, timer);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async #refreshProviderInBackground(providerId: string): Promise<void> {
|
|
563
|
+
try {
|
|
564
|
+
await this.#modelRegistry.refreshProvider(providerId, "online");
|
|
565
|
+
// Provider refresh already updated the registry snapshot. Re-reading it
|
|
566
|
+
// here must stay purely in-memory — do not call modelRegistry.refresh()
|
|
567
|
+
// again or tab switches will pay an extra whole-registry reload after the
|
|
568
|
+
// network round-trip completes.
|
|
569
|
+
this.#loadModelsFromCurrentRegistryState();
|
|
570
|
+
this.#buildProviderTabs();
|
|
571
|
+
this.#updateTabBar();
|
|
572
|
+
this.#applyTabFilter();
|
|
573
|
+
} catch (error) {
|
|
574
|
+
this.#errorMessage = error instanceof Error ? error.message : String(error);
|
|
575
|
+
this.#updateList();
|
|
576
|
+
} finally {
|
|
577
|
+
this.#setProviderRefreshing(providerId, false);
|
|
578
|
+
this.#updateTabBar();
|
|
579
|
+
this.#tui.requestRender();
|
|
580
|
+
}
|
|
489
581
|
}
|
|
490
582
|
|
|
491
583
|
#updateTabBar(): void {
|
|
@@ -496,15 +588,21 @@ export class ModelSelectorComponent extends Container {
|
|
|
496
588
|
tabBar.onTabChange = (_tab, index) => {
|
|
497
589
|
this.#activeTabIndex = index;
|
|
498
590
|
this.#selectedIndex = 0;
|
|
591
|
+
this.#cancelScheduledProviderRefreshesExcept(this.#getActiveProviderId());
|
|
499
592
|
this.#applyTabFilter();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
593
|
+
this.#scheduleSelectedProviderRefresh();
|
|
594
|
+
this.#updateTabBar();
|
|
595
|
+
// Let TUI's normal post-input render paint the new tab immediately.
|
|
596
|
+
// The live refresh is debounced onto a later timer so tab cycling never
|
|
597
|
+
// shares a stack frame with provider refresh work.
|
|
598
|
+
this.#tui.requestRender();
|
|
505
599
|
};
|
|
506
600
|
this.#tabBar = tabBar;
|
|
507
601
|
this.#headerContainer.addChild(tabBar);
|
|
602
|
+
const refreshStatusText = this.#getActiveProviderRefreshStatusText();
|
|
603
|
+
if (refreshStatusText) {
|
|
604
|
+
this.#headerContainer.addChild(new Text(refreshStatusText, 0, 0));
|
|
605
|
+
}
|
|
508
606
|
}
|
|
509
607
|
|
|
510
608
|
#getActiveTab(): ProviderTabState {
|
|
@@ -874,7 +972,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
874
972
|
}
|
|
875
973
|
|
|
876
974
|
// Up arrow - navigate list (wrap to bottom when at top)
|
|
877
|
-
if (
|
|
975
|
+
if (matchesSelectUp(keyData)) {
|
|
878
976
|
const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
|
|
879
977
|
if (itemCount === 0) return;
|
|
880
978
|
this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
|
|
@@ -883,7 +981,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
883
981
|
}
|
|
884
982
|
|
|
885
983
|
// Down arrow - navigate list (wrap to top when at bottom)
|
|
886
|
-
if (
|
|
984
|
+
if (matchesSelectDown(keyData)) {
|
|
887
985
|
const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
|
|
888
986
|
if (itemCount === 0) return;
|
|
889
987
|
this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
|
|
@@ -925,13 +1023,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
925
1023
|
: this.#menuRoleActions.length;
|
|
926
1024
|
if (optionCount === 0) return;
|
|
927
1025
|
|
|
928
|
-
if (
|
|
1026
|
+
if (matchesSelectUp(keyData)) {
|
|
929
1027
|
this.#menuSelectedIndex = (this.#menuSelectedIndex - 1 + optionCount) % optionCount;
|
|
930
1028
|
this.#updateMenu();
|
|
931
1029
|
return;
|
|
932
1030
|
}
|
|
933
1031
|
|
|
934
|
-
if (
|
|
1032
|
+
if (matchesSelectDown(keyData)) {
|
|
935
1033
|
this.#menuSelectedIndex = (this.#menuSelectedIndex + 1) % optionCount;
|
|
936
1034
|
this.#updateMenu();
|
|
937
1035
|
return;
|
|
@@ -2,7 +2,7 @@ import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
|
2
2
|
import type { OAuthProviderInfo } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
3
3
|
import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
|
-
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
|
5
|
+
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
6
6
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
7
7
|
import { DynamicBorder } from "./dynamic-border";
|
|
8
8
|
|
|
@@ -193,7 +193,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
193
193
|
}
|
|
194
194
|
handleInput(keyData: string): void {
|
|
195
195
|
// Up arrow
|
|
196
|
-
if (
|
|
196
|
+
if (matchesSelectUp(keyData)) {
|
|
197
197
|
if (this.#allProviders.length > 0) {
|
|
198
198
|
this.#selectedIndex = this.#selectedIndex === 0 ? this.#allProviders.length - 1 : this.#selectedIndex - 1;
|
|
199
199
|
}
|
|
@@ -201,7 +201,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
201
201
|
this.#updateList();
|
|
202
202
|
}
|
|
203
203
|
// Down arrow
|
|
204
|
-
else if (
|
|
204
|
+
else if (matchesSelectDown(keyData)) {
|
|
205
205
|
if (this.#allProviders.length > 0) {
|
|
206
206
|
this.#selectedIndex = this.#selectedIndex === this.#allProviders.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
207
207
|
}
|
|
@@ -25,7 +25,9 @@ import { PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "
|
|
|
25
25
|
import { toPathList } from "../../tools/search";
|
|
26
26
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
27
27
|
import { getMarkdownTheme, theme } from "../theme/theme";
|
|
28
|
+
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
28
29
|
import { DynamicBorder } from "./dynamic-border";
|
|
30
|
+
import { formatContextUsage } from "./status-line/context-thresholds";
|
|
29
31
|
|
|
30
32
|
/** Max thinking characters in collapsed state */
|
|
31
33
|
const MAX_THINKING_CHARS_COLLAPSED = 200;
|
|
@@ -266,23 +268,27 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
266
268
|
const progress = session?.progress;
|
|
267
269
|
if (!progress) return "";
|
|
268
270
|
const stats: string[] = [];
|
|
269
|
-
|
|
270
|
-
// Current per-turn context — what the user reads as "how full is the context".
|
|
271
|
-
// Falls back to cumulative billing volume (Σ-prefixed) when context size is unknown.
|
|
271
|
+
// Current per-turn context — match the status line's `<pct>%/<window>` gauge (e.g. `5.1%/1M`).
|
|
272
272
|
if (progress.contextTokens && progress.contextTokens > 0) {
|
|
273
273
|
const ctx =
|
|
274
274
|
progress.contextWindow && progress.contextWindow > 0
|
|
275
|
-
?
|
|
276
|
-
: `${formatNumber(progress.contextTokens)}
|
|
275
|
+
? formatContextUsage((progress.contextTokens / progress.contextWindow) * 100, progress.contextWindow)
|
|
276
|
+
: `${formatNumber(progress.contextTokens)}`;
|
|
277
277
|
stats.push(ctx);
|
|
278
|
-
if (progress.tokens > 0) stats.push(`Σ${formatNumber(progress.tokens)}`);
|
|
279
|
-
} else if (progress.tokens > 0) {
|
|
280
|
-
stats.push(`Σ${formatNumber(progress.tokens)}`);
|
|
281
278
|
}
|
|
282
|
-
if (progress.durationMs > 0)
|
|
279
|
+
if (progress.durationMs > 0) {
|
|
280
|
+
stats.push(formatDuration(progress.durationMs));
|
|
281
|
+
}
|
|
283
282
|
const parts: string[] = [];
|
|
284
|
-
if (stats.length > 0
|
|
285
|
-
|
|
283
|
+
if (stats.length > 0 || progress.toolCount > 0) {
|
|
284
|
+
const toolCountStat =
|
|
285
|
+
progress.toolCount > 0 ? `${formatNumber(progress.toolCount)} ${theme.icon.extensionTool}` : undefined;
|
|
286
|
+
const statSegments = [toolCountStat, ...stats].filter((segment): segment is string => Boolean(segment));
|
|
287
|
+
parts.push(theme.fg("dim", statSegments.join(theme.sep.dot)));
|
|
288
|
+
}
|
|
289
|
+
if (progress.cost > 0) {
|
|
290
|
+
parts.push(theme.fg("statusLineCost", `$${progress.cost.toFixed(2)}`));
|
|
291
|
+
}
|
|
286
292
|
return parts.join(theme.sep.dot);
|
|
287
293
|
}
|
|
288
294
|
|
|
@@ -660,7 +666,7 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
660
666
|
}
|
|
661
667
|
|
|
662
668
|
// j / down — move selection down
|
|
663
|
-
if (keyData === "j" ||
|
|
669
|
+
if (keyData === "j" || matchesSelectDown(keyData)) {
|
|
664
670
|
if (entryCount > 0) {
|
|
665
671
|
this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 1, entryCount - 1);
|
|
666
672
|
}
|
|
@@ -669,7 +675,7 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
669
675
|
}
|
|
670
676
|
|
|
671
677
|
// k / up — move selection up
|
|
672
|
-
if (keyData === "k" ||
|
|
678
|
+
if (keyData === "k" || matchesSelectUp(keyData)) {
|
|
673
679
|
if (entryCount > 0) {
|
|
674
680
|
this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 1, 0);
|
|
675
681
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "@oh-my-pi/pi-tui";
|
|
14
14
|
import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { theme } from "../../modes/theme/theme";
|
|
16
|
-
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
16
|
+
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
17
17
|
import type { SessionInfo } from "../../session/session-manager";
|
|
18
18
|
import { DynamicBorder } from "./dynamic-border";
|
|
19
19
|
import { HookSelectorComponent } from "./hook-selector";
|
|
@@ -192,12 +192,12 @@ class SessionList implements Component {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// Up arrow
|
|
195
|
-
if (
|
|
195
|
+
if (matchesSelectUp(keyData)) {
|
|
196
196
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
197
197
|
return;
|
|
198
198
|
}
|
|
199
199
|
// Down arrow
|
|
200
|
-
if (
|
|
200
|
+
if (matchesSelectDown(keyData)) {
|
|
201
201
|
this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + 1);
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
@@ -79,6 +79,13 @@ const CONDITIONS: Record<string, () => boolean> = {
|
|
|
79
79
|
return false;
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
|
+
mnemosyneActive: () => {
|
|
83
|
+
try {
|
|
84
|
+
return Settings.instance.get("memory.backend") === "mnemosyne";
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
82
89
|
};
|
|
83
90
|
|
|
84
91
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import type { ThemeColor } from "../../../modes/theme/theme";
|
|
2
3
|
|
|
3
4
|
export type ContextUsageLevel = "normal" | "warning" | "purple" | "error";
|
|
@@ -54,6 +55,16 @@ export function getContextUsageLevel(contextPercent: number, contextWindow: numb
|
|
|
54
55
|
return "normal";
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Format context usage as `<percent>%/<window>` (e.g. `5.1%/1M`), matching the
|
|
60
|
+
* status line's context gauge so subagent and footer renderers stay in sync.
|
|
61
|
+
* A `null`/`undefined` percent (unknown, e.g. right after compaction) renders as `?`.
|
|
62
|
+
*/
|
|
63
|
+
export function formatContextUsage(contextPercent: number | null | undefined, contextWindow: number): string {
|
|
64
|
+
const pct = contextPercent === null || contextPercent === undefined ? "?" : `${contextPercent.toFixed(1)}%`;
|
|
65
|
+
return `${pct}/${formatNumber(contextWindow)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
export function getContextUsageThemeColor(level: ContextUsageLevel): ThemeColor {
|
|
58
69
|
switch (level) {
|
|
59
70
|
case "error":
|
|
@@ -36,6 +36,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
36
36
|
leftSegments: ["pi", "hostname", "model", "mode", "path", "git", "pr", "subagents"],
|
|
37
37
|
rightSegments: [
|
|
38
38
|
"session_name",
|
|
39
|
+
"cache_hit",
|
|
39
40
|
"token_in",
|
|
40
41
|
"token_out",
|
|
41
42
|
"token_rate",
|