@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.3
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 +67 -0
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +13 -1
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +2 -1
- package/package.json +9 -9
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/cli-commands.ts +1 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/models-config-schema.ts +3 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/exa/mcp-client.ts +11 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +4 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/manager.ts +40 -21
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +45 -14
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/tips.txt +5 -1
- package/src/modes/components/transcript-container.ts +35 -6
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +18 -7
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/interactive-mode.ts +38 -3
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/tools/browser.md +29 -29
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/sdk.ts +87 -30
- package/src/session/agent-session.ts +96 -14
- package/src/session/auth-storage.ts +4 -0
- package/src/session/history-storage.ts +11 -18
- package/src/session/messages.ts +80 -0
- package/src/session/session-manager.ts +7 -1
- package/src/slash-commands/types.ts +27 -10
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/text.ts +112 -1
- package/src/tools/bash.ts +3 -4
- package/src/tools/index.ts +16 -0
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +3 -3
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/search.ts +18 -1
- package/src/tools/ssh.ts +26 -10
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +66 -38
- package/src/web/search/index.ts +3 -1
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +2 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Controls:
|
|
9
9
|
* - Up/Down or j/k: move selection
|
|
10
|
-
* - Tab / Shift+Tab: switch source tab
|
|
10
|
+
* - Tab / Shift+Tab or Left/Right: switch source tab
|
|
11
11
|
* - Space: enable/disable selected agent
|
|
12
12
|
* - Enter: edit model override for selected agent
|
|
13
13
|
* - N: start agent creation flow
|
|
@@ -20,6 +20,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
20
20
|
import {
|
|
21
21
|
type Component,
|
|
22
22
|
Container,
|
|
23
|
+
Editor,
|
|
23
24
|
extractPrintableText,
|
|
24
25
|
fuzzyMatch,
|
|
25
26
|
Input,
|
|
@@ -49,7 +50,7 @@ import { createAgentSession } from "../../sdk";
|
|
|
49
50
|
import { discoverAgents } from "../../task/discovery";
|
|
50
51
|
import type { AgentDefinition, AgentSource } from "../../task/types";
|
|
51
52
|
import { shortenPath } from "../../tools/render-utils";
|
|
52
|
-
import { theme } from "../theme/theme";
|
|
53
|
+
import { getEditorTheme, theme } from "../theme/theme";
|
|
53
54
|
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
54
55
|
import { DynamicBorder } from "./dynamic-border";
|
|
55
56
|
|
|
@@ -97,8 +98,10 @@ const SOURCE_LABEL: Record<AgentSource, string> = {
|
|
|
97
98
|
bundled: "Bundled",
|
|
98
99
|
};
|
|
99
100
|
|
|
100
|
-
const
|
|
101
|
+
const LIST_FOOTER =
|
|
102
|
+
" ↑/↓: navigate Space: toggle Enter: model override N: new agent ←/→: source Ctrl+R: reload Esc: close";
|
|
101
103
|
|
|
104
|
+
const IDENTIFIER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+){1,5}$/;
|
|
102
105
|
function joinPatterns(patterns: string[]): string {
|
|
103
106
|
if (patterns.length === 0) return "(session model)";
|
|
104
107
|
return patterns.join(", ");
|
|
@@ -307,7 +310,7 @@ class TwoColumnBody implements Component {
|
|
|
307
310
|
const rightWidth = width - leftWidth - 3;
|
|
308
311
|
const leftLines = this.leftPane.render(leftWidth);
|
|
309
312
|
const rightLines = this.rightPane.render(rightWidth);
|
|
310
|
-
const lineCount =
|
|
313
|
+
const lineCount = this.maxHeight;
|
|
311
314
|
const out: string[] = [];
|
|
312
315
|
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
313
316
|
|
|
@@ -339,11 +342,13 @@ export class AgentDashboard extends Container {
|
|
|
339
342
|
#loading = true;
|
|
340
343
|
#loadError: string | null = null;
|
|
341
344
|
#notice: string | null = null;
|
|
345
|
+
#builtRows = -1;
|
|
346
|
+
#builtCols = -1;
|
|
342
347
|
|
|
343
348
|
#editInput: Input | null = null;
|
|
344
349
|
#editingAgentName: string | null = null;
|
|
345
350
|
|
|
346
|
-
#createInput:
|
|
351
|
+
#createInput: Editor | null = null;
|
|
347
352
|
#createDescription = "";
|
|
348
353
|
#createScope: AgentScope = "project";
|
|
349
354
|
#createGenerating = false;
|
|
@@ -466,8 +471,46 @@ export class AgentDashboard extends Container {
|
|
|
466
471
|
this.#clampSelection();
|
|
467
472
|
}
|
|
468
473
|
|
|
474
|
+
/** Live terminal height so the dashboard tracks resize while open. */
|
|
475
|
+
#terminalRows(): number {
|
|
476
|
+
return process.stdout.rows || this.terminalHeight || 24;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#noticeBlockLines(): number {
|
|
480
|
+
if (!this.#notice) return 0;
|
|
481
|
+
return wrapTextWithAnsi(theme.fg("success", replaceTabs(this.#notice)), this.#uiWidth()).length + 1;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#footerLines(): number {
|
|
485
|
+
return Math.max(1, wrapTextWithAnsi(theme.fg("dim", LIST_FOOTER), this.#uiWidth()).length);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Height budget for the two-column body, sized to the live terminal. */
|
|
489
|
+
#computeBodyHeight(): number {
|
|
490
|
+
// Chrome around the body: top border + title + tab bar + spacer (4),
|
|
491
|
+
// optional notice block, then spacer + footer + bottom border.
|
|
492
|
+
const chrome = 4 + this.#noticeBlockLines() + 1 + this.#footerLines() + 1;
|
|
493
|
+
return Math.max(5, this.#terminalRows() - chrome);
|
|
494
|
+
}
|
|
495
|
+
|
|
469
496
|
#getMaxVisibleItems(): number {
|
|
470
|
-
|
|
497
|
+
// List pane chrome inside the body: search line, blank line, count line.
|
|
498
|
+
return Math.max(3, this.#computeBodyHeight() - 3);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
override render(width: number): string[] {
|
|
502
|
+
// Rebuild when terminal geometry changes so the full-screen overlay
|
|
503
|
+
// re-fits on resize.
|
|
504
|
+
if (this.#terminalRows() !== this.#builtRows || this.#uiWidth() !== this.#builtCols) {
|
|
505
|
+
this.#buildLayout();
|
|
506
|
+
}
|
|
507
|
+
const lines = super.render(width);
|
|
508
|
+
// Pad to the full viewport so every state (list, edit, create) covers the
|
|
509
|
+
// screen as a true full-screen view instead of letting the transcript peek
|
|
510
|
+
// through below it.
|
|
511
|
+
const rows = this.#terminalRows();
|
|
512
|
+
while (lines.length < rows) lines.push("");
|
|
513
|
+
return lines;
|
|
471
514
|
}
|
|
472
515
|
|
|
473
516
|
#clampSelection(): void {
|
|
@@ -557,10 +600,15 @@ export class AgentDashboard extends Container {
|
|
|
557
600
|
this.#createError = null;
|
|
558
601
|
this.#createSpec = null;
|
|
559
602
|
this.#createDescription = "";
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
603
|
+
const editor = new Editor(getEditorTheme());
|
|
604
|
+
editor.setBorderVisible(false);
|
|
605
|
+
editor.setPromptGutter("> ");
|
|
606
|
+
editor.setMaxHeight(Math.max(3, Math.min(8, this.#terminalRows() - 12)));
|
|
607
|
+
editor.disableSubmit = true;
|
|
608
|
+
editor.onChange = value => {
|
|
609
|
+
this.#createDescription = value;
|
|
563
610
|
};
|
|
611
|
+
this.#createInput = editor;
|
|
564
612
|
this.#buildLayout();
|
|
565
613
|
}
|
|
566
614
|
|
|
@@ -578,6 +626,20 @@ export class AgentDashboard extends Container {
|
|
|
578
626
|
this.#buildLayout();
|
|
579
627
|
}
|
|
580
628
|
|
|
629
|
+
#submitCreateDescription(): void {
|
|
630
|
+
if (!this.#createInput || this.#createGenerating) return;
|
|
631
|
+
const description = this.#createInput.getExpandedText();
|
|
632
|
+
this.#createDescription = description;
|
|
633
|
+
void this.#generateAgentFromDescription(description);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#insertCreateNewline(): void {
|
|
637
|
+
if (!this.#createInput || this.#createGenerating) return;
|
|
638
|
+
this.#createInput.handleInput("\n");
|
|
639
|
+
this.#createDescription = this.#createInput.getExpandedText();
|
|
640
|
+
this.#buildLayout();
|
|
641
|
+
}
|
|
642
|
+
|
|
581
643
|
async #generateAgentFromDescription(rawDescription: string): Promise<void> {
|
|
582
644
|
const description = rawDescription.trim();
|
|
583
645
|
this.#createDescription = description;
|
|
@@ -626,7 +688,7 @@ export class AgentDashboard extends Container {
|
|
|
626
688
|
throw new Error("No available model to generate agent specification.");
|
|
627
689
|
}
|
|
628
690
|
|
|
629
|
-
const systemPrompt = prompt.render(agentCreationArchitectPrompt, {
|
|
691
|
+
const systemPrompt = prompt.render(agentCreationArchitectPrompt, {});
|
|
630
692
|
const userPrompt = prompt.render(agentCreationUserPrompt, { request: description });
|
|
631
693
|
|
|
632
694
|
const { session } = await createAgentSession({
|
|
@@ -694,10 +756,14 @@ export class AgentDashboard extends Container {
|
|
|
694
756
|
}
|
|
695
757
|
}
|
|
696
758
|
|
|
697
|
-
const frontmatter = YAML.stringify(
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
759
|
+
const frontmatter = YAML.stringify(
|
|
760
|
+
{
|
|
761
|
+
name: spec.identifier,
|
|
762
|
+
description: spec.whenToUse,
|
|
763
|
+
},
|
|
764
|
+
null,
|
|
765
|
+
2,
|
|
766
|
+
).trimEnd();
|
|
701
767
|
const content = `---\n${frontmatter}\n---\n\n${spec.systemPrompt.trim()}\n`;
|
|
702
768
|
await Bun.write(filePath, content);
|
|
703
769
|
await this.#reloadData();
|
|
@@ -789,13 +855,13 @@ export class AgentDashboard extends Container {
|
|
|
789
855
|
}
|
|
790
856
|
return parts.join("");
|
|
791
857
|
}
|
|
792
|
-
|
|
793
858
|
#renderCreateInput(): void {
|
|
794
859
|
this.addChild(new Text(theme.bold(theme.fg("accent", " Create New Agent")), 0, 0));
|
|
795
860
|
this.addChild(new Spacer(1));
|
|
796
861
|
this.addChild(new Text(theme.fg("muted", "Describe what the new agent should do:"), 0, 0));
|
|
797
862
|
this.addChild(new Spacer(1));
|
|
798
863
|
if (this.#createInput) {
|
|
864
|
+
this.#createInput.setMaxHeight(Math.max(3, Math.min(8, this.#terminalRows() - 12)));
|
|
799
865
|
this.addChild(this.#createInput);
|
|
800
866
|
}
|
|
801
867
|
this.addChild(new Spacer(1));
|
|
@@ -805,7 +871,7 @@ export class AgentDashboard extends Container {
|
|
|
805
871
|
this.addChild(new Text(theme.fg("accent", "Generating agent specification..."), 0, 0));
|
|
806
872
|
if (this.#createStreamingText) {
|
|
807
873
|
this.addChild(new Spacer(1));
|
|
808
|
-
const maxPreview = Math.max(3, this
|
|
874
|
+
const maxPreview = Math.max(3, this.#terminalRows() - 18);
|
|
809
875
|
const contentWidth = Math.max(20, this.#uiWidth() - 4);
|
|
810
876
|
const wrappedLines: string[] = [];
|
|
811
877
|
for (const raw of this.#createStreamingText.split("\n")) {
|
|
@@ -826,7 +892,9 @@ export class AgentDashboard extends Container {
|
|
|
826
892
|
this.addChild(new Text(theme.fg("error", replaceTabs(this.#createError)), 0, 0));
|
|
827
893
|
}
|
|
828
894
|
this.addChild(new Spacer(1));
|
|
829
|
-
const hints = this.#createGenerating
|
|
895
|
+
const hints = this.#createGenerating
|
|
896
|
+
? " Generating..."
|
|
897
|
+
: " Ctrl+Enter: generate Enter: newline Tab: toggle scope Esc: cancel";
|
|
830
898
|
this.addChild(new Text(theme.fg("dim", hints), 0, 0));
|
|
831
899
|
}
|
|
832
900
|
|
|
@@ -968,22 +1036,15 @@ export class AgentDashboard extends Container {
|
|
|
968
1036
|
effectivePatterns,
|
|
969
1037
|
effectiveResolution,
|
|
970
1038
|
);
|
|
971
|
-
const bodyHeight =
|
|
1039
|
+
const bodyHeight = this.#computeBodyHeight();
|
|
972
1040
|
this.addChild(new TwoColumnBody(listPane, inspector, bodyHeight));
|
|
973
1041
|
this.addChild(new Spacer(1));
|
|
974
|
-
this.addChild(
|
|
975
|
-
new Text(
|
|
976
|
-
theme.fg(
|
|
977
|
-
"dim",
|
|
978
|
-
" ↑/↓: navigate Space: toggle Enter: model override N: new agent Tab: source Ctrl+R: reload Esc: close",
|
|
979
|
-
),
|
|
980
|
-
0,
|
|
981
|
-
0,
|
|
982
|
-
),
|
|
983
|
-
);
|
|
1042
|
+
this.addChild(new Text(theme.fg("dim", LIST_FOOTER), 0, 0));
|
|
984
1043
|
}
|
|
985
1044
|
|
|
986
1045
|
this.addChild(new DynamicBorder());
|
|
1046
|
+
this.#builtRows = this.#terminalRows();
|
|
1047
|
+
this.#builtCols = this.#uiWidth();
|
|
987
1048
|
}
|
|
988
1049
|
|
|
989
1050
|
handleInput(data: string): void {
|
|
@@ -1024,13 +1085,24 @@ export class AgentDashboard extends Container {
|
|
|
1024
1085
|
}
|
|
1025
1086
|
return;
|
|
1026
1087
|
}
|
|
1088
|
+
if (
|
|
1089
|
+
!this.#createGenerating &&
|
|
1090
|
+
(matchesKey(data, "ctrl+enter") || (data.charCodeAt(0) === 10 && data.length > 1))
|
|
1091
|
+
) {
|
|
1092
|
+
this.#submitCreateDescription();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (!this.#createGenerating && (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n")) {
|
|
1096
|
+
this.#insertCreateNewline();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1027
1099
|
if (!this.#createGenerating && (matchesKey(data, "tab") || matchesKey(data, "shift+tab"))) {
|
|
1028
1100
|
this.#toggleCreateScope();
|
|
1029
1101
|
return;
|
|
1030
1102
|
}
|
|
1031
1103
|
if (!this.#createGenerating && this.#createInput) {
|
|
1032
1104
|
this.#createInput.handleInput(data);
|
|
1033
|
-
this.#createDescription = this.#createInput.
|
|
1105
|
+
this.#createDescription = this.#createInput.getExpandedText();
|
|
1034
1106
|
this.#buildLayout();
|
|
1035
1107
|
}
|
|
1036
1108
|
return;
|
|
@@ -1064,11 +1136,11 @@ export class AgentDashboard extends Container {
|
|
|
1064
1136
|
return;
|
|
1065
1137
|
}
|
|
1066
1138
|
|
|
1067
|
-
if (matchesKey(data, "tab")) {
|
|
1139
|
+
if (matchesKey(data, "tab") || matchesKey(data, "right")) {
|
|
1068
1140
|
this.#switchTab(1);
|
|
1069
1141
|
return;
|
|
1070
1142
|
}
|
|
1071
|
-
if (matchesKey(data, "shift+tab")) {
|
|
1143
|
+
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
1072
1144
|
this.#switchTab(-1);
|
|
1073
1145
|
return;
|
|
1074
1146
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
Text,
|
|
21
21
|
truncateToWidth,
|
|
22
22
|
visibleWidth,
|
|
23
|
+
wrapTextWithAnsi,
|
|
23
24
|
} from "@oh-my-pi/pi-tui";
|
|
24
25
|
import { Settings } from "../../../config/settings";
|
|
25
26
|
import { DynamicBorder } from "../../../modes/components/dynamic-border";
|
|
@@ -37,11 +38,15 @@ import {
|
|
|
37
38
|
} from "./state-manager";
|
|
38
39
|
import type { DashboardState } from "./types";
|
|
39
40
|
|
|
41
|
+
const EXT_FOOTER = " ↑/↓: navigate Space: toggle ←/→: provider Esc: close";
|
|
42
|
+
|
|
40
43
|
export class ExtensionDashboard extends Container {
|
|
41
44
|
#state!: DashboardState;
|
|
42
45
|
#mainList!: ExtensionList;
|
|
43
46
|
#inspector!: InspectorPanel;
|
|
44
47
|
#refreshToken = 0;
|
|
48
|
+
#builtRows = -1;
|
|
49
|
+
#builtCols = -1;
|
|
45
50
|
|
|
46
51
|
onClose?: () => void;
|
|
47
52
|
onRequestRender?: () => void;
|
|
@@ -71,7 +76,7 @@ export class ExtensionDashboard extends Container {
|
|
|
71
76
|
|
|
72
77
|
// Calculate max visible items based on terminal height
|
|
73
78
|
// Reserve ~10 lines for header, tabs, help text, borders
|
|
74
|
-
const maxVisible =
|
|
79
|
+
const maxVisible = this.#maxVisibleItems();
|
|
75
80
|
|
|
76
81
|
// Create main list - always focused
|
|
77
82
|
this.#mainList = new ExtensionList(
|
|
@@ -107,6 +112,45 @@ export class ExtensionDashboard extends Container {
|
|
|
107
112
|
return tab && tab.id !== "all" ? tab.id : null;
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
/** Live terminal height so the dashboard tracks resize while open. */
|
|
116
|
+
#terminalRows(): number {
|
|
117
|
+
return process.stdout.rows || this.terminalHeight || 24;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#uiWidth(): number {
|
|
121
|
+
return Math.max(20, process.stdout.columns || 80);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#footerLines(): number {
|
|
125
|
+
return Math.max(1, wrapTextWithAnsi(theme.fg("dim", EXT_FOOTER), this.#uiWidth()).length);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Height budget for the two-column body, sized to the live terminal. */
|
|
129
|
+
#computeBodyHeight(): number {
|
|
130
|
+
// Chrome: top border + title + tab bar + spacer (4), then spacer + footer + bottom border.
|
|
131
|
+
const chrome = 4 + 1 + this.#footerLines() + 1;
|
|
132
|
+
return Math.max(5, this.#terminalRows() - chrome);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#maxVisibleItems(): number {
|
|
136
|
+
// List chrome inside the body: search line, blank line, scroll indicator.
|
|
137
|
+
return Math.max(3, this.#computeBodyHeight() - 3);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override render(width: number): string[] {
|
|
141
|
+
// Rebuild when terminal geometry changes so the full-screen overlay
|
|
142
|
+
// re-fits on resize.
|
|
143
|
+
if (this.#terminalRows() !== this.#builtRows || this.#uiWidth() !== this.#builtCols) {
|
|
144
|
+
this.#buildLayout();
|
|
145
|
+
}
|
|
146
|
+
const lines = super.render(width);
|
|
147
|
+
// Pad to the full viewport so the dashboard covers the screen instead of
|
|
148
|
+
// letting the transcript peek through below it.
|
|
149
|
+
const rows = this.#terminalRows();
|
|
150
|
+
while (lines.length < rows) lines.push("");
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
110
154
|
#buildLayout(): void {
|
|
111
155
|
this.clear();
|
|
112
156
|
|
|
@@ -120,16 +164,18 @@ export class ExtensionDashboard extends Container {
|
|
|
120
164
|
this.addChild(new Text(this.#renderTabBar(), 0, 0));
|
|
121
165
|
this.addChild(new Spacer(1));
|
|
122
166
|
|
|
123
|
-
// 2-column body
|
|
124
|
-
|
|
125
|
-
|
|
167
|
+
// 2-column body sized to fill the live terminal viewport.
|
|
168
|
+
const bodyMaxHeight = this.#computeBodyHeight();
|
|
169
|
+
this.#mainList.setMaxVisible(this.#maxVisibleItems());
|
|
126
170
|
this.addChild(new TwoColumnBody(this.#mainList, this.#inspector, bodyMaxHeight));
|
|
127
171
|
|
|
128
172
|
this.addChild(new Spacer(1));
|
|
129
|
-
this.addChild(new Text(theme.fg("dim",
|
|
173
|
+
this.addChild(new Text(theme.fg("dim", EXT_FOOTER), 0, 0));
|
|
130
174
|
|
|
131
175
|
// Bottom border
|
|
132
176
|
this.addChild(new DynamicBorder());
|
|
177
|
+
this.#builtRows = this.#terminalRows();
|
|
178
|
+
this.#builtCols = this.#uiWidth();
|
|
133
179
|
}
|
|
134
180
|
|
|
135
181
|
#renderTabBar(): string {
|
|
@@ -289,12 +335,12 @@ export class ExtensionDashboard extends Container {
|
|
|
289
335
|
return;
|
|
290
336
|
}
|
|
291
337
|
|
|
292
|
-
// Tab/Shift+Tab: Cycle through tabs
|
|
293
|
-
if (matchesKey(data, "tab")) {
|
|
338
|
+
// Tab/Shift+Tab or Left/Right: Cycle through tabs
|
|
339
|
+
if (matchesKey(data, "tab") || matchesKey(data, "right")) {
|
|
294
340
|
this.#switchTab(1);
|
|
295
341
|
return;
|
|
296
342
|
}
|
|
297
|
-
if (matchesKey(data, "shift+tab")) {
|
|
343
|
+
if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
|
|
298
344
|
this.#switchTab(-1);
|
|
299
345
|
return;
|
|
300
346
|
}
|
|
@@ -328,8 +374,8 @@ class TwoColumnBody implements Component {
|
|
|
328
374
|
const leftLines = this.leftPane.render(leftWidth);
|
|
329
375
|
const rightLines = this.rightPane.render(rightWidth);
|
|
330
376
|
|
|
331
|
-
//
|
|
332
|
-
const numLines =
|
|
377
|
+
// Fill the full body height so the dashboard reads as a full-screen view.
|
|
378
|
+
const numLines = this.maxHeight;
|
|
333
379
|
const combined: string[] = [];
|
|
334
380
|
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
335
381
|
|
|
@@ -11,18 +11,82 @@ import {
|
|
|
11
11
|
visibleWidth,
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { theme } from "../../modes/theme/theme";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
matchesAppInterrupt,
|
|
16
|
+
matchesSelectDown,
|
|
17
|
+
matchesSelectPageDown,
|
|
18
|
+
matchesSelectPageUp,
|
|
19
|
+
matchesSelectUp,
|
|
20
|
+
} from "../../modes/utils/keybinding-matchers";
|
|
15
21
|
import type { HistoryEntry, HistoryStorage } from "../../session/history-storage";
|
|
16
22
|
import { DynamicBorder } from "./dynamic-border";
|
|
23
|
+
import { rawKeyHint } from "./keybinding-hints";
|
|
24
|
+
|
|
25
|
+
/** Visible result rows; also the jump distance for PageUp/PageDown. */
|
|
26
|
+
const MAX_VISIBLE = 10;
|
|
27
|
+
|
|
28
|
+
/** Split a query the same way `HistoryStorage` tokenizes it, so highlights align with matches. */
|
|
29
|
+
function queryTokens(query: string): string[] {
|
|
30
|
+
return query
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
33
|
+
.filter(tok => tok.length > 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Wrap every case-insensitive occurrence of any token in `text` with the accent color. */
|
|
37
|
+
function highlightTokens(text: string, tokens: string[]): string {
|
|
38
|
+
if (tokens.length === 0) return text;
|
|
39
|
+
|
|
40
|
+
const lower = text.toLowerCase();
|
|
41
|
+
const ranges: Array<[number, number]> = [];
|
|
42
|
+
for (const tok of tokens) {
|
|
43
|
+
let from = lower.indexOf(tok);
|
|
44
|
+
while (from !== -1) {
|
|
45
|
+
ranges.push([from, from + tok.length]);
|
|
46
|
+
from = lower.indexOf(tok, from + tok.length);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (ranges.length === 0) return text;
|
|
50
|
+
|
|
51
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
52
|
+
let out = "";
|
|
53
|
+
let pos = 0;
|
|
54
|
+
for (const [start, end] of ranges) {
|
|
55
|
+
if (end <= pos) continue; // fully covered by a previous (merged) range
|
|
56
|
+
const from = Math.max(start, pos);
|
|
57
|
+
if (from > pos) out += text.slice(pos, from);
|
|
58
|
+
out += theme.fg("accent", text.slice(from, end));
|
|
59
|
+
pos = end;
|
|
60
|
+
}
|
|
61
|
+
if (pos < text.length) out += text.slice(pos);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Compact "time since" label (e.g. `now`, `5m`, `2h`, `3d`, `2w`, `6mo`, `1y`) from epoch seconds. */
|
|
66
|
+
function relativeTime(epochSeconds: number): string {
|
|
67
|
+
const seconds = Math.max(0, Math.floor(Date.now() / 1000) - epochSeconds);
|
|
68
|
+
if (seconds < 60) return "now";
|
|
69
|
+
const minutes = Math.floor(seconds / 60);
|
|
70
|
+
if (minutes < 60) return `${minutes}m`;
|
|
71
|
+
const hours = Math.floor(minutes / 60);
|
|
72
|
+
if (hours < 24) return `${hours}h`;
|
|
73
|
+
const days = Math.floor(hours / 24);
|
|
74
|
+
if (days < 7) return `${days}d`;
|
|
75
|
+
if (days < 30) return `${Math.floor(days / 7)}w`;
|
|
76
|
+
if (days < 365) return `${Math.floor(days / 30)}mo`;
|
|
77
|
+
return `${Math.floor(days / 365)}y`;
|
|
78
|
+
}
|
|
17
79
|
|
|
18
80
|
class HistoryResultsList implements Component {
|
|
19
81
|
#results: HistoryEntry[] = [];
|
|
82
|
+
#tokens: string[] = [];
|
|
20
83
|
#selectedIndex = 0;
|
|
21
|
-
#maxVisible =
|
|
84
|
+
#maxVisible = MAX_VISIBLE;
|
|
22
85
|
|
|
23
|
-
setResults(results: HistoryEntry[], selectedIndex: number): void {
|
|
86
|
+
setResults(results: HistoryEntry[], selectedIndex: number, tokens: string[]): void {
|
|
24
87
|
this.#results = results;
|
|
25
88
|
this.#selectedIndex = selectedIndex;
|
|
89
|
+
this.#tokens = tokens;
|
|
26
90
|
}
|
|
27
91
|
|
|
28
92
|
setSelectedIndex(selectedIndex: number): void {
|
|
@@ -37,10 +101,14 @@ class HistoryResultsList implements Component {
|
|
|
37
101
|
const lines: string[] = [];
|
|
38
102
|
|
|
39
103
|
if (this.#results.length === 0) {
|
|
40
|
-
|
|
104
|
+
const message = this.#tokens.length > 0 ? "No matching history" : "No history yet";
|
|
105
|
+
lines.push(theme.fg("muted", ` ${theme.status.info} ${message}`));
|
|
41
106
|
return lines;
|
|
42
107
|
}
|
|
43
108
|
|
|
109
|
+
const cursorSymbol = `${theme.nav.cursor} `;
|
|
110
|
+
const gutterWidth = visibleWidth(cursorSymbol);
|
|
111
|
+
|
|
44
112
|
const startIndex = Math.max(
|
|
45
113
|
0,
|
|
46
114
|
Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), this.#results.length - this.#maxVisible),
|
|
@@ -51,18 +119,32 @@ class HistoryResultsList implements Component {
|
|
|
51
119
|
const entry = this.#results[i];
|
|
52
120
|
const isSelected = i === this.#selectedIndex;
|
|
53
121
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const maxWidth = width - cursorWidth;
|
|
122
|
+
const timeStr = relativeTime(entry.created_at);
|
|
123
|
+
const timeWidth = visibleWidth(timeStr);
|
|
124
|
+
const showTime = width >= gutterWidth + 12 + timeWidth;
|
|
58
125
|
|
|
126
|
+
const promptBudget = Math.max(4, width - gutterWidth - (showTime ? timeWidth + 1 : 0));
|
|
59
127
|
const normalized = entry.prompt.replace(/\s+/g, " ").trim();
|
|
60
|
-
const
|
|
61
|
-
|
|
128
|
+
const plain = truncateToWidth(normalized, promptBudget);
|
|
129
|
+
const highlighted = highlightTokens(plain, this.#tokens);
|
|
130
|
+
|
|
131
|
+
const cursor = isSelected ? theme.fg("accent", cursorSymbol) : padding(gutterWidth);
|
|
132
|
+
let line = cursor + (isSelected ? theme.bold(highlighted) : highlighted);
|
|
133
|
+
|
|
134
|
+
if (showTime) {
|
|
135
|
+
// Pad the prompt region so the timestamp sits flush right with a one-cell gap.
|
|
136
|
+
line = `${truncateToWidth(line, width - timeWidth - 1, Ellipsis.Unicode, true)} ${theme.fg("dim", timeStr)}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lines.push(
|
|
140
|
+
isSelected
|
|
141
|
+
? theme.bg("selectedBg", truncateToWidth(line, width, Ellipsis.Omit, true))
|
|
142
|
+
: truncateToWidth(line, width),
|
|
143
|
+
);
|
|
62
144
|
}
|
|
63
145
|
|
|
64
146
|
if (startIndex > 0 || endIndex < this.#results.length) {
|
|
65
|
-
const scrollText = `
|
|
147
|
+
const scrollText = ` ${this.#selectedIndex + 1}/${this.#results.length}`;
|
|
66
148
|
lines.push(theme.fg("muted", truncateToWidth(scrollText, width, Ellipsis.Omit)));
|
|
67
149
|
}
|
|
68
150
|
|
|
@@ -99,8 +181,12 @@ export class HistorySearchComponent extends Container {
|
|
|
99
181
|
|
|
100
182
|
this.#resultsList = new HistoryResultsList();
|
|
101
183
|
|
|
184
|
+
const title = theme.bold(theme.fg("accent", `${theme.icon.rewind} Search History`));
|
|
185
|
+
const dot = theme.fg("dim", theme.sep.dot);
|
|
186
|
+
const hint = [rawKeyHint("↑↓", "navigate"), rawKeyHint("enter", "select"), rawKeyHint("esc", "cancel")].join(dot);
|
|
187
|
+
|
|
102
188
|
this.addChild(new Spacer(1));
|
|
103
|
-
this.addChild(new Text(
|
|
189
|
+
this.addChild(new Text(title, 1, 0));
|
|
104
190
|
this.addChild(new Spacer(1));
|
|
105
191
|
this.addChild(new DynamicBorder());
|
|
106
192
|
this.addChild(new Spacer(1));
|
|
@@ -108,7 +194,7 @@ export class HistorySearchComponent extends Container {
|
|
|
108
194
|
this.addChild(new Spacer(1));
|
|
109
195
|
this.addChild(this.#resultsList);
|
|
110
196
|
this.addChild(new Spacer(1));
|
|
111
|
-
this.addChild(new Text(
|
|
197
|
+
this.addChild(new Text(hint, 1, 0));
|
|
112
198
|
this.addChild(new Spacer(1));
|
|
113
199
|
this.addChild(new DynamicBorder());
|
|
114
200
|
|
|
@@ -130,6 +216,34 @@ export class HistorySearchComponent extends Container {
|
|
|
130
216
|
return;
|
|
131
217
|
}
|
|
132
218
|
|
|
219
|
+
if (matchesSelectPageUp(keyData)) {
|
|
220
|
+
if (this.#results.length === 0) return;
|
|
221
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - MAX_VISIBLE);
|
|
222
|
+
this.#resultsList.setSelectedIndex(this.#selectedIndex);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (matchesSelectPageDown(keyData)) {
|
|
227
|
+
if (this.#results.length === 0) return;
|
|
228
|
+
this.#selectedIndex = Math.min(this.#results.length - 1, this.#selectedIndex + MAX_VISIBLE);
|
|
229
|
+
this.#resultsList.setSelectedIndex(this.#selectedIndex);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (matchesKey(keyData, "home")) {
|
|
234
|
+
if (this.#results.length === 0) return;
|
|
235
|
+
this.#selectedIndex = 0;
|
|
236
|
+
this.#resultsList.setSelectedIndex(this.#selectedIndex);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (matchesKey(keyData, "end")) {
|
|
241
|
+
if (this.#results.length === 0) return;
|
|
242
|
+
this.#selectedIndex = this.#results.length - 1;
|
|
243
|
+
this.#resultsList.setSelectedIndex(this.#selectedIndex);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
133
247
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
134
248
|
const selected = this.#results[this.#selectedIndex];
|
|
135
249
|
if (selected) {
|
|
@@ -153,6 +267,6 @@ export class HistorySearchComponent extends Container {
|
|
|
153
267
|
? this.#historyStorage.search(query, this.#resultLimit)
|
|
154
268
|
: this.#historyStorage.getRecent(this.#resultLimit);
|
|
155
269
|
this.#selectedIndex = 0;
|
|
156
|
-
this.#resultsList.setResults(this.#results, this.#selectedIndex);
|
|
270
|
+
this.#resultsList.setResults(this.#results, this.#selectedIndex, query ? queryTokens(query) : []);
|
|
157
271
|
}
|
|
158
272
|
}
|