@oh-my-pi/pi-coding-agent 15.9.3 → 15.9.67
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 +74 -1
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/config/keybindings.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +24 -5
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/modes/components/assistant-message.d.ts +16 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/types.d.ts +8 -1
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/eval-render.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +25 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/config/keybindings.ts +58 -1
- package/src/config/model-registry.ts +54 -4
- package/src/config/settings-schema.ts +25 -5
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +7 -7
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +100 -27
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/__tests__/shared-executors.test.ts +2 -2
- package/src/eval/agent-bridge.ts +4 -5
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/client.ts +80 -2
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/main.ts +8 -2
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +44 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/tool-execution.ts +71 -13
- package/src/modes/components/transcript-container.ts +93 -32
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/command-controller.ts +0 -116
- package/src/modes/controllers/event-controller.ts +67 -12
- package/src/modes/controllers/input-controller.ts +33 -1
- package/src/modes/controllers/selector-controller.ts +38 -1
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +52 -17
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +11 -2
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +17 -9
- package/src/session/agent-session.ts +43 -14
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/slash-commands/builtin-registry.ts +3 -11
- package/src/system-prompt.ts +4 -0
- package/src/task/render.ts +38 -11
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-render.ts +24 -9
- package/src/tools/eval.ts +14 -19
- package/src/tools/fetch.ts +34 -14
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +6 -8
- package/src/tools/read.ts +65 -19
- package/src/tools/render-utils.ts +46 -0
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +60 -11
- package/src/tools/ssh.ts +21 -8
- package/src/tools/write.ts +17 -8
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/hyperlink.ts +42 -7
- package/src/tui/output-block.ts +14 -0
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +23 -55
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
extractPrintableText,
|
|
11
11
|
matchesKey,
|
|
12
12
|
padding,
|
|
13
|
+
ScrollView,
|
|
13
14
|
truncateToWidth,
|
|
14
15
|
visibleWidth,
|
|
15
16
|
} from "@oh-my-pi/pi-tui";
|
|
@@ -134,25 +135,33 @@ export class ExtensionList implements Component {
|
|
|
134
135
|
const startIdx = this.#scrollOffset;
|
|
135
136
|
const endIdx = Math.min(startIdx + this.#maxVisible, this.#listItems.length);
|
|
136
137
|
|
|
138
|
+
// Reserve the rightmost column for the scrollbar when overflowing
|
|
139
|
+
const overflow = this.#listItems.length > this.#maxVisible;
|
|
140
|
+
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
141
|
+
|
|
137
142
|
// Render visible items
|
|
143
|
+
const rows: string[] = [];
|
|
138
144
|
for (let i = startIdx; i < endIdx; i++) {
|
|
139
145
|
const listItem = this.#listItems[i];
|
|
140
146
|
const isSelected = this.#focused && i === this.#selectedIndex;
|
|
141
147
|
|
|
142
148
|
if (listItem.type === "master") {
|
|
143
|
-
|
|
149
|
+
rows.push(this.#renderMasterSwitch(listItem, isSelected, rowWidth));
|
|
144
150
|
} else if (listItem.type === "kind-header") {
|
|
145
|
-
|
|
151
|
+
rows.push(this.#renderKindHeader(listItem, isSelected, rowWidth));
|
|
146
152
|
} else {
|
|
147
|
-
|
|
153
|
+
rows.push(this.#renderExtensionRow(listItem.item, isSelected, rowWidth, masterDisabled));
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
const sv = new ScrollView(rows, {
|
|
158
|
+
height: rows.length,
|
|
159
|
+
scrollbar: "auto",
|
|
160
|
+
totalRows: this.#listItems.length,
|
|
161
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
162
|
+
});
|
|
163
|
+
sv.setScrollOffset(this.#scrollOffset);
|
|
164
|
+
lines.push(...sv.render(width));
|
|
156
165
|
|
|
157
166
|
return lines;
|
|
158
167
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Input,
|
|
6
6
|
matchesKey,
|
|
7
7
|
padding,
|
|
8
|
+
ScrollView,
|
|
8
9
|
Spacer,
|
|
9
10
|
Text,
|
|
10
11
|
truncateToWidth,
|
|
@@ -115,15 +116,19 @@ class HistoryResultsList implements Component {
|
|
|
115
116
|
);
|
|
116
117
|
const endIndex = Math.min(startIndex + this.#maxVisible, this.#results.length);
|
|
117
118
|
|
|
119
|
+
const overflow = this.#results.length > this.#maxVisible;
|
|
120
|
+
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
121
|
+
const rows: string[] = [];
|
|
122
|
+
|
|
118
123
|
for (let i = startIndex; i < endIndex; i++) {
|
|
119
124
|
const entry = this.#results[i];
|
|
120
125
|
const isSelected = i === this.#selectedIndex;
|
|
121
126
|
|
|
122
127
|
const timeStr = relativeTime(entry.created_at);
|
|
123
128
|
const timeWidth = visibleWidth(timeStr);
|
|
124
|
-
const showTime =
|
|
129
|
+
const showTime = rowWidth >= gutterWidth + 12 + timeWidth;
|
|
125
130
|
|
|
126
|
-
const promptBudget = Math.max(4,
|
|
131
|
+
const promptBudget = Math.max(4, rowWidth - gutterWidth - (showTime ? timeWidth + 1 : 0));
|
|
127
132
|
const normalized = entry.prompt.replace(/\s+/g, " ").trim();
|
|
128
133
|
const plain = truncateToWidth(normalized, promptBudget);
|
|
129
134
|
const highlighted = highlightTokens(plain, this.#tokens);
|
|
@@ -133,21 +138,24 @@ class HistoryResultsList implements Component {
|
|
|
133
138
|
|
|
134
139
|
if (showTime) {
|
|
135
140
|
// Pad the prompt region so the timestamp sits flush right with a one-cell gap.
|
|
136
|
-
line = `${truncateToWidth(line,
|
|
141
|
+
line = `${truncateToWidth(line, rowWidth - timeWidth - 1, Ellipsis.Unicode, true)} ${theme.fg("dim", timeStr)}`;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
|
|
144
|
+
rows.push(
|
|
140
145
|
isSelected
|
|
141
|
-
? theme.bg("selectedBg", truncateToWidth(line,
|
|
142
|
-
: truncateToWidth(line,
|
|
146
|
+
? theme.bg("selectedBg", truncateToWidth(line, rowWidth, Ellipsis.Omit, true))
|
|
147
|
+
: truncateToWidth(line, rowWidth),
|
|
143
148
|
);
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
const sv = new ScrollView(rows, {
|
|
152
|
+
height: rows.length,
|
|
153
|
+
scrollbar: "auto",
|
|
154
|
+
totalRows: this.#results.length,
|
|
155
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
156
|
+
});
|
|
157
|
+
sv.setScrollOffset(startIndex);
|
|
158
|
+
lines.push(...sv.render(width));
|
|
151
159
|
return lines;
|
|
152
160
|
}
|
|
153
161
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getKeybindings,
|
|
7
7
|
Input,
|
|
8
8
|
matchesKey,
|
|
9
|
+
ScrollView,
|
|
9
10
|
Spacer,
|
|
10
11
|
type Tab,
|
|
11
12
|
TabBar,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
type TUI,
|
|
14
15
|
visibleWidth,
|
|
15
16
|
} from "@oh-my-pi/pi-tui";
|
|
17
|
+
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
16
18
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
17
19
|
import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../../config/model-registry";
|
|
18
20
|
import { resolveModelRoleValue } from "../../config/model-resolver";
|
|
@@ -147,6 +149,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
147
149
|
#tui: TUI;
|
|
148
150
|
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
149
151
|
#temporaryOnly: boolean;
|
|
152
|
+
#currentContextTokens: number;
|
|
150
153
|
|
|
151
154
|
#menuRoleActions: MenuRoleAction[] = [];
|
|
152
155
|
|
|
@@ -172,7 +175,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
172
175
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
173
176
|
onSelect: RoleSelectCallback,
|
|
174
177
|
onCancel: () => void,
|
|
175
|
-
options?: { temporaryOnly?: boolean; initialSearchInput?: string },
|
|
178
|
+
options?: { temporaryOnly?: boolean; initialSearchInput?: string; currentContextTokens?: number },
|
|
176
179
|
) {
|
|
177
180
|
super();
|
|
178
181
|
|
|
@@ -183,6 +186,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
183
186
|
this.#onSelectCallback = onSelect;
|
|
184
187
|
this.#onCancelCallback = onCancel;
|
|
185
188
|
this.#temporaryOnly = options?.temporaryOnly ?? false;
|
|
189
|
+
const currentContextTokens = options?.currentContextTokens ?? 0;
|
|
190
|
+
this.#currentContextTokens =
|
|
191
|
+
Number.isFinite(currentContextTokens) && currentContextTokens > 0 ? Math.floor(currentContextTokens) : 0;
|
|
186
192
|
const initialSearchInput = options?.initialSearchInput;
|
|
187
193
|
|
|
188
194
|
// Initialize menu role actions (built-in + custom from settings)
|
|
@@ -215,8 +221,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
215
221
|
this.#searchInput.setValue(initialSearchInput);
|
|
216
222
|
}
|
|
217
223
|
this.#searchInput.onSubmit = () => {
|
|
218
|
-
// Enter on search input opens menu if we have
|
|
219
|
-
if (this.#
|
|
224
|
+
// Enter on search input opens menu if we have an enabled selection
|
|
225
|
+
if (this.#getSelectedItem()) {
|
|
220
226
|
this.#openMenu();
|
|
221
227
|
}
|
|
222
228
|
};
|
|
@@ -460,7 +466,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
460
466
|
this.#filteredModels = models;
|
|
461
467
|
this.#canonicalModels = canonicalModels;
|
|
462
468
|
this.#filteredCanonicalModels = canonicalModels;
|
|
463
|
-
|
|
469
|
+
const visibleModels = this.#isCanonicalTab() ? canonicalModels : models;
|
|
470
|
+
this.#selectedIndex = this.#coerceSelectedIndex(
|
|
471
|
+
Math.min(this.#selectedIndex, Math.max(0, visibleModels.length - 1)),
|
|
472
|
+
visibleModels,
|
|
473
|
+
);
|
|
464
474
|
}
|
|
465
475
|
|
|
466
476
|
async #loadModels(): Promise<void> {
|
|
@@ -626,6 +636,74 @@ export class ModelSelectorComponent extends Container {
|
|
|
626
636
|
return this.#getActiveTabId() === CANONICAL_TAB;
|
|
627
637
|
}
|
|
628
638
|
|
|
639
|
+
#isModelOverContextLimit(model: Model): boolean {
|
|
640
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
641
|
+
return this.#currentContextTokens > 0 && contextWindow > 0 && this.#currentContextTokens > contextWindow;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#isItemDisabled(item: ModelItem | CanonicalModelItem): boolean {
|
|
645
|
+
return this.#isModelOverContextLimit(item.model);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#formatContextLimitSuffix(model: Model): string {
|
|
649
|
+
if (!this.#isModelOverContextLimit(model)) {
|
|
650
|
+
return "";
|
|
651
|
+
}
|
|
652
|
+
return ` ${theme.status.disabled} context>${formatNumber(model.contextWindow).toLowerCase()}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
#getVisibleItems(): ReadonlyArray<ModelItem | CanonicalModelItem> {
|
|
656
|
+
return this.#isCanonicalTab() ? this.#filteredCanonicalModels : this.#filteredModels;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
#coerceSelectedIndex(
|
|
660
|
+
index: number,
|
|
661
|
+
visibleItems: ReadonlyArray<ModelItem | CanonicalModelItem> = this.#getVisibleItems(),
|
|
662
|
+
): number {
|
|
663
|
+
const maxIndex = visibleItems.length - 1;
|
|
664
|
+
if (maxIndex < 0) {
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
const clamped = Math.max(0, Math.min(index, maxIndex));
|
|
668
|
+
const clampedItem = visibleItems[clamped];
|
|
669
|
+
if (clampedItem && !this.#isItemDisabled(clampedItem)) {
|
|
670
|
+
return clamped;
|
|
671
|
+
}
|
|
672
|
+
for (let i = clamped + 1; i <= maxIndex; i++) {
|
|
673
|
+
const item = visibleItems[i];
|
|
674
|
+
if (item && !this.#isItemDisabled(item)) {
|
|
675
|
+
return i;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
for (let i = clamped - 1; i >= 0; i--) {
|
|
679
|
+
const item = visibleItems[i];
|
|
680
|
+
if (item && !this.#isItemDisabled(item)) {
|
|
681
|
+
return i;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return clamped;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
#moveSelection(delta: number): void {
|
|
688
|
+
const visibleItems = this.#getVisibleItems();
|
|
689
|
+
const count = visibleItems.length;
|
|
690
|
+
if (count === 0) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
let index = this.#selectedIndex;
|
|
694
|
+
for (let step = 0; step < count; step++) {
|
|
695
|
+
index = (index + delta + count) % count;
|
|
696
|
+
const item = visibleItems[index];
|
|
697
|
+
if (item && !this.#isItemDisabled(item)) {
|
|
698
|
+
this.#selectedIndex = index;
|
|
699
|
+
this.#updateList();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
this.#selectedIndex = this.#coerceSelectedIndex(this.#selectedIndex, visibleItems);
|
|
704
|
+
this.#updateList();
|
|
705
|
+
}
|
|
706
|
+
|
|
629
707
|
#filterModels(query: string): void {
|
|
630
708
|
const activeTabId = this.#getActiveTabId();
|
|
631
709
|
const activeProviderId = this.#getActiveProviderId();
|
|
@@ -696,8 +774,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
696
774
|
this.#filteredCanonicalModels = baseCanonicalModels;
|
|
697
775
|
}
|
|
698
776
|
|
|
699
|
-
const
|
|
700
|
-
this.#selectedIndex =
|
|
777
|
+
const visibleItems = isCanonicalTab ? this.#filteredCanonicalModels : this.#filteredModels;
|
|
778
|
+
this.#selectedIndex = this.#coerceSelectedIndex(
|
|
779
|
+
Math.min(this.#selectedIndex, Math.max(0, visibleItems.length - 1)),
|
|
780
|
+
visibleItems,
|
|
781
|
+
);
|
|
701
782
|
this.#updateList();
|
|
702
783
|
}
|
|
703
784
|
|
|
@@ -778,6 +859,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
778
859
|
|
|
779
860
|
const showProvider = this.#getActiveTabId() === ALL_TAB;
|
|
780
861
|
|
|
862
|
+
const rows: string[] = [];
|
|
781
863
|
// Show visible slice of filtered models
|
|
782
864
|
for (let i = startIndex; i < endIndex; i++) {
|
|
783
865
|
const item = visibleItems[i];
|
|
@@ -786,6 +868,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
786
868
|
const providerItem = isCanonicalTab ? undefined : (item as ModelItem);
|
|
787
869
|
|
|
788
870
|
const isSelected = i === this.#selectedIndex;
|
|
871
|
+
const isDisabled = this.#isItemDisabled(item);
|
|
872
|
+
const disabledSuffix = this.#formatContextLimitSuffix(item.model);
|
|
789
873
|
|
|
790
874
|
// Build role badges (inverted: color as background, black text)
|
|
791
875
|
const roleBadgeTokens: string[] = [];
|
|
@@ -815,34 +899,42 @@ export class ModelSelectorComponent extends Container {
|
|
|
815
899
|
if (isCanonicalTab) {
|
|
816
900
|
const variants = theme.fg("dim", ` [${canonicalItem?.variantCount ?? 0}]`);
|
|
817
901
|
const backing = theme.fg("dim", ` -> ${item.model.provider}/${item.model.id}`);
|
|
818
|
-
line = `${prefix}${theme.fg("accent", item.id)}${variants}${backing}${badgeText}`;
|
|
902
|
+
line = `${prefix}${theme.fg("accent", item.id)}${variants}${backing}${badgeText}${disabledSuffix}`;
|
|
819
903
|
} else if (showProvider) {
|
|
820
904
|
const providerPrefix = theme.fg("dim", `${providerItem?.provider ?? ""}/`);
|
|
821
|
-
line = `${prefix}${providerPrefix}${theme.fg("accent", providerItem?.id ?? item.id)}${badgeText}`;
|
|
905
|
+
line = `${prefix}${providerPrefix}${theme.fg("accent", providerItem?.id ?? item.id)}${badgeText}${disabledSuffix}`;
|
|
822
906
|
} else {
|
|
823
|
-
line = `${prefix}${theme.fg("accent", item.id)}${badgeText}`;
|
|
907
|
+
line = `${prefix}${theme.fg("accent", item.id)}${badgeText}${disabledSuffix}`;
|
|
824
908
|
}
|
|
825
909
|
} else {
|
|
826
910
|
const prefix = " ";
|
|
827
911
|
if (isCanonicalTab) {
|
|
828
912
|
const variants = theme.fg("dim", ` [${canonicalItem?.variantCount ?? 0}]`);
|
|
829
913
|
const backing = theme.fg("dim", ` -> ${item.model.provider}/${item.model.id}`);
|
|
830
|
-
line = `${prefix}${item.id}${variants}${backing}${badgeText}`;
|
|
914
|
+
line = `${prefix}${item.id}${variants}${backing}${badgeText}${disabledSuffix}`;
|
|
831
915
|
} else if (showProvider) {
|
|
832
916
|
const providerPrefix = theme.fg("dim", `${providerItem?.provider ?? ""}/`);
|
|
833
|
-
line = `${prefix}${providerPrefix}${providerItem?.id ?? item.id}${badgeText}`;
|
|
917
|
+
line = `${prefix}${providerPrefix}${providerItem?.id ?? item.id}${badgeText}${disabledSuffix}`;
|
|
834
918
|
} else {
|
|
835
|
-
line = `${prefix}${item.id}${badgeText}`;
|
|
919
|
+
line = `${prefix}${item.id}${badgeText}${disabledSuffix}`;
|
|
836
920
|
}
|
|
837
921
|
}
|
|
838
922
|
|
|
839
|
-
|
|
923
|
+
if (isDisabled) {
|
|
924
|
+
line = theme.fg("dim", Bun.stripANSI(line));
|
|
925
|
+
}
|
|
926
|
+
rows.push(line);
|
|
840
927
|
}
|
|
841
928
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
929
|
+
if (rows.length > 0) {
|
|
930
|
+
const sv = new ScrollView(rows, {
|
|
931
|
+
height: rows.length,
|
|
932
|
+
scrollbar: "auto",
|
|
933
|
+
totalRows: visibleItems.length,
|
|
934
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
935
|
+
});
|
|
936
|
+
sv.setScrollOffset(startIndex);
|
|
937
|
+
this.#listContainer.addChild(sv);
|
|
846
938
|
}
|
|
847
939
|
|
|
848
940
|
// Show error message or "no results" if empty
|
|
@@ -863,8 +955,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
863
955
|
const suffix = isCanonicalTab
|
|
864
956
|
? ` (${selected.model.provider}/${selected.model.id}, ${(selected as CanonicalModelItem).variantCount} variants)`
|
|
865
957
|
: "";
|
|
958
|
+
const limitWarning = this.#isItemDisabled(selected)
|
|
959
|
+
? theme.fg(
|
|
960
|
+
"dim",
|
|
961
|
+
` — current context ${formatNumber(this.#currentContextTokens).toLowerCase()} > ${formatNumber(selected.model.contextWindow).toLowerCase()} limit`,
|
|
962
|
+
)
|
|
963
|
+
: "";
|
|
866
964
|
this.#listContainer.addChild(
|
|
867
|
-
new Text(theme.fg("muted", ` Model Name: ${selected.model.name}${suffix}`), 0, 0),
|
|
965
|
+
new Text(theme.fg("muted", ` Model Name: ${selected.model.name}${suffix}`) + limitWarning, 0, 0),
|
|
868
966
|
);
|
|
869
967
|
}
|
|
870
968
|
}
|
|
@@ -890,7 +988,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
890
988
|
}
|
|
891
989
|
|
|
892
990
|
#openMenu(): void {
|
|
893
|
-
|
|
991
|
+
const selectedItem = this.#getSelectedItem();
|
|
992
|
+
if (!selectedItem || this.#isItemDisabled(selectedItem)) return;
|
|
894
993
|
|
|
895
994
|
this.#isMenuOpen = true;
|
|
896
995
|
this.#menuStep = "role";
|
|
@@ -978,26 +1077,20 @@ export class ModelSelectorComponent extends Container {
|
|
|
978
1077
|
|
|
979
1078
|
// Up arrow - navigate list (wrap to bottom when at top)
|
|
980
1079
|
if (matchesSelectUp(keyData)) {
|
|
981
|
-
|
|
982
|
-
if (itemCount === 0) return;
|
|
983
|
-
this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
|
|
984
|
-
this.#updateList();
|
|
1080
|
+
this.#moveSelection(-1);
|
|
985
1081
|
return;
|
|
986
1082
|
}
|
|
987
1083
|
|
|
988
1084
|
// Down arrow - navigate list (wrap to top when at bottom)
|
|
989
1085
|
if (matchesSelectDown(keyData)) {
|
|
990
|
-
|
|
991
|
-
if (itemCount === 0) return;
|
|
992
|
-
this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
|
|
993
|
-
this.#updateList();
|
|
1086
|
+
this.#moveSelection(1);
|
|
994
1087
|
return;
|
|
995
1088
|
}
|
|
996
1089
|
|
|
997
1090
|
// Enter - open context menu or select directly in temporary mode
|
|
998
1091
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
999
1092
|
const selectedItem = this.#getSelectedItem();
|
|
1000
|
-
if (selectedItem) {
|
|
1093
|
+
if (selectedItem && !this.#isItemDisabled(selectedItem)) {
|
|
1001
1094
|
if (this.#temporaryOnly) {
|
|
1002
1095
|
// In temporary mode, skip menu and select directly
|
|
1003
1096
|
this.#handleSelect(selectedItem, null);
|
|
@@ -1020,7 +1113,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1020
1113
|
}
|
|
1021
1114
|
#handleMenuInput(keyData: string): void {
|
|
1022
1115
|
const selectedItem = this.#getSelectedItem();
|
|
1023
|
-
if (!selectedItem) return;
|
|
1116
|
+
if (!selectedItem || this.#isItemDisabled(selectedItem)) return;
|
|
1024
1117
|
|
|
1025
1118
|
const optionCount =
|
|
1026
1119
|
this.#menuStep === "thinking" && this.#menuSelectedRole !== null
|
|
@@ -1079,6 +1172,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
1079
1172
|
role: string | null,
|
|
1080
1173
|
thinkingLevel?: ConfiguredThinkingLevel,
|
|
1081
1174
|
): void {
|
|
1175
|
+
if (this.#isItemDisabled(item)) {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1082
1178
|
// For temporary role, don't save to settings - just notify caller
|
|
1083
1179
|
if (role === null) {
|
|
1084
1180
|
this.#onSelectCallback(item.model, null, undefined, item.selector);
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
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
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Container,
|
|
5
|
+
extractPrintableText,
|
|
6
|
+
fuzzyFilter,
|
|
7
|
+
matchesKey,
|
|
8
|
+
ScrollView,
|
|
9
|
+
Spacer,
|
|
10
|
+
TruncatedText,
|
|
11
|
+
} from "@oh-my-pi/pi-tui";
|
|
4
12
|
import { theme } from "../../modes/theme/theme";
|
|
5
13
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
6
14
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
@@ -162,14 +170,10 @@ export class OAuthSelectorComponent extends Container {
|
|
|
162
170
|
return this.#isSearchEnabled() || this.#searchQuery.length > 0;
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
#renderStatusLine(
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
? `${selectedCount}/${total} of ${this.#allProviders.length}`
|
|
170
|
-
: `${selectedCount}/${total}`;
|
|
171
|
-
const suffix = this.#searchQuery.trim() ? ` Search: ${this.#searchQuery}` : " Type to search";
|
|
172
|
-
return theme.fg("muted", ` (${count})${suffix}`);
|
|
173
|
+
#renderStatusLine(_total: number): string {
|
|
174
|
+
const query = this.#searchQuery.trim();
|
|
175
|
+
const suffix = query ? `Search: ${this.#searchQuery}` : "Type to search";
|
|
176
|
+
return theme.fg("muted", ` ${suffix}`);
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
#getProviderSearchText(provider: OAuthProviderInfo): string {
|
|
@@ -223,6 +227,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
223
227
|
: Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
|
|
224
228
|
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
225
229
|
|
|
230
|
+
const rows: string[] = [];
|
|
226
231
|
for (let i = startIndex; i < endIndex; i++) {
|
|
227
232
|
const provider = this.#filteredProviders[i];
|
|
228
233
|
if (!provider) continue;
|
|
@@ -239,11 +244,22 @@ export class OAuthSelectorComponent extends Container {
|
|
|
239
244
|
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
240
245
|
line = text + statusIndicator;
|
|
241
246
|
}
|
|
242
|
-
|
|
247
|
+
rows.push(line);
|
|
243
248
|
}
|
|
244
249
|
|
|
245
|
-
|
|
246
|
-
|
|
250
|
+
if (rows.length > 0) {
|
|
251
|
+
const sv = new ScrollView(rows, {
|
|
252
|
+
height: rows.length,
|
|
253
|
+
scrollbar: "auto",
|
|
254
|
+
totalRows: total,
|
|
255
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
256
|
+
});
|
|
257
|
+
sv.setScrollOffset(startIndex);
|
|
258
|
+
this.#listContainer.addChild(sv);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Search status line (scrollbar covers overflow indication)
|
|
262
|
+
if (this.#shouldRenderSearchStatus()) {
|
|
247
263
|
this.#listContainer.addChild(new TruncatedText(this.#renderStatusLine(total), 0, 0));
|
|
248
264
|
}
|
|
249
265
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Enter on main session -> close overlay (jump back)
|
|
16
16
|
*/
|
|
17
17
|
import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
18
|
-
import { Container, Markdown, type MarkdownTheme, matchesKey } from "@oh-my-pi/pi-tui";
|
|
18
|
+
import { Container, Markdown, type MarkdownTheme, matchesKey, ScrollView } from "@oh-my-pi/pi-tui";
|
|
19
19
|
import { formatDuration, formatNumber, logger } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import type { KeyId } from "../../config/keybindings";
|
|
21
21
|
import { isSilentAbort } from "../../session/messages";
|
|
@@ -230,23 +230,21 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
230
230
|
lines.push(...new DynamicBorder().render(width));
|
|
231
231
|
|
|
232
232
|
// --- Scrolled content viewport ---
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
233
|
+
const sv = new ScrollView(
|
|
234
|
+
this.#renderedLines.slice(this.#scrollOffset, this.#scrollOffset + this.#viewportHeight),
|
|
235
|
+
{
|
|
236
|
+
height: this.#viewportHeight,
|
|
237
|
+
scrollbar: "auto",
|
|
238
|
+
totalRows: this.#renderedLines.length,
|
|
239
|
+
theme: { track: t => theme.fg("dim", t), thumb: t => theme.fg("accent", t) },
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
sv.setScrollOffset(this.#scrollOffset);
|
|
243
|
+
for (const row of sv.render(Math.max(1, width - 1))) lines.push(` ${row}`);
|
|
242
244
|
|
|
243
245
|
// --- Footer ---
|
|
244
|
-
const scrollInfo =
|
|
245
|
-
this.#renderedLines.length > this.#viewportHeight
|
|
246
|
-
? ` ${theme.fg("dim", `[${this.#scrollOffset + 1}-${Math.min(this.#scrollOffset + this.#viewportHeight, this.#renderedLines.length)}/${this.#renderedLines.length}]`)}`
|
|
247
|
-
: "";
|
|
248
246
|
lines.push("");
|
|
249
|
-
lines.push(` ${this.#viewerFooterLines[0] ?? ""}
|
|
247
|
+
lines.push(` ${this.#viewerFooterLines[0] ?? ""}`);
|
|
250
248
|
for (let i = 1; i < this.#viewerFooterLines.length; i++) {
|
|
251
249
|
lines.push(` ${this.#viewerFooterLines[i]}`);
|
|
252
250
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
matchesKey,
|
|
7
7
|
padding,
|
|
8
8
|
replaceTabs,
|
|
9
|
+
ScrollView,
|
|
9
10
|
Spacer,
|
|
10
11
|
Text,
|
|
11
12
|
truncateToWidth,
|
|
@@ -247,6 +248,11 @@ class SessionList implements Component {
|
|
|
247
248
|
const endIndex = Math.min(startIndex + maxVisible, this.#filteredSessions.length);
|
|
248
249
|
|
|
249
250
|
// Render visible sessions (3 lines, or 4 when a title adds a preview line).
|
|
251
|
+
// Each session block is built into sessionLines, then wrapped by ScrollView
|
|
252
|
+
// so the right-edge scrollbar is proportional at the physical-line level.
|
|
253
|
+
const sessionLines: string[] = [];
|
|
254
|
+
const overflow = this.#filteredSessions.length > maxVisible;
|
|
255
|
+
const rowWidth = Math.max(0, width - (overflow ? 1 : 0));
|
|
250
256
|
for (let i = startIndex; i < endIndex; i++) {
|
|
251
257
|
const session = this.#filteredSessions[i];
|
|
252
258
|
const isSelected = i === this.#selectedIndex;
|
|
@@ -258,22 +264,22 @@ class SessionList implements Component {
|
|
|
258
264
|
const cursorSymbol = `${theme.nav.cursor} `;
|
|
259
265
|
const cursorWidth = visibleWidth(cursorSymbol);
|
|
260
266
|
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : padding(cursorWidth);
|
|
261
|
-
const maxWidth =
|
|
267
|
+
const maxWidth = rowWidth - cursorWidth; // Account for cursor width
|
|
262
268
|
|
|
263
269
|
if (session.title) {
|
|
264
270
|
// Has title: show title on first line, dimmed first message on second line
|
|
265
271
|
const truncatedTitle = truncateToWidth(session.title, maxWidth);
|
|
266
272
|
const titleLine = cursor + (isSelected ? theme.bold(truncatedTitle) : truncatedTitle);
|
|
267
|
-
|
|
273
|
+
sessionLines.push(titleLine);
|
|
268
274
|
|
|
269
275
|
// Second line: dimmed first message preview
|
|
270
276
|
const truncatedPreview = truncateToWidth(normalizedMessage, maxWidth);
|
|
271
|
-
|
|
277
|
+
sessionLines.push(` ${theme.fg("dim", truncatedPreview)}`);
|
|
272
278
|
} else {
|
|
273
279
|
// No title: show first message as main line
|
|
274
280
|
const truncatedMsg = truncateToWidth(normalizedMessage, maxWidth);
|
|
275
281
|
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
|
|
276
|
-
|
|
282
|
+
sessionLines.push(messageLine);
|
|
277
283
|
}
|
|
278
284
|
|
|
279
285
|
// Metadata line: date + file size + lifecycle status (+ project dir in
|
|
@@ -290,18 +296,23 @@ class SessionList implements Component {
|
|
|
290
296
|
if (this.#showCwd && session.cwd) {
|
|
291
297
|
metadata += ` ${dot} ${dim(shortenPath(session.cwd))}`;
|
|
292
298
|
}
|
|
293
|
-
const metadataLine = truncateToWidth(metadata,
|
|
299
|
+
const metadataLine = truncateToWidth(metadata, rowWidth);
|
|
294
300
|
|
|
295
|
-
|
|
296
|
-
|
|
301
|
+
sessionLines.push(metadataLine);
|
|
302
|
+
sessionLines.push(""); // Blank line between sessions
|
|
297
303
|
}
|
|
298
304
|
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
// Wrap the rendered window in a ScrollView for a proportional right-edge bar.
|
|
306
|
+
const visibleCount = endIndex - startIndex;
|
|
307
|
+
const linesPerItem = visibleCount > 0 ? sessionLines.length / visibleCount : 1;
|
|
308
|
+
const sv = new ScrollView(sessionLines, {
|
|
309
|
+
height: sessionLines.length,
|
|
310
|
+
scrollbar: "auto",
|
|
311
|
+
totalRows: Math.round(this.#filteredSessions.length * linesPerItem),
|
|
312
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
313
|
+
});
|
|
314
|
+
sv.setScrollOffset(Math.round(startIndex * linesPerItem));
|
|
315
|
+
lines.push(...sv.render(width));
|
|
305
316
|
|
|
306
317
|
// Add keybinding hint
|
|
307
318
|
lines.push("");
|