@oh-my-pi/pi-coding-agent 15.9.5 → 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 +35 -0
- package/dist/types/config/keybindings.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +11 -1
- 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 +5 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- 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/interactive-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- 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/output-block.d.ts +11 -0
- package/package.json +9 -9
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/config/keybindings.ts +58 -1
- package/src/config/settings-schema.ts +11 -1
- 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 +28 -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/export/ttsr.ts +9 -0
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +6 -5
- 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 +1 -1
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +22 -1
- package/src/modes/components/copy-selector.ts +249 -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 +27 -13
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/controllers/command-controller.ts +0 -116
- package/src/modes/controllers/event-controller.ts +26 -10
- package/src/modes/controllers/selector-controller.ts +38 -1
- package/src/modes/interactive-mode.ts +4 -4
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/copy-targets.ts +254 -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/session/agent-session.ts +6 -2
- package/src/slash-commands/builtin-registry.ts +3 -11
- package/src/task/render.ts +38 -11
- 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 +5 -5
- package/src/tools/read.ts +7 -7
- package/src/tools/render-utils.ts +46 -0
- 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/output-block.ts +14 -0
- package/src/web/search/render.ts +3 -3
- 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
|
@@ -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("");
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
34
34
|
import { toolRenderers } from "../../tools/renderers";
|
|
35
35
|
import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
|
|
36
|
-
import { renderStatusLine } from "../../tui";
|
|
36
|
+
import { isFramedBlockComponent, renderStatusLine } from "../../tui";
|
|
37
37
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
38
38
|
import { renderDiff } from "./diff";
|
|
39
39
|
|
|
@@ -45,6 +45,12 @@ function ensureInvalidate(component: unknown): Component {
|
|
|
45
45
|
return c as Component;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function addBoxChild(box: Box, component: unknown): boolean {
|
|
49
|
+
const child = ensureInvalidate(component);
|
|
50
|
+
box.addChild(child);
|
|
51
|
+
return isFramedBlockComponent(child);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
/**
|
|
49
55
|
* Drop trailing removal/hunk-header lines that appear in a streaming diff
|
|
50
56
|
* before the matching `+added` lines have arrived. Without this, a partial
|
|
@@ -107,7 +113,7 @@ function rawTextInputFromPartialJson(partialJson: unknown): string | undefined {
|
|
|
107
113
|
// Function-tool arguments stream as JSON. Custom/free-form tools stream raw
|
|
108
114
|
// text in the same transport field; only the raw form is a valid fallback for
|
|
109
115
|
// the conventional `input` parameter.
|
|
110
|
-
if (first === "{" || first ===
|
|
116
|
+
if (first === "{" || first === '"') return undefined;
|
|
111
117
|
return partialJson;
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -582,6 +588,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
582
588
|
const inline = Boolean((tool as { inline?: boolean }).inline);
|
|
583
589
|
this.#contentBox.setBgFn(inline ? undefined : bgFn);
|
|
584
590
|
this.#contentBox.clear();
|
|
591
|
+
let contentBoxHasFramedBlock = false;
|
|
585
592
|
// Mirror the built-in renderer branch so custom renderers (notably the
|
|
586
593
|
// task tool, whose live instance routes through here) receive the same
|
|
587
594
|
// render context — e.g. the `hasResult` flag that suppresses the task
|
|
@@ -594,16 +601,16 @@ export class ToolExecutionComponent extends Container {
|
|
|
594
601
|
try {
|
|
595
602
|
const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
|
|
596
603
|
if (callComponent) {
|
|
597
|
-
this.#contentBox
|
|
604
|
+
contentBoxHasFramedBlock = addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
|
|
598
605
|
}
|
|
599
606
|
} catch (err) {
|
|
600
607
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
601
608
|
// Fall back to default on error
|
|
602
|
-
this.#contentBox
|
|
609
|
+
addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
603
610
|
}
|
|
604
611
|
} else {
|
|
605
612
|
// No custom renderCall, show tool name
|
|
606
|
-
this.#contentBox
|
|
613
|
+
addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
607
614
|
}
|
|
608
615
|
|
|
609
616
|
// Render result component if we have a result
|
|
@@ -626,23 +633,24 @@ export class ToolExecutionComponent extends Container {
|
|
|
626
633
|
this.#args,
|
|
627
634
|
);
|
|
628
635
|
if (resultComponent) {
|
|
629
|
-
this.#contentBox
|
|
636
|
+
contentBoxHasFramedBlock = addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
|
|
630
637
|
}
|
|
631
638
|
} catch (err) {
|
|
632
639
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
633
640
|
// Fall back to showing raw output on error
|
|
634
641
|
const output = this.#getTextOutput();
|
|
635
642
|
if (output) {
|
|
636
|
-
this.#contentBox
|
|
643
|
+
addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
637
644
|
}
|
|
638
645
|
}
|
|
639
646
|
} else if (this.#result) {
|
|
640
647
|
// Has result but no custom renderResult
|
|
641
648
|
const output = this.#getTextOutput();
|
|
642
649
|
if (output) {
|
|
643
|
-
this.#contentBox
|
|
650
|
+
addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
644
651
|
}
|
|
645
652
|
}
|
|
653
|
+
this.#contentBox.setPaddingX(contentBoxHasFramedBlock ? 0 : 1);
|
|
646
654
|
} else if (this.#toolName in toolRenderers) {
|
|
647
655
|
// Built-in tools with renderers
|
|
648
656
|
const renderer = toolRenderers[this.#toolName];
|
|
@@ -661,6 +669,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
661
669
|
// Multi-file: render each file as its own Box (identical to separate tool calls)
|
|
662
670
|
this.#contentBox.setBgFn(undefined);
|
|
663
671
|
this.#contentBox.clear();
|
|
672
|
+
this.#contentBox.setPaddingX(1);
|
|
664
673
|
|
|
665
674
|
const renderContext = this.#buildRenderContext();
|
|
666
675
|
this.#renderState.renderContext = renderContext;
|
|
@@ -683,7 +692,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
683
692
|
theme,
|
|
684
693
|
);
|
|
685
694
|
if (resultComponent) {
|
|
686
|
-
fileBox
|
|
695
|
+
const fileBoxHasFramedBlock = addBoxChild(fileBox, resultComponent);
|
|
696
|
+
fileBox.setPaddingX(fileBoxHasFramedBlock ? 0 : 1);
|
|
687
697
|
}
|
|
688
698
|
} catch (err) {
|
|
689
699
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
@@ -719,6 +729,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
719
729
|
// Inline renderers skip background styling
|
|
720
730
|
this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
|
|
721
731
|
this.#contentBox.clear();
|
|
732
|
+
let contentBoxHasFramedBlock = false;
|
|
722
733
|
|
|
723
734
|
const renderContext = this.#buildRenderContext();
|
|
724
735
|
this.#renderState.renderContext = renderContext;
|
|
@@ -729,12 +740,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
729
740
|
try {
|
|
730
741
|
const callComponent = renderer.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
|
|
731
742
|
if (callComponent) {
|
|
732
|
-
|
|
743
|
+
contentBoxHasFramedBlock =
|
|
744
|
+
addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
|
|
733
745
|
}
|
|
734
746
|
} catch (err) {
|
|
735
747
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
736
748
|
// Fall back to default on error
|
|
737
|
-
this.#contentBox
|
|
749
|
+
addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
|
|
738
750
|
}
|
|
739
751
|
}
|
|
740
752
|
|
|
@@ -752,17 +764,19 @@ export class ToolExecutionComponent extends Container {
|
|
|
752
764
|
this.#getCallArgsForRender(),
|
|
753
765
|
);
|
|
754
766
|
if (resultComponent) {
|
|
755
|
-
|
|
767
|
+
contentBoxHasFramedBlock =
|
|
768
|
+
addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
|
|
756
769
|
}
|
|
757
770
|
} catch (err) {
|
|
758
771
|
logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
|
|
759
772
|
// Fall back to showing raw output on error
|
|
760
773
|
const output = this.#getTextOutput();
|
|
761
774
|
if (output) {
|
|
762
|
-
this.#contentBox
|
|
775
|
+
addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
|
|
763
776
|
}
|
|
764
777
|
}
|
|
765
778
|
}
|
|
779
|
+
this.#contentBox.setPaddingX(contentBoxHasFramedBlock ? 0 : 1);
|
|
766
780
|
}
|
|
767
781
|
} else {
|
|
768
782
|
// Other built-in tools: use Text directly with caching
|