@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
|
@@ -42,6 +42,9 @@ import { matchesAppToolsExpand, matchesSelectCancel, matchesSelectDown, matchesS
|
|
|
42
42
|
import { ChatTranscriptBuilder } from "../chat/chat-transcript-builder";
|
|
43
43
|
import { TranscriptBrowser, type TranscriptBrowserFrame } from "../chat/transcript-browser";
|
|
44
44
|
import { padToWidth } from "../render/utils";
|
|
45
|
+
import { expandKeyHint } from "../render/render-utils";
|
|
46
|
+
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
47
|
+
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
45
48
|
import {
|
|
46
49
|
appendOutlineEntries,
|
|
47
50
|
type ComposedColumn,
|
|
@@ -486,16 +489,17 @@ export class RewindSelectorComponent implements Component {
|
|
|
486
489
|
prepared,
|
|
487
490
|
}).column;
|
|
488
491
|
const position = this.#targets.length > 0 ? `${this.#selected + 1}/${this.#targets.length} ` : "";
|
|
489
|
-
const
|
|
492
|
+
const upDown = editorKeys("tui.select.up", "tui.select.down");
|
|
493
|
+
const leftRight = formatKeyHints(["left", "right"]);
|
|
494
|
+
const lateral = columns.length > 0 ? `${leftRight} branches` : `${leftRight} user turns`;
|
|
495
|
+
const keys = `${upDown} step ${lateral} ${formatKeyHint("f")} filter ${formatKeyHint("enter")} rewind ${expandKeyHint()} expand ${editorKey("tui.select.cancel")} cancel`;
|
|
490
496
|
return {
|
|
491
497
|
header: [this.#header()],
|
|
492
498
|
body: {
|
|
493
499
|
lines: composed.lines,
|
|
494
500
|
anchor: this.#outlineAnchor(composed),
|
|
495
501
|
},
|
|
496
|
-
footer: [
|
|
497
|
-
theme.fg("dim", `${position}↑/↓ step ${lateral} f filter enter rewind ctrl+o expand esc cancel`),
|
|
498
|
-
],
|
|
502
|
+
footer: [theme.fg("dim", `${position}${keys}`)],
|
|
499
503
|
};
|
|
500
504
|
}
|
|
501
505
|
|
|
@@ -525,6 +529,8 @@ export class RewindSelectorComponent implements Component {
|
|
|
525
529
|
matches.length === 0
|
|
526
530
|
? theme.fg("error", "no matches")
|
|
527
531
|
: theme.fg("dim", `${selected >= 0 ? selected + 1 : "-"}/${matches.length}`);
|
|
532
|
+
const upDown = editorKeys("tui.select.up", "tui.select.down");
|
|
533
|
+
const keys = `${upDown} step ${formatKeyHints(["left", "right"])} user turns ${formatKeyHint("enter")} rewind ${editorKey("tui.select.cancel")} show all`;
|
|
528
534
|
return {
|
|
529
535
|
header: [this.#header()],
|
|
530
536
|
body: {
|
|
@@ -539,7 +545,7 @@ export class RewindSelectorComponent implements Component {
|
|
|
539
545
|
: undefined,
|
|
540
546
|
},
|
|
541
547
|
footer: [
|
|
542
|
-
`${theme.fg("accent", "filter:")} ${query}${theme.fg("accent", "▏")} ${count} ${theme.fg("dim",
|
|
548
|
+
`${theme.fg("accent", "filter:")} ${query}${theme.fg("accent", "▏")} ${count} ${theme.fg("dim", keys)}`,
|
|
543
549
|
],
|
|
544
550
|
};
|
|
545
551
|
}
|
|
@@ -2,8 +2,8 @@ import { type Component, Ellipsis, matchesKey, ScrollView, Text, truncateToWidth
|
|
|
2
2
|
import { theme } from "../theme/theme";
|
|
3
3
|
import { matchesSelectCancel } from "../keybinding-matchers";
|
|
4
4
|
import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { formatKeyHints } from "../app-keybindings";
|
|
6
|
+
import { editorKey } from "../chrome/keybinding-hints";
|
|
7
7
|
const PANEL_CHROME_ROWS = 4;
|
|
8
8
|
|
|
9
9
|
/** Terminal surface needed to size the session info viewport. */
|
|
@@ -74,7 +74,8 @@ export class SessionInfoOverlay implements Component {
|
|
|
74
74
|
|
|
75
75
|
render(width: number): readonly string[] {
|
|
76
76
|
const innerWidth = Math.max(1, width - 4);
|
|
77
|
-
|
|
77
|
+
const footerHint = `${formatKeyHints(["up", "down"])} scroll · ${editorKey("tui.select.cancel")} close`;
|
|
78
|
+
this.#footer.setLines([theme.fg("dim", truncateToWidth(footerHint, innerWidth))]);
|
|
78
79
|
|
|
79
80
|
const maxBodyHeight = Math.max(1, this.#host.terminal.rows - PANEL_CHROME_ROWS);
|
|
80
81
|
const fullWidthInfoLines = this.#info.render(innerWidth);
|
|
@@ -38,6 +38,8 @@ import { shortenPath } from "../render/render-utils";
|
|
|
38
38
|
import { HookSelectorComponent } from "./hook-selector";
|
|
39
39
|
import { bottomBorder, OverlayPanel, row, topBorder } from "../chrome/overlay-box";
|
|
40
40
|
import { MenuSelection, getMenuWindow } from "../components/menu-selection";
|
|
41
|
+
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
42
|
+
import { interruptKey } from "../chrome/keybinding-hints";
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Themed glyph + colored label for a session's lifecycle status, or `undefined`
|
|
@@ -615,7 +617,10 @@ class SessionList<T extends SessionSelectorEntry> implements Component {
|
|
|
615
617
|
} else {
|
|
616
618
|
// "Current folder" scope - hint to try "all"
|
|
617
619
|
lines.push(
|
|
618
|
-
truncateToWidth(
|
|
620
|
+
truncateToWidth(
|
|
621
|
+
theme.fg("muted", `No sessions in current folder. Press ${formatKeyHint("tab")} to view all.`),
|
|
622
|
+
width,
|
|
623
|
+
),
|
|
619
624
|
);
|
|
620
625
|
}
|
|
621
626
|
return lines;
|
|
@@ -1094,7 +1099,12 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1094
1099
|
/** Blank · keybinding hint · bottom border. Rendered by {@link render}. */
|
|
1095
1100
|
#footerLines(width: number): string[] {
|
|
1096
1101
|
const scopeHint = this.#scope === "all" ? "current folder" : "all projects";
|
|
1097
|
-
|
|
1102
|
+
// Keys mirror SessionList#handleInput; cancel is `app.interrupt` (raw Escape when unbound).
|
|
1103
|
+
const cancel = interruptKey();
|
|
1104
|
+
const hint = theme.fg(
|
|
1105
|
+
"muted",
|
|
1106
|
+
`[${formatKeyHints(["delete", "backspace"])} delete · ${formatKeyHint("enter")} select · ${formatKeyHint("tab")} ${scopeHint} · ${cancel} cancel]`,
|
|
1107
|
+
);
|
|
1098
1108
|
return [row("", width), row(hint, width), row("", width), bottomBorder(width)];
|
|
1099
1109
|
}
|
|
1100
1110
|
|
|
@@ -48,6 +48,8 @@ import { SnapcompactShapePreview } from "./snapcompact-shape-preview";
|
|
|
48
48
|
import { getPreset } from "../status-line/presets";
|
|
49
49
|
import { FormField, SelectFormField, TextFormField } from "../components/form";
|
|
50
50
|
import { formTheme } from "../chrome/form-theme";
|
|
51
|
+
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
52
|
+
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
55
|
* Free-text string setting field backed by the shared text form field.
|
|
@@ -70,7 +72,7 @@ function createSettingsTextField(
|
|
|
70
72
|
secret,
|
|
71
73
|
initialValue: currentValue || undefined,
|
|
72
74
|
empty: "submit",
|
|
73
|
-
hint: "
|
|
75
|
+
hint: ` ${editorKey("tui.input.submit")} to save · ${editorKey("tui.select.cancel")} to cancel · Clear field to unset`,
|
|
74
76
|
onSubmit,
|
|
75
77
|
onCancel,
|
|
76
78
|
requestRender,
|
|
@@ -106,7 +108,7 @@ function createSettingsSelectField(
|
|
|
106
108
|
onSelectionChange,
|
|
107
109
|
onSubmit: onSelect,
|
|
108
110
|
onCancel,
|
|
109
|
-
hint: "
|
|
111
|
+
hint: ` ${editorKey("tui.select.confirm")} to select · ${editorKey("tui.select.cancel")} to go back`,
|
|
110
112
|
footer,
|
|
111
113
|
requestRender,
|
|
112
114
|
});
|
|
@@ -172,9 +174,10 @@ class MultiSelectSubmenu extends Container {
|
|
|
172
174
|
this.#cursor = this.#options.findIndex(option => option.value === item.value);
|
|
173
175
|
};
|
|
174
176
|
this.#selectList.onCancel = this.#onClose;
|
|
177
|
+
const back = `${editorKey("tui.select.cancel")} to go back`;
|
|
175
178
|
const hint = this.#ordered
|
|
176
|
-
?
|
|
177
|
-
:
|
|
179
|
+
? ` Click to toggle · drag selected items to reorder · ${formatKeyHints(["left", "right"])} move · 1-9 place · ${back}`
|
|
180
|
+
: ` Click/${editorKey("tui.select.confirm")}/${formatKeyHint("space")} to toggle · ${back}`;
|
|
178
181
|
this.#field = new FormField(this.#selectList, {
|
|
179
182
|
theme: formTheme,
|
|
180
183
|
label: this.#title,
|
|
@@ -340,7 +343,7 @@ class ProviderLimitsSubmenu extends Container {
|
|
|
340
343
|
items,
|
|
341
344
|
maxVisible: 12,
|
|
342
345
|
selectTheme: getSelectListTheme(),
|
|
343
|
-
hint: "
|
|
346
|
+
hint: ` ${editorKey("tui.select.confirm")} to edit provider · ${editorKey("tui.select.cancel")} to go back`,
|
|
344
347
|
onSubmit: value => {
|
|
345
348
|
if (value === "__clear_all") {
|
|
346
349
|
this.#settings.set("providers.maxInFlightRequests", {});
|
|
@@ -369,7 +372,7 @@ class ProviderLimitsSubmenu extends Container {
|
|
|
369
372
|
"Enter a positive number. Decimals round down. Clear the field to make this provider unlimited.",
|
|
370
373
|
initialValue: limits[provider]?.toString() ?? undefined,
|
|
371
374
|
empty: "submit",
|
|
372
|
-
hint: "
|
|
375
|
+
hint: ` ${editorKey("tui.input.submit")} to save · ${editorKey("tui.select.cancel")} to cancel · Clear field to unset`,
|
|
373
376
|
validate: value => {
|
|
374
377
|
if (value.trim() === "") return undefined;
|
|
375
378
|
const limit = Number(value.trim());
|
|
@@ -567,17 +570,21 @@ export class SettingsSelectorComponent implements Component {
|
|
|
567
570
|
}
|
|
568
571
|
|
|
569
572
|
#footerHintText(): string {
|
|
573
|
+
const confirm = editorKey("tui.select.confirm");
|
|
574
|
+
const cancel = editorKey("tui.select.cancel");
|
|
575
|
+
const tab = formatKeyHint("tab");
|
|
576
|
+
const switchTabs = `${formatKeyHints(["left", "right"])} to switch tabs`;
|
|
570
577
|
if (this.#searchList) {
|
|
571
|
-
return
|
|
578
|
+
return `${confirm} to change · ${tab} to jump tabs · ${cancel} to exit search`;
|
|
572
579
|
}
|
|
573
580
|
if (this.#currentTabId === "plugins") {
|
|
574
|
-
return
|
|
581
|
+
return `${tab} to switch tabs · ${cancel} to close`;
|
|
575
582
|
}
|
|
576
583
|
if (this.#currentList?.sectionFocused) {
|
|
577
|
-
return "
|
|
584
|
+
return `${editorKeys("tui.select.up", "tui.select.down")} to jump sections · ${tab}/${confirm} to settings · ${switchTabs} · ${cancel} to close`;
|
|
578
585
|
}
|
|
579
|
-
const nav = this.#hasSectionJump ?
|
|
580
|
-
return
|
|
586
|
+
const nav = this.#hasSectionJump ? `${tab} to jump sections · ${switchTabs}` : `${tab} to switch tabs`;
|
|
587
|
+
return `${confirm}/${formatKeyHint("space")} to change · ${nav} · Type to search · ${cancel} to close`;
|
|
581
588
|
}
|
|
582
589
|
|
|
583
590
|
/** Single-line search banner: accent icon, editable query with live cursor, right-aligned match count. */
|
|
@@ -55,6 +55,8 @@ import { canonicalizeMessage } from "../chat/thinking-display";
|
|
|
55
55
|
import { resolveAssistantErrorPresentation } from "../chat/transcript-render-helpers";
|
|
56
56
|
import { OverlayPanel, PanelDivider } from "../chrome/overlay-box";
|
|
57
57
|
import { TreeView, type TreeRow } from "../components/tree-view";
|
|
58
|
+
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
59
|
+
import { editorKeys, interruptKey } from "../chrome/keybinding-hints";
|
|
58
60
|
|
|
59
61
|
/** Filter mode for tree display */
|
|
60
62
|
type FilterMode = TreeFilterMode;
|
|
@@ -523,7 +525,9 @@ class TreeList implements Component {
|
|
|
523
525
|
lines.push(truncateToWidth(theme.fg("muted", `(0/0)${this.#getFilterLabel()}`), width));
|
|
524
526
|
} else if (this.#searchQuery.length > 0) {
|
|
525
527
|
lines.push(truncateToWidth(theme.fg("muted", `No entries match search "${this.#searchQuery}"`), width));
|
|
526
|
-
lines.push(
|
|
528
|
+
lines.push(
|
|
529
|
+
truncateToWidth(theme.fg("muted", `Press ${formatKeyHint("backspace")} to clear the search`), width),
|
|
530
|
+
);
|
|
527
531
|
lines.push(truncateToWidth(theme.fg("muted", `(0/${totalCount})${this.#getFilterLabel()}`), width));
|
|
528
532
|
} else {
|
|
529
533
|
const filterLabel = this.#getFilterLabel().trim() || "[default]";
|
|
@@ -533,7 +537,15 @@ class TreeList implements Component {
|
|
|
533
537
|
width,
|
|
534
538
|
),
|
|
535
539
|
);
|
|
536
|
-
lines.push(
|
|
540
|
+
lines.push(
|
|
541
|
+
truncateToWidth(
|
|
542
|
+
theme.fg(
|
|
543
|
+
"muted",
|
|
544
|
+
`Press ${formatKeyHint("alt+a")} to show all, ${formatKeyHint("alt+d")} for default`,
|
|
545
|
+
),
|
|
546
|
+
width,
|
|
547
|
+
),
|
|
548
|
+
);
|
|
537
549
|
lines.push(truncateToWidth(theme.fg("muted", `(0/${totalCount})${this.#getFilterLabel()}`), width));
|
|
538
550
|
}
|
|
539
551
|
return lines;
|
|
@@ -1010,7 +1022,8 @@ class LabelInput implements Component {
|
|
|
1010
1022
|
const lines: string[] = [];
|
|
1011
1023
|
lines.push(truncateToWidth(theme.fg("muted", "Label (empty to remove):"), width));
|
|
1012
1024
|
lines.push(...this.#input.render(width));
|
|
1013
|
-
|
|
1025
|
+
const cancel = interruptKey();
|
|
1026
|
+
lines.push(truncateToWidth(theme.fg("dim", `${formatKeyHint("enter")}: save ${cancel}: cancel`), width));
|
|
1014
1027
|
return lines;
|
|
1015
1028
|
}
|
|
1016
1029
|
|
|
@@ -1068,7 +1081,17 @@ export class TreeSelectorComponent extends OverlayPanel {
|
|
|
1068
1081
|
new TruncatedText(
|
|
1069
1082
|
theme.fg(
|
|
1070
1083
|
"muted",
|
|
1071
|
-
|
|
1084
|
+
[
|
|
1085
|
+
`${formatKeyHint("enter")}: switch.`,
|
|
1086
|
+
`${formatKeyHints(["alt+up", "alt+down"])}: previous/next turn.`,
|
|
1087
|
+
`${editorKeys("tui.select.pageUp", "tui.select.pageDown")} (${formatKeyHints(["left", "right"])}): page.`,
|
|
1088
|
+
`${formatKeyHints(["home", "end"])}: first/last item.`,
|
|
1089
|
+
`${formatKeyHint("shift+enter")}: summarize & switch.`,
|
|
1090
|
+
`${formatKeyHint("shift+l")}: label.`,
|
|
1091
|
+
`${formatKeyHint("ctrl+o")}: filter.`,
|
|
1092
|
+
`${formatKeyHints(["alt+d", "alt+t", "alt+u", "alt+l", "alt+a"])}: filter.`,
|
|
1093
|
+
"Type to search",
|
|
1094
|
+
].join(" "),
|
|
1072
1095
|
),
|
|
1073
1096
|
0,
|
|
1074
1097
|
0,
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
matchesSelectUp,
|
|
39
39
|
} from "../keybinding-matchers";
|
|
40
40
|
import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
|
|
41
|
+
import { formatKeyHint } from "../app-keybindings";
|
|
42
|
+
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
41
43
|
|
|
42
44
|
/** Local calendar-day activity consumed by the usage heatmap. */
|
|
43
45
|
export interface DailyActivityPoint {
|
|
@@ -725,8 +727,12 @@ export class UsageDashboardComponent implements Component {
|
|
|
725
727
|
const checkedText = latestFetchedAt ? `checked ${formatDuration(this.#nowMs - latestFetchedAt)} ago` : "";
|
|
726
728
|
const title = this.#view === "detail" ? "Usage · Details" : "Usage";
|
|
727
729
|
|
|
728
|
-
const scrollHint = maxScroll > 0 ? "
|
|
729
|
-
const
|
|
730
|
+
const scrollHint = maxScroll > 0 ? `${editorKeys("tui.select.up", "tui.select.down")} scroll · ` : "";
|
|
731
|
+
const cancel = editorKey("tui.select.cancel");
|
|
732
|
+
const hint =
|
|
733
|
+
this.#view === "detail"
|
|
734
|
+
? `${scrollHint}${cancel} back`
|
|
735
|
+
: `${scrollHint}${formatKeyHint("enter")} details · ${cancel} close`;
|
|
730
736
|
this.#panel.title = title;
|
|
731
737
|
this.#header.setLines([checkedText ? theme.fg("dim", checkedText) : ""]);
|
|
732
738
|
this.#body.setLines(contentSource.slice(this.#scroll, this.#scroll + contentRows));
|
|
@@ -5,6 +5,7 @@ import { getComposerCacheDir } from "@oh-my-pi/pi-utils/dirs";
|
|
|
5
5
|
import type { LspServerInfo, RecentSession } from "./welcome";
|
|
6
6
|
import type { ComposerPreferences, ComposerStatusSnapshot } from "./composer";
|
|
7
7
|
import type { SymbolPreset } from "../theme/theme";
|
|
8
|
+
import { isWordCompletionMethod } from "./word-completion";
|
|
8
9
|
|
|
9
10
|
const CACHE_VERSION = 1;
|
|
10
11
|
const STATUS_CACHE_VERSION = 3;
|
|
@@ -204,7 +205,7 @@ function readUiState(file: string): { preferences: ComposerPreferences; theme: C
|
|
|
204
205
|
typeof imeSafeCursor !== "boolean" ||
|
|
205
206
|
typeof autocompleteMaxVisible !== "number" ||
|
|
206
207
|
typeof spellingTypoDetection !== "boolean" ||
|
|
207
|
-
|
|
208
|
+
!isWordCompletionMethod(spellingAutocomplete) ||
|
|
208
209
|
typeof spellingAutocorrect !== "boolean"
|
|
209
210
|
) {
|
|
210
211
|
return undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer hints: the right-aligned placeholder shown in the empty composer to
|
|
3
|
+
* surface gestures that matter right now (e.g. background agents are running).
|
|
4
|
+
*
|
|
5
|
+
* Hints are checked in priority order; the first one that applies wins. A hint
|
|
6
|
+
* retires for good once the user has performed its gesture
|
|
7
|
+
* {@link ComposerHint.learnedAfter} times — the host records each use under the
|
|
8
|
+
* hint's id and reports counts through {@link ComposerHintContext.uses}.
|
|
9
|
+
*
|
|
10
|
+
* Add a hint by appending to {@link COMPOSER_HINTS}, extending
|
|
11
|
+
* {@link ComposerHintId}, and recording the gesture where it is handled.
|
|
12
|
+
*/
|
|
13
|
+
import { formatDoubleTap, formatKeyHint, type KeybindingsManager } from "../app-keybindings";
|
|
14
|
+
import { theme } from "../theme/theme";
|
|
15
|
+
|
|
16
|
+
/** Stable hint ids; persisted as usage-counter keys, so never rename one. */
|
|
17
|
+
export type ComposerHintId = "agents" | "effort";
|
|
18
|
+
|
|
19
|
+
/** Live state that hint conditions read. Built by the host on every render of the empty composer. */
|
|
20
|
+
export interface ComposerHintContext {
|
|
21
|
+
/** Subagents currently running in the background. */
|
|
22
|
+
runningAgents: number;
|
|
23
|
+
/** The view is focused on a subagent session instead of the main one. */
|
|
24
|
+
focusedOnAgent: boolean;
|
|
25
|
+
/** The viewed session already has messages. */
|
|
26
|
+
conversationStarted: boolean;
|
|
27
|
+
keybindings: KeybindingsManager;
|
|
28
|
+
/** Times the user has performed the gesture a hint teaches. */
|
|
29
|
+
uses(id: ComposerHintId): number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A gesture tip: `key` renders highlighted, `label` describes what it does. */
|
|
33
|
+
export interface ComposerHint {
|
|
34
|
+
id: ComposerHintId;
|
|
35
|
+
/** Uses of the gesture after which the hint stops showing. */
|
|
36
|
+
learnedAfter: number;
|
|
37
|
+
/** Key and label when the hint applies, otherwise `undefined`. */
|
|
38
|
+
resolve(ctx: ComposerHintContext): { key: string; label: string } | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Registered hints, highest priority first. */
|
|
42
|
+
export const COMPOSER_HINTS: readonly ComposerHint[] = [
|
|
43
|
+
{
|
|
44
|
+
// Matches the ←← gesture in the input controller, which opens the agent hub
|
|
45
|
+
// from the main session (in a focused view it hops to the parent instead).
|
|
46
|
+
id: "agents",
|
|
47
|
+
learnedAfter: 3,
|
|
48
|
+
resolve: ctx => {
|
|
49
|
+
if (ctx.runningAgents === 0 || ctx.focusedOnAgent) return undefined;
|
|
50
|
+
return {
|
|
51
|
+
key: formatDoubleTap("left"),
|
|
52
|
+
label: `to see ${ctx.runningAgents} running ${ctx.runningAgents === 1 ? "agent" : "agents"}`,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "effort",
|
|
58
|
+
learnedAfter: 3,
|
|
59
|
+
resolve: ctx => {
|
|
60
|
+
if (ctx.conversationStarted) return undefined;
|
|
61
|
+
const key = ctx.keybindings.getKeys("app.thinking.cycle")[0];
|
|
62
|
+
return key ? { key: formatKeyHint(key), label: "to change thinking effort" } : undefined;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/** Render the highest-priority applicable, not-yet-learned hint, if any. */
|
|
68
|
+
export function resolveComposerHint(ctx: ComposerHintContext): string | undefined {
|
|
69
|
+
for (const hint of COMPOSER_HINTS) {
|
|
70
|
+
if (ctx.uses(hint.id) >= hint.learnedAfter) continue;
|
|
71
|
+
const resolved = hint.resolve(ctx);
|
|
72
|
+
if (resolved) return `${theme.fg("accent", resolved.key)} ${theme.fg("dim", theme.italic(resolved.label))}`;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
package/src/prompt/composer.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { sliceWithWidth, truncateToWidth, visibleWidth } from "../utils";
|
|
16
16
|
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
17
17
|
import { CustomEditor } from "./custom-editor";
|
|
18
|
+
import type { WordCompletionMethod } from "./word-completion";
|
|
18
19
|
import { type AnimationFrame, TranscriptContainer } from "../chrome/transcript-container";
|
|
19
20
|
import { type LspServerInfo, type RecentSession, WelcomeComponent } from "./welcome";
|
|
20
21
|
import { ensureThemeSync, getEditorTheme, theme } from "../theme/theme";
|
|
@@ -31,7 +32,7 @@ export interface ComposerPreferences {
|
|
|
31
32
|
readonly imeSafeCursor: boolean;
|
|
32
33
|
readonly autocompleteMaxVisible: number;
|
|
33
34
|
readonly spellingTypoDetection: boolean;
|
|
34
|
-
readonly spellingAutocomplete:
|
|
35
|
+
readonly spellingAutocomplete: WordCompletionMethod;
|
|
35
36
|
readonly spellingAutocorrect: boolean;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -45,7 +46,7 @@ export const COMPOSER_DEFAULTS: ComposerPreferences = {
|
|
|
45
46
|
imeSafeCursor: false,
|
|
46
47
|
autocompleteMaxVisible: 10,
|
|
47
48
|
spellingTypoDetection: true,
|
|
48
|
-
spellingAutocomplete:
|
|
49
|
+
spellingAutocomplete: "auto",
|
|
49
50
|
spellingAutocorrect: false,
|
|
50
51
|
};
|
|
51
52
|
|
|
@@ -26,11 +26,18 @@ import {
|
|
|
26
26
|
skillChipStyle,
|
|
27
27
|
skillToken,
|
|
28
28
|
} from "./composer-attachments";
|
|
29
|
-
import {
|
|
29
|
+
import { type MacOSSpellingFeatures, MacOSSpellingProvider } from "./macos-spelling";
|
|
30
30
|
import { hasMagicKeyword, highlightMagicKeywords } from "./magic-keywords";
|
|
31
31
|
import { isQueuedMessageList, parseQueueShorthand, QUEUE_LIST_MARKER_RE } from "./queue-input";
|
|
32
|
+
import { type WordCompletionMethod, WordCompletionProvider } from "./word-completion";
|
|
32
33
|
import { fgOrPlain, theme } from "../theme/theme";
|
|
33
34
|
|
|
35
|
+
/** Independently switchable prose-assistance features of the composer. */
|
|
36
|
+
export interface SpellingFeatures extends MacOSSpellingFeatures {
|
|
37
|
+
/** Word-completion engine; `off` disables ghost text. */
|
|
38
|
+
autocomplete: WordCompletionMethod;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
type ConfigurableEditorAction = Extract<
|
|
35
42
|
AppKeybinding,
|
|
36
43
|
| "app.interrupt"
|
|
@@ -374,6 +381,7 @@ export type ComposerChipDescriptor =
|
|
|
374
381
|
*/
|
|
375
382
|
export class CustomEditor extends Editor {
|
|
376
383
|
#spelling = new MacOSSpellingProvider();
|
|
384
|
+
#wordCompletion = new WordCompletionProvider();
|
|
377
385
|
imageLinks?: readonly (string | undefined)[];
|
|
378
386
|
|
|
379
387
|
/** Draft images pasted into the composer, consumed on submit. Co-located with
|
|
@@ -432,14 +440,24 @@ export class CustomEditor extends Editor {
|
|
|
432
440
|
this.#requestShimmerRepaint?.();
|
|
433
441
|
};
|
|
434
442
|
this.#spelling.onUpdate = requestTextAssistRepaint;
|
|
443
|
+
this.#wordCompletion.onUpdate = requestTextAssistRepaint;
|
|
435
444
|
this.onTextAssistApplied = requestTextAssistRepaint;
|
|
436
|
-
this.setTextAssistProvider(
|
|
445
|
+
this.setTextAssistProvider({
|
|
446
|
+
getWordCompletion: (lines, cursorLine, cursorCol) =>
|
|
447
|
+
this.#wordCompletion.getWordCompletion(lines, cursorLine, cursorCol),
|
|
448
|
+
wordCompletionFeedback: (lines, cursorLine, cursorCol, suggestion, accepted) =>
|
|
449
|
+
this.#wordCompletion.wordCompletionFeedback(lines, cursorLine, cursorCol, suggestion, accepted),
|
|
450
|
+
tryAutocorrect: (lines, cursorLine, cursorCol) => this.#spelling.tryAutocorrect(lines, cursorLine, cursorCol),
|
|
451
|
+
getWordReplacements: (lines, cursorLine, cursorCol) =>
|
|
452
|
+
this.#spelling.getWordReplacements(lines, cursorLine, cursorCol),
|
|
453
|
+
});
|
|
437
454
|
if (args[0] instanceof TUI) this.tui = args[0];
|
|
438
455
|
}
|
|
439
456
|
|
|
440
|
-
/** Independently configure typo detection, word
|
|
457
|
+
/** Independently configure typo detection, the word-completion engine, and autocorrect. */
|
|
441
458
|
setSpellingFeatures(features: SpellingFeatures): void {
|
|
442
|
-
this.#spelling.setFeatures(features);
|
|
459
|
+
this.#spelling.setFeatures({ typoDetection: features.typoDetection, autocorrect: features.autocorrect });
|
|
460
|
+
this.#wordCompletion.setMethod(features.autocomplete);
|
|
443
461
|
}
|
|
444
462
|
|
|
445
463
|
/** Clear the composer draft: optionally commit `historyText` to history, then
|