@oh-my-pi/pi-tui 18.3.2 → 18.3.4
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 +22 -0
- package/dist/types/app-keybindings.d.ts +2 -17
- package/dist/types/autocomplete.d.ts +18 -3
- package/dist/types/chrome/keybinding-hints.d.ts +16 -10
- package/dist/types/components/editor.d.ts +11 -0
- package/dist/types/key-hint-format.d.ts +29 -0
- package/dist/types/keys.d.ts +2 -1
- package/dist/types/overlays/agent-hub.d.ts +1 -1
- package/dist/types/overlays/agent-transcript-viewer.d.ts +1 -1
- package/dist/types/overlays/model-picker.d.ts +1 -3
- package/dist/types/prompt/composer-hints.d.ts +42 -0
- package/dist/types/prompt/composer.d.ts +2 -1
- package/dist/types/prompt/custom-editor.d.ts +8 -2
- package/dist/types/prompt/macos-spelling.d.ts +7 -16
- package/dist/types/prompt/prompt-action-autocomplete.d.ts +4 -1
- package/dist/types/prompt/prose-gate.d.ts +24 -0
- package/dist/types/prompt/word-completion.d.ts +46 -0
- package/dist/types/render/render-utils.d.ts +1 -1
- package/dist/types/theme/active-symbols.d.ts +12 -0
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/mcp.d.ts +3 -1
- package/dist/types/tools/task-yield-assembly.d.ts +10 -2
- package/dist/types/tools/task.d.ts +4 -0
- package/package.json +11 -10
- package/src/app-keybindings.ts +11 -104
- package/src/apps/autoresearch-dashboard.ts +10 -3
- package/src/apps/debug/log-viewer.ts +3 -2
- package/src/apps/debug/raw-sse.ts +2 -1
- package/src/apps/git/git-tui.ts +9 -5
- package/src/apps/live-visualizer.ts +2 -1
- package/src/apps/ps-top.ts +13 -4
- package/src/autocomplete.ts +42 -7
- package/src/chat/compaction-summary-message.ts +2 -1
- package/src/chat/execution-shared.ts +4 -2
- package/src/chat/tool-execution.ts +3 -1
- package/src/chat/ttsr-notification.ts +4 -3
- package/src/chat/user-message.ts +2 -2
- package/src/chrome/keybinding-hints.ts +38 -13
- package/src/components/editor.ts +180 -58
- package/src/components/settings-list.ts +10 -3
- package/src/components/tab-bar.ts +2 -1
- package/src/debug-server.ts +4 -1
- package/src/hotkeys-markdown.ts +14 -17
- package/src/key-hint-format.ts +122 -0
- package/src/keys.ts +2 -1
- package/src/overlays/advisor-config.ts +28 -6
- package/src/overlays/agent-hub.ts +8 -5
- package/src/overlays/agent-transcript-viewer.ts +6 -4
- package/src/overlays/agents-hub.ts +22 -8
- package/src/overlays/annotation-overlay.ts +31 -17
- package/src/overlays/ask-dialog.ts +14 -19
- package/src/overlays/bordered-loader.ts +2 -1
- package/src/overlays/btw-history-panel.ts +15 -12
- package/src/overlays/btw-panel.ts +12 -7
- package/src/overlays/cleanse-panel.ts +9 -6
- package/src/overlays/codex-reset-fireworks.ts +5 -3
- package/src/overlays/copy-selector.ts +13 -5
- package/src/overlays/extensions/extension-dashboard.ts +6 -1
- package/src/overlays/history-search.ts +3 -2
- package/src/overlays/hook-editor.ts +7 -2
- package/src/overlays/hook-input.ts +2 -1
- package/src/overlays/hook-selector.ts +5 -1
- package/src/overlays/login-dialog.ts +17 -4
- package/src/overlays/logout-account-selector.ts +10 -1
- package/src/overlays/mcp-add-wizard.ts +32 -20
- package/src/overlays/model-hub.ts +55 -28
- package/src/overlays/model-picker.ts +27 -12
- package/src/overlays/move-overlay.ts +5 -1
- package/src/overlays/omfg-panel.ts +8 -5
- package/src/overlays/pause-screen.ts +7 -3
- package/src/overlays/plan-review-overlay.ts +40 -14
- package/src/overlays/plan-save-overlay.ts +3 -1
- package/src/overlays/plugin-settings.ts +15 -8
- package/src/overlays/reset-usage-selector.ts +10 -3
- package/src/overlays/rewind-selector.ts +11 -5
- package/src/overlays/session-info-overlay.ts +4 -3
- package/src/overlays/session-selector.ts +12 -2
- package/src/overlays/settings-selector.ts +18 -11
- package/src/overlays/tree-selector.ts +27 -4
- package/src/overlays/usage-dashboard.ts +8 -2
- package/src/prompt/composer-cache.ts +2 -1
- package/src/prompt/composer-hints.ts +75 -0
- package/src/prompt/composer.ts +3 -2
- package/src/prompt/custom-editor.ts +22 -4
- package/src/prompt/macos-spelling.ts +17 -158
- package/src/prompt/prompt-action-autocomplete.ts +9 -2
- package/src/prompt/prose-gate.ts +87 -0
- package/src/prompt/tips.txt +6 -6
- package/src/prompt/welcome.ts +35 -2
- package/src/prompt/word-completion.ts +205 -0
- package/src/render/render-utils.ts +4 -4
- package/src/setup/scenes/composer.ts +5 -1
- package/src/setup/scenes/model.ts +3 -1
- package/src/setup/scenes/providers.ts +4 -1
- package/src/setup/scenes/sign-in.ts +4 -2
- package/src/setup/scenes/splash.ts +9 -4
- package/src/setup/scenes/theme.ts +18 -5
- package/src/setup/wizard-overlay.ts +5 -4
- package/src/theme/active-symbols.ts +20 -0
- package/src/theme/glyph-bundle.json +28 -1
- package/src/theme/symbols.ts +88 -6
- package/src/theme/theme-class.ts +0 -1
- package/src/theme/theme.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -1
- package/src/tools/eval.ts +3 -3
- package/src/tools/mcp.ts +3 -1
- package/src/tools/task-yield-assembly.ts +34 -8
- package/src/tools/task.ts +14 -5
- package/dist/types/overlays/tiny-title-download-progress.d.ts +0 -21
- package/src/overlays/tiny-title-download-progress.ts +0 -102
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.3.
|
|
4
|
+
"version": "18.3.4",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -37,17 +37,18 @@
|
|
|
37
37
|
"lint": "oxlint .",
|
|
38
38
|
"test": "bun test --parallel test/*.test.ts",
|
|
39
39
|
"fix": "oxlint --fix --fix-suggestions . && bun run fmt",
|
|
40
|
-
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
40
|
+
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'",
|
|
41
|
+
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@oh-my-pi/omptype": "18.3.
|
|
44
|
-
"@oh-my-pi/pi-agent-core": "18.3.
|
|
45
|
-
"@oh-my-pi/pi-ai": "18.3.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.3.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.3.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.3.
|
|
49
|
-
"@oh-my-pi/pi-wire": "18.3.
|
|
50
|
-
"@oh-my-pi/snapcompact": "18.3.
|
|
44
|
+
"@oh-my-pi/omptype": "18.3.4",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.3.4",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.3.4",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.3.4",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.3.4",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.3.4",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.3.4",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.3.4"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/app-keybindings.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "@oh-my-pi/pi-utils";
|
|
3
|
+
// Subpaths, not the `@oh-my-pi/pi-utils` barrel: the barrel loads the native
|
|
4
|
+
// addon (file-lock), and key-hint formatting runs on addon-free CLI paths
|
|
5
|
+
// (`omp --version`, help) through cli/command-help.ts.
|
|
6
|
+
import { getActiveProfile, getAgentDir, getProfileRootDir } from "@oh-my-pi/pi-utils/dirs";
|
|
7
|
+
import { isEnoent } from "@oh-my-pi/pi-utils/fs-error";
|
|
8
|
+
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
9
|
+
import { stringifyYamlConfig } from "@oh-my-pi/pi-utils/yaml-config";
|
|
11
10
|
import { JSONC, YAML } from "bun";
|
|
11
|
+
import { formatKeyHints } from "./key-hint-format";
|
|
12
12
|
import {
|
|
13
13
|
type Keybinding,
|
|
14
14
|
type KeybindingDefinitions,
|
|
@@ -645,12 +645,9 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
645
645
|
return this.getResolvedBindings();
|
|
646
646
|
}
|
|
647
647
|
|
|
648
|
-
/**
|
|
649
|
-
* Get display string for a keybinding (e.g., "ctrl+c/escape").
|
|
650
|
-
*/
|
|
648
|
+
/** Display string for a keybinding's keys (see {@link formatKeyHints}); empty when unbound. */
|
|
651
649
|
getDisplayString(keybinding: Keybinding): string {
|
|
652
|
-
|
|
653
|
-
return formatKeyHints(keys.length === 0 ? [] : keys);
|
|
650
|
+
return formatKeyHints(this.getKeys(keybinding));
|
|
654
651
|
}
|
|
655
652
|
|
|
656
653
|
/**
|
|
@@ -664,97 +661,7 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
664
661
|
}
|
|
665
662
|
}
|
|
666
663
|
|
|
667
|
-
|
|
668
|
-
* Key hint formatting utilities for UI labels.
|
|
669
|
-
*
|
|
670
|
-
* Modifier labels are platform-aware: macOS names the physical keys `Option`
|
|
671
|
-
* (`alt`) and `Cmd` (`super`), so rendering `Alt`/`Super` there would name keys
|
|
672
|
-
* absent from a Mac keyboard. Every other platform keeps `Alt`/`Super`.
|
|
673
|
-
*/
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Platform override for key-hint rendering; `undefined` resolves to the host
|
|
677
|
-
* `process.platform`. Mirrors `setKittyProtocolActive` in the TUI keys module:
|
|
678
|
-
* a single seam that keeps hint output deterministic in tests without mutating
|
|
679
|
-
* the global `process.platform`.
|
|
680
|
-
*/
|
|
681
|
-
let keyHintPlatformOverride: NodeJS.Platform | undefined;
|
|
682
|
-
|
|
683
|
-
/** Pin the platform used to render modifier labels (test seam). */
|
|
684
|
-
export function setKeyHintPlatform(platform: NodeJS.Platform | undefined): void {
|
|
685
|
-
keyHintPlatformOverride = platform;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/** Platform currently used for key-hint rendering. */
|
|
689
|
-
export function keyHintPlatform(): NodeJS.Platform {
|
|
690
|
-
return keyHintPlatformOverride ?? process.platform;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
type Modifier = "ctrl" | "shift" | "alt" | "super";
|
|
694
|
-
|
|
695
|
-
function isModifier(part: string): part is Modifier {
|
|
696
|
-
return part === "ctrl" || part === "shift" || part === "alt" || part === "super";
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Human label for a modifier, using each platform's own key names. `ctrl` and
|
|
701
|
-
* `shift` are the same everywhere; `alt`/`super` become `Option`/`Cmd` on macOS.
|
|
702
|
-
*/
|
|
703
|
-
export function modifierLabel(mod: Modifier, platform: NodeJS.Platform = keyHintPlatform()): string {
|
|
704
|
-
switch (mod) {
|
|
705
|
-
case "ctrl":
|
|
706
|
-
return "Ctrl";
|
|
707
|
-
case "shift":
|
|
708
|
-
return "Shift";
|
|
709
|
-
case "alt":
|
|
710
|
-
return platform === "darwin" ? "Option" : "Alt";
|
|
711
|
-
case "super":
|
|
712
|
-
return platform === "darwin" ? "Cmd" : "Super";
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const KEY_LABELS: Record<string, string> = {
|
|
717
|
-
esc: "Esc",
|
|
718
|
-
escape: "Esc",
|
|
719
|
-
enter: "Enter",
|
|
720
|
-
return: "Enter",
|
|
721
|
-
space: "Space",
|
|
722
|
-
tab: "Tab",
|
|
723
|
-
backspace: "Backspace",
|
|
724
|
-
delete: "Delete",
|
|
725
|
-
home: "Home",
|
|
726
|
-
end: "End",
|
|
727
|
-
pageup: "PgUp",
|
|
728
|
-
pagedown: "PgDn",
|
|
729
|
-
up: "Up",
|
|
730
|
-
down: "Down",
|
|
731
|
-
left: "Left",
|
|
732
|
-
right: "Right",
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
function formatKeyPart(part: string, platform: NodeJS.Platform): string {
|
|
736
|
-
const lower = part.toLowerCase();
|
|
737
|
-
if (isModifier(lower)) return modifierLabel(lower, platform);
|
|
738
|
-
const label = KEY_LABELS[lower];
|
|
739
|
-
if (label) return label;
|
|
740
|
-
if (part.length === 1) return part.toUpperCase();
|
|
741
|
-
return `${part.charAt(0).toUpperCase()}${part.slice(1)}`;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/** Format a key chord with human-readable, platform-aware modifier labels. */
|
|
745
|
-
export function formatKeyHint(key: KeyId): string {
|
|
746
|
-
const platform = keyHintPlatform();
|
|
747
|
-
return key
|
|
748
|
-
.split("+")
|
|
749
|
-
.map(part => formatKeyPart(part, platform))
|
|
750
|
-
.join("+");
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/** Format alternative key chords as slash-separated human-readable hints. */
|
|
754
|
-
export function formatKeyHints(keys: KeyId | KeyId[]): string {
|
|
755
|
-
const list = Array.isArray(keys) ? keys : [keys];
|
|
756
|
-
return list.map(formatKeyHint).join("/");
|
|
757
|
-
}
|
|
664
|
+
export * from "./key-hint-format";
|
|
758
665
|
|
|
759
666
|
export type { Keybinding, KeybindingsConfig, KeyId };
|
|
760
667
|
export { migrateKeybindingsConfigFile };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
1
2
|
import { matchesKey } from "../keys";
|
|
2
3
|
import { replaceTabs, truncateToWidth, visibleWidth } from "../utils";
|
|
3
4
|
import { ScrollView } from "../components/scroll-view";
|
|
@@ -186,7 +187,10 @@ function renderExpandedHeader(runtime: AutoresearchDashboardRuntime, width: numb
|
|
|
186
187
|
const state = runtime.state;
|
|
187
188
|
const status = renderModeStatus(runtime, state);
|
|
188
189
|
const label = state.name ? ` autoresearch: ${replaceTabs(state.name)} ` : " autoresearch ";
|
|
189
|
-
const hint = theme.fg(
|
|
190
|
+
const hint = theme.fg(
|
|
191
|
+
"dim",
|
|
192
|
+
` ${formatKeyHint("ctrl+x")} collapse ${formatKeyHint("ctrl+shift+x")} overlay${status ? ` ${status}` : ""} `,
|
|
193
|
+
);
|
|
190
194
|
const fillWidth = Math.max(0, width - visibleWidth(label) - visibleWidth(hint));
|
|
191
195
|
return truncateToWidth(theme.fg("accent", label) + theme.fg("borderMuted", "-".repeat(fillWidth)) + hint, width);
|
|
192
196
|
}
|
|
@@ -256,7 +260,7 @@ function renderCollapsedLine(runtime: AutoresearchDashboardRuntime, state: Exper
|
|
|
256
260
|
} else if (!runtime.autoresearchMode) {
|
|
257
261
|
parts.push(theme.fg("dim", ` | ${renderModeStatus(runtime, state)}`));
|
|
258
262
|
}
|
|
259
|
-
parts.push(theme.fg("dim",
|
|
263
|
+
parts.push(theme.fg("dim", ` | ${formatKeyHint("ctrl+x")} expand`));
|
|
260
264
|
return parts.join("");
|
|
261
265
|
}
|
|
262
266
|
|
|
@@ -471,7 +475,10 @@ function renderOverlayRunningLine(
|
|
|
471
475
|
}
|
|
472
476
|
|
|
473
477
|
function renderOverlayFooter(width: number, theme: Theme): string {
|
|
474
|
-
const hint = theme.fg(
|
|
478
|
+
const hint = theme.fg(
|
|
479
|
+
"dim",
|
|
480
|
+
` ${formatKeyHints(["up", "down"])} ${formatKeyHints(["j", "k"])} ${formatKeyHint("pageUp")} ${formatKeyHint("pageDown")} ${formatKeyHint("g")} ${formatKeyHint("shift+g")} ${formatKeyHint("escape")} `,
|
|
481
|
+
);
|
|
475
482
|
const fill = Math.max(0, width - visibleWidth(hint));
|
|
476
483
|
return theme.fg("borderMuted", "-".repeat(fill)) + hint;
|
|
477
484
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatKeyHint, formatKeyHints } from "../../app-keybindings";
|
|
1
2
|
import type { Component } from "../../tui";
|
|
2
3
|
import { extractPrintableText, matchesKey } from "../../keys";
|
|
3
4
|
import { routeSgrMouseInput, type SgrMouseEvent } from "../../mouse";
|
|
@@ -715,7 +716,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
715
716
|
}
|
|
716
717
|
|
|
717
718
|
#controlsText(): string {
|
|
718
|
-
return "
|
|
719
|
+
return `${formatKeyHint("escape")} close · ${formatKeyHint("ctrl+c")} copy · ${formatKeyHints(["up", "down"])}/wheel move · click toggle · ${formatKeyHints(["shift+up", "shift+down"])} select · ${formatKeyHints(["left", "right"])} collapse/expand · ${formatKeyHint("ctrl+a")} all · ${formatKeyHint("ctrl+o")} older · ${formatKeyHint("ctrl+p")} pid`;
|
|
719
720
|
}
|
|
720
721
|
|
|
721
722
|
#filterText(): string {
|
|
@@ -731,7 +732,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
731
732
|
#statusText(): string {
|
|
732
733
|
return this.#statusMessage
|
|
733
734
|
? theme.fg("success", this.#statusMessage)
|
|
734
|
-
: theme.fg("dim", "
|
|
735
|
+
: theme.fg("dim", `${formatKeyHint("enter")} loads older when highlighted; printable keys update filter`);
|
|
735
736
|
}
|
|
736
737
|
|
|
737
738
|
async #handleLoadOlder(additionalCount: number = LOAD_OLDER_CHUNK): Promise<void> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatKeyHint } from "../../app-keybindings";
|
|
1
2
|
import type { Component } from "../../tui";
|
|
2
3
|
import { matchesKey } from "../../keys";
|
|
3
4
|
import { routeSgrMouseInput, type SgrMouseEvent } from "../../mouse";
|
|
@@ -252,7 +253,7 @@ export class RawSseViewerComponent implements Component {
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
#statusText(): string {
|
|
255
|
-
const help = "
|
|
256
|
+
const help = `${formatKeyHint("escape")} close · ${formatKeyHint("ctrl+c")} copy raw · ${formatKeyHint("end")} follow tail · wheel scroll · click summary toggles follow`;
|
|
256
257
|
return this.#statusMessage
|
|
257
258
|
? `${theme.fg("success", this.#statusMessage)} ${theme.fg("dim", help)}`
|
|
258
259
|
: theme.fg("dim", help);
|
package/src/apps/git/git-tui.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* every file underneath it.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { formatKeyHint, formatKeyHints } from "../../app-keybindings";
|
|
27
28
|
import { SplitPane } from "../../components/layout/split-pane";
|
|
28
29
|
import { Stack } from "../../components/layout/stack";
|
|
29
30
|
import { matchesKey } from "../../keys";
|
|
@@ -340,7 +341,10 @@ class GitTuiComponent implements Component {
|
|
|
340
341
|
if (this.#pendingDiscard !== key) {
|
|
341
342
|
this.#pendingDiscard = key;
|
|
342
343
|
this.#setStatus(
|
|
343
|
-
theme.fg(
|
|
344
|
+
theme.fg(
|
|
345
|
+
"warning",
|
|
346
|
+
`Discard changes to ${action.selection.label}? Press ${formatKeyHint("delete")} again to confirm`,
|
|
347
|
+
),
|
|
344
348
|
);
|
|
345
349
|
return;
|
|
346
350
|
}
|
|
@@ -451,7 +455,7 @@ class GitTuiComponent implements Component {
|
|
|
451
455
|
if (!hunk.patch) return;
|
|
452
456
|
if (action === "discard" && this.#pendingDiscard !== hunk.patch) {
|
|
453
457
|
this.#pendingDiscard = hunk.patch;
|
|
454
|
-
this.#setStatus(theme.fg("warning",
|
|
458
|
+
this.#setStatus(theme.fg("warning", `Discard hunk? Press ${formatKeyHint("x")} (or click) again to confirm`));
|
|
455
459
|
return;
|
|
456
460
|
}
|
|
457
461
|
this.#pendingDiscard = null;
|
|
@@ -487,7 +491,7 @@ class GitTuiComponent implements Component {
|
|
|
487
491
|
}
|
|
488
492
|
if (action === "discard" && this.#pendingDiscard !== patch) {
|
|
489
493
|
this.#pendingDiscard = patch;
|
|
490
|
-
this.#setStatus(theme.fg("warning",
|
|
494
|
+
this.#setStatus(theme.fg("warning", `Discard selected lines? Press ${formatKeyHint("x")} again to confirm`));
|
|
491
495
|
return;
|
|
492
496
|
}
|
|
493
497
|
this.#pendingDiscard = null;
|
|
@@ -768,8 +772,8 @@ class GitTuiComponent implements Component {
|
|
|
768
772
|
theme.fg(
|
|
769
773
|
"dim",
|
|
770
774
|
this.#focus === "diff"
|
|
771
|
-
? "alt
|
|
772
|
-
: "
|
|
775
|
+
? `${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHints(["]", "["])} file · ${formatKeyHints(["shift+up", "shift+down"])} select · ${formatKeyHints(["s", "u"])} stage · ${formatKeyHints(["x", "delete"])} discard · ${formatKeyHint("v")} view · ${formatKeyHint("c")} commit · ${formatKeyHint("q")} quit`
|
|
776
|
+
: `${formatKeyHints(["up", "down"])} move · ${formatKeyHints(["left", "right"])} fold · ${formatKeyHint("space")} stage · ${formatKeyHint("delete")} discard · ${formatKeyHint("enter")} open · ${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHint("c")} commit · ${formatKeyHint("t")} tree · ${formatKeyHint("q")} quit`,
|
|
773
777
|
);
|
|
774
778
|
const free = width - row.width - right.width - 1;
|
|
775
779
|
const middleText = free > visibleWidth(middle) + 4 ? middle : truncateToWidth(middle, Math.max(0, free - 4));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatKeyHint } from "../app-keybindings";
|
|
1
2
|
import type { Component } from "../tui";
|
|
2
3
|
import { OverlayPanel, PanelRows } from "../chrome/overlay-box";
|
|
3
4
|
import { type KeyId, matchesKey } from "../keys";
|
|
@@ -197,7 +198,7 @@ export class LiveVisualizer implements Component {
|
|
|
197
198
|
const frames = theme.spinnerFrames;
|
|
198
199
|
const icon = this.#phase === "working" ? frames[this.#frame % frames.length] : PHASE_ICONS[this.#phase];
|
|
199
200
|
const status = `${icon} ${this.#phase}`;
|
|
200
|
-
const fullLabel = ` ${status} · space mute ·
|
|
201
|
+
const fullLabel = ` ${status} · ${formatKeyHint("space")} mute · ${formatKeyHint("escape")} end `;
|
|
201
202
|
const shortLabel = ` ${status} `;
|
|
202
203
|
const label =
|
|
203
204
|
innerWidth >= visibleWidth(fullLabel) + 1
|
package/src/apps/ps-top.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `x` kill, `r` restart, `a` toggle all scopes, `q`/`esc`/`ctrl+c` quit.
|
|
7
7
|
* Sub-views (info, logs): `esc`/`q` back.
|
|
8
8
|
*/
|
|
9
|
+
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
9
10
|
import { KeyValueList, type KeyValueRow } from "../components/key-value-list";
|
|
10
11
|
import { ScrollView } from "../components/scroll-view";
|
|
11
12
|
import { renderTableRow, type TableColumn } from "../components/table";
|
|
@@ -281,11 +282,16 @@ export class PsTopComponent implements Component {
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
#renderTable(width: number, height: number): string[] {
|
|
284
|
-
const
|
|
285
|
+
const scopeKind = this.#all
|
|
286
|
+
? "(all)"
|
|
287
|
+
: this.#target.dir !== undefined || this.#target.global !== undefined
|
|
288
|
+
? "(target)"
|
|
289
|
+
: "(current + global)";
|
|
290
|
+
const scopesLabel = `${this.#flat.length} process${this.#flat.length === 1 ? "" : "es"} in ${this.#reports.length} scope${this.#reports.length === 1 ? "" : "s"} ${chalk.dim(scopeKind)}`;
|
|
285
291
|
const header = this.#header(width, scopesLabel);
|
|
286
292
|
const footer = this.#footer(
|
|
287
293
|
width,
|
|
288
|
-
"
|
|
294
|
+
`${formatKeyHints(["up", "down"])} select · ${formatKeyHint("enter")} info · ${formatKeyHint("l")} logs · ${formatKeyHint("s")} stop · ${formatKeyHint("x")} kill · ${formatKeyHint("r")} restart · ${formatKeyHint("a")} all scopes · ${formatKeyHint("q")} quit`,
|
|
289
295
|
);
|
|
290
296
|
const bodyHeight = height - 1 - footer.length;
|
|
291
297
|
|
|
@@ -350,7 +356,7 @@ export class PsTopComponent implements Component {
|
|
|
350
356
|
#renderInfo(width: number, height: number): string[] {
|
|
351
357
|
const info = this.#info;
|
|
352
358
|
const header = this.#header(width, "process info");
|
|
353
|
-
const footer = this.#footer(width, "
|
|
359
|
+
const footer = this.#footer(width, `${formatKeyHint("escape")} back · ${formatKeyHint("q")} back`);
|
|
354
360
|
const body = [""];
|
|
355
361
|
if (info) {
|
|
356
362
|
const daemon = info.daemon;
|
|
@@ -383,7 +389,10 @@ export class PsTopComponent implements Component {
|
|
|
383
389
|
width,
|
|
384
390
|
`logs ${chalk.bold(name)}${this.#logsState ? chalk.dim(` · ${this.#logsState}`) : ""}`,
|
|
385
391
|
);
|
|
386
|
-
const footer = this.#footer(
|
|
392
|
+
const footer = this.#footer(
|
|
393
|
+
width,
|
|
394
|
+
`${formatKeyHint("escape")} back · ${formatKeyHint("q")} back · view refreshes live`,
|
|
395
|
+
);
|
|
387
396
|
const bodyHeight = height - 1 - footer.length;
|
|
388
397
|
this.#logsView.setLines(this.#logsLines.map(line => ` ${line}`));
|
|
389
398
|
this.#logsView.setHeight(bodyHeight);
|
package/src/autocomplete.ts
CHANGED
|
@@ -6,6 +6,14 @@ import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
6
6
|
|
|
7
7
|
const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* How long an `@` fuzzy search may run before the immediate-directory prefix
|
|
11
|
+
* listing is reported through `onPartial`. Fuzzy walks of normal repos finish
|
|
12
|
+
* well under this, so they never flash an interim list; huge roots (a volume
|
|
13
|
+
* of sibling projects) take seconds and would otherwise show nothing new.
|
|
14
|
+
*/
|
|
15
|
+
const AT_PARTIAL_DELAY_MS = 150;
|
|
16
|
+
|
|
9
17
|
function buildAutocompleteFuzzyDiscoveryProfile(
|
|
10
18
|
query: string,
|
|
11
19
|
basePath: string,
|
|
@@ -149,6 +157,17 @@ export function subsequenceMatch(query: string, target: string): boolean {
|
|
|
149
157
|
return qi === query.length;
|
|
150
158
|
}
|
|
151
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Whether an `@` file completion `value` still fits the live `@` token.
|
|
162
|
+
* The editor narrows a stale `@` list with this while a fresh search runs;
|
|
163
|
+
* mirrors the subsequence filter `getSuggestions` applies to fuzzy results.
|
|
164
|
+
*/
|
|
165
|
+
export function atCompletionMatches(token: string, value: string): boolean {
|
|
166
|
+
const query = parsePathPrefix(token).rawPrefix.replaceAll("\\", "/").toLowerCase();
|
|
167
|
+
const target = parsePathPrefix(value).rawPrefix.replace(/"$/, "").toLowerCase();
|
|
168
|
+
return subsequenceMatch(query, target);
|
|
169
|
+
}
|
|
170
|
+
|
|
152
171
|
/** Ranked-tier subsequence score (100/80/60/40−gaps·5); higher is better, 0 is no match. */
|
|
153
172
|
export function subsequenceScore(query: string, target: string): number {
|
|
154
173
|
if (query.length === 0) return 1;
|
|
@@ -205,12 +224,16 @@ export interface SlashCommand {
|
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
export interface AutocompleteProvider {
|
|
208
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* Get autocomplete suggestions for current text/cursor position. Expensive providers SHOULD stop when `signal` aborts.
|
|
229
|
+
* Slow providers MAY report interim suggestions through `onPartial` before resolving; the resolved value supersedes them.
|
|
230
|
+
*/
|
|
209
231
|
getSuggestions(
|
|
210
232
|
lines: string[],
|
|
211
233
|
cursorLine: number,
|
|
212
234
|
cursorCol: number,
|
|
213
235
|
signal?: AbortSignal,
|
|
236
|
+
onPartial?: (suggestions: { items: AutocompleteItem[]; prefix: string }) => void,
|
|
214
237
|
): Promise<{
|
|
215
238
|
items: AutocompleteItem[];
|
|
216
239
|
prefix: string; // What we're matching against (e.g., "/" or "src/")
|
|
@@ -558,6 +581,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
558
581
|
cursorLine: number,
|
|
559
582
|
cursorCol: number,
|
|
560
583
|
signal?: AbortSignal,
|
|
584
|
+
onPartial?: (suggestions: { items: AutocompleteItem[]; prefix: string }) => void,
|
|
561
585
|
): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
|
|
562
586
|
if (signal?.aborted) return null;
|
|
563
587
|
const currentLine = lines[cursorLine] || "";
|
|
@@ -653,16 +677,27 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
653
677
|
if (items.length === 0) return null;
|
|
654
678
|
return { items, prefix: atPrefix };
|
|
655
679
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
680
|
+
if (rawPrefix.length === 0) {
|
|
681
|
+
const items = await this.#getFileSuggestions("@");
|
|
682
|
+
return items.length > 0 ? { items, prefix: atPrefix } : null;
|
|
683
|
+
}
|
|
684
|
+
const fuzzy = this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix, signal });
|
|
685
|
+
if (onPartial) {
|
|
686
|
+
const settled = await Promise.race([
|
|
687
|
+
fuzzy.then(() => true),
|
|
688
|
+
Bun.sleep(AT_PARTIAL_DELAY_MS).then(() => false),
|
|
689
|
+
]);
|
|
690
|
+
if (!settled) {
|
|
691
|
+
const listing = await this.#getFileSuggestions(atPrefix);
|
|
692
|
+
if (listing.length > 0 && !signal?.aborted) onPartial({ items: listing, prefix: atPrefix });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const suggestions = await fuzzy;
|
|
696
|
+
if (suggestions.length === 0) {
|
|
661
697
|
const fallback = await this.#getFileSuggestions(atPrefix);
|
|
662
698
|
if (fallback.length === 0) return null;
|
|
663
699
|
return { items: fallback, prefix: atPrefix };
|
|
664
700
|
}
|
|
665
|
-
if (suggestions.length === 0) return null;
|
|
666
701
|
|
|
667
702
|
return {
|
|
668
703
|
items: suggestions,
|
|
@@ -4,6 +4,7 @@ import { type Component } from "../tui";
|
|
|
4
4
|
import { Markdown } from "../components/markdown";
|
|
5
5
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { getMarkdownTheme, theme } from "../theme";
|
|
7
|
+
import { expandKeyHint } from "../render/render-utils";
|
|
7
8
|
import type { BranchSummaryMessage, CompactionSummaryMessage, CustomMessage } from "./messages";
|
|
8
9
|
|
|
9
10
|
/** Divider labels per compaction method; unknown/legacy methods fall back to "compacted". */
|
|
@@ -54,7 +55,7 @@ class DividerSummary implements Component {
|
|
|
54
55
|
const rule = theme.tree.horizontal;
|
|
55
56
|
const label = this.#label();
|
|
56
57
|
// sep.dot ships pre-padded (" · "); trim so the hint joins with single spaces.
|
|
57
|
-
const hint = `${theme.sep.dot.trim()}
|
|
58
|
+
const hint = `${theme.sep.dot.trim()} ${expandKeyHint()}`;
|
|
58
59
|
const plainWidth = Bun.stringWidth(`${label} ${hint}`, { countAnsiEscapeCodes: false });
|
|
59
60
|
// ` label hint ` framed by rules on both sides.
|
|
60
61
|
const remaining = width - plainWidth - 2;
|
|
@@ -15,6 +15,8 @@ import type { OutputArtifactError } from "../tools/streaming-output";
|
|
|
15
15
|
import { formatArtifactErrorNotice, formatTruncationMetaNotice, type TruncationMeta } from "../tools/output-meta";
|
|
16
16
|
import { DynamicBorder } from "../chrome/dynamic-border";
|
|
17
17
|
import { Ellipsis, truncateToWidth, visibleWidth } from "../utils";
|
|
18
|
+
import { interruptKey } from "../chrome/keybinding-hints";
|
|
19
|
+
import { expandKeyHint } from "../render/render-utils";
|
|
18
20
|
|
|
19
21
|
/** Output rows shown while an execution is collapsed. */
|
|
20
22
|
export const PREVIEW_LINES = 20;
|
|
@@ -59,7 +61,7 @@ export function buildExecutionFrame(
|
|
|
59
61
|
ui,
|
|
60
62
|
spinner => theme.fg(colorKey, spinner),
|
|
61
63
|
text => theme.fg("muted", text),
|
|
62
|
-
`Running… (
|
|
64
|
+
`Running… (${interruptKey()} to cancel)`,
|
|
63
65
|
getSymbolTheme().spinnerFrames,
|
|
64
66
|
);
|
|
65
67
|
|
|
@@ -84,7 +86,7 @@ export function buildStatusFooter(opts: {
|
|
|
84
86
|
const parts: string[] = [];
|
|
85
87
|
|
|
86
88
|
if (opts.hiddenLineCount > 0 && !opts.suppressHiddenCount) {
|
|
87
|
-
parts.push(theme.fg("dim", `… ${opts.hiddenLineCount} more lines (
|
|
89
|
+
parts.push(theme.fg("dim", `… ${opts.hiddenLineCount} more lines (${expandKeyHint()} to expand)`));
|
|
88
90
|
}
|
|
89
91
|
if (opts.status === "cancelled") {
|
|
90
92
|
parts.push(theme.fg("warning", "(cancelled)"));
|
|
@@ -937,7 +937,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
937
937
|
const benignSkip = this.#isBenignSkip();
|
|
938
938
|
const stateBgKey =
|
|
939
939
|
this.#isPartial || benignSkip ? "toolPendingBg" : this.#result?.isError ? "toolErrorBg" : "toolSuccessBg";
|
|
940
|
-
|
|
940
|
+
// bgFill, not bg: rows carry nested full resets (e.g. truncateToWidth's
|
|
941
|
+
// `\x1b[0m` before its ellipsis) that would otherwise punch holes in the tint.
|
|
942
|
+
const stateBgFn = (t: string) => theme.bgFill(stateBgKey, t);
|
|
941
943
|
|
|
942
944
|
// A benign skip is a synthetic placeholder for a call that never executed,
|
|
943
945
|
// so bypass any bespoke error frame and draw the neutral generic card —
|
|
@@ -2,6 +2,7 @@ import { Text } from "../components/text";
|
|
|
2
2
|
import { type Component, Container } from "../tui";
|
|
3
3
|
import { MessageNoticeComponent, type MessageNoticePresentation } from "../chrome/message-notice";
|
|
4
4
|
import { theme } from "../theme";
|
|
5
|
+
import { expandKeyHint } from "../render/render-utils";
|
|
5
6
|
|
|
6
7
|
/** Rule fields shown in rewind notifications. */
|
|
7
8
|
export interface NotificationRule {
|
|
@@ -83,7 +84,7 @@ export class TtsrNotificationComponent extends Container {
|
|
|
83
84
|
|
|
84
85
|
const body: Component[] = [new Text(theme.italic(displayText), 0, 0)];
|
|
85
86
|
if (truncated) {
|
|
86
|
-
body.push(new Text(theme.italic(
|
|
87
|
+
body.push(new Text(theme.italic(` (${expandKeyHint()} to expand)`), 0, 0));
|
|
87
88
|
}
|
|
88
89
|
return { icon: theme.icon.warning, header, body };
|
|
89
90
|
}
|
|
@@ -114,9 +115,9 @@ export class TtsrNotificationComponent extends Container {
|
|
|
114
115
|
|
|
115
116
|
const hidden = this.#rules.length - visible.length;
|
|
116
117
|
if (hidden > 0) {
|
|
117
|
-
body.push(new Text(theme.italic(`… +${hidden} more (
|
|
118
|
+
body.push(new Text(theme.italic(`… +${hidden} more (${expandKeyHint()} to expand)`), 0, 0));
|
|
118
119
|
} else if (elidedDetail) {
|
|
119
|
-
body.push(new Text(theme.italic(
|
|
120
|
+
body.push(new Text(theme.italic(` (${expandKeyHint()} to expand)`), 0, 0));
|
|
120
121
|
}
|
|
121
122
|
return { icon: theme.icon.warning, header, body };
|
|
122
123
|
}
|
package/src/chat/user-message.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
skillChipStyle,
|
|
16
16
|
} from "../prompt/composer-attachments";
|
|
17
17
|
import { MODEL_MENTION_TAG_RE } from "../prompt/model-mention-syntax";
|
|
18
|
-
import { fileHyperlink } from "../render";
|
|
18
|
+
import { expandKeyHint, fileHyperlink } from "../render";
|
|
19
19
|
import { imageReferenceHyperlink } from "../prompt/image-references";
|
|
20
20
|
import { highlightMagicKeywords } from "../prompt/magic-keywords";
|
|
21
21
|
import type { ReactionTarget } from "./reaction";
|
|
@@ -198,7 +198,7 @@ class SyntheticSummary implements Component {
|
|
|
198
198
|
render(width: number): readonly string[] {
|
|
199
199
|
width = Math.max(1, width);
|
|
200
200
|
if (this.#cache?.width === width) return this.#cache.lines;
|
|
201
|
-
const hint = `${theme.sep.dot.trim()}
|
|
201
|
+
const hint = `${theme.sep.dot.trim()} ${expandKeyHint()}`;
|
|
202
202
|
const lines = [` ${theme.fg("dim", truncateSummary(`${this.#summary} ${hint}`, Math.max(10, width - 1)))}`];
|
|
203
203
|
this.#cache = { width, lines };
|
|
204
204
|
return lines;
|
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utilities for formatting keybinding hints in the UI.
|
|
3
3
|
*/
|
|
4
|
-
import { getKeybindings, type Keybinding } from "../keybindings";
|
|
5
|
-
import {
|
|
4
|
+
import { getKeybindings, type KeyId, type Keybinding } from "../keybindings";
|
|
5
|
+
import {
|
|
6
|
+
type AppKeybinding,
|
|
7
|
+
formatKeyHint,
|
|
8
|
+
formatKeyHints,
|
|
9
|
+
type KeyName,
|
|
10
|
+
type KeybindingsManager,
|
|
11
|
+
} from "../app-keybindings";
|
|
6
12
|
import { theme } from "../theme/index";
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
|
-
*
|
|
15
|
+
* Primary (first) key bound to an editor action, formatted for footer hints;
|
|
16
|
+
* empty when unbound. Full alternatives: `formatKeyHints(getKeybindings().getKeys(action))`.
|
|
9
17
|
*/
|
|
10
18
|
export function editorKey(action: Keybinding): string {
|
|
11
|
-
|
|
19
|
+
const [key] = getKeybindings().getKeys(action);
|
|
20
|
+
return key ? formatKeyHint(key) : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Primary keys of several actions, slash-joined: `editorKeys("tui.select.up", "tui.select.down")` → `↑/↓`. */
|
|
24
|
+
export function editorKeys(...actions: Keybinding[]): string {
|
|
25
|
+
return actions.map(editorKey).join("/");
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
/**
|
|
15
|
-
*
|
|
29
|
+
* Keys bound to `action`, or `fallback` when the active registry has none — a
|
|
30
|
+
* TUI-only registry carries no `app.*` bindings. Mirrors the fallbacks in
|
|
31
|
+
* `keybinding-matchers.ts`, so hints name the keys the matcher accepts.
|
|
16
32
|
*/
|
|
33
|
+
export function boundKeys(action: Keybinding, fallback: readonly KeyId[]): readonly KeyId[] {
|
|
34
|
+
const keys = getKeybindings().getKeys(action);
|
|
35
|
+
return keys.length > 0 ? keys : fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Primary interrupt key (`app.interrupt`, raw Escape when unbound, as `matchesAppInterrupt`). */
|
|
39
|
+
export function interruptKey(): string {
|
|
40
|
+
return formatKeyHint(boundKeys("app.interrupt", ["escape"])[0] ?? "escape");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Primary key bound to an app action (see {@link editorKey}); all keys: `getDisplayString`. */
|
|
17
44
|
export function appKey(keybindings: KeybindingsManager, action: AppKeybinding): string {
|
|
18
|
-
|
|
45
|
+
const [key] = keybindings.getKeys(action);
|
|
46
|
+
return key ? formatKeyHint(key) : "";
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
/**
|
|
@@ -44,12 +72,9 @@ export function appKeyHint(keybindings: KeybindingsManager, action: AppKeybindin
|
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
/**
|
|
47
|
-
* Format a
|
|
48
|
-
*
|
|
49
|
-
* @param key - Raw key string
|
|
50
|
-
* @param description - Description text
|
|
51
|
-
* @returns Formatted string with dim key and muted description
|
|
75
|
+
* Format a hint for fixed (non-configurable) keys, e.g. `rawKeyHint(["up", "down"], "navigate")`.
|
|
76
|
+
* Alternatives render slash-separated (see {@link formatKeyHints}).
|
|
52
77
|
*/
|
|
53
|
-
export function rawKeyHint(
|
|
54
|
-
return theme.fg("dim",
|
|
78
|
+
export function rawKeyHint(keys: KeyName | readonly KeyName[], description: string): string {
|
|
79
|
+
return theme.fg("dim", formatKeyHints(keys)) + theme.fg("muted", ` ${description}`);
|
|
55
80
|
}
|