@oh-my-pi/pi-tui 18.3.1 → 18.3.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 +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/overlays/rewind-selector.d.ts +4 -2
- 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/status-line/host.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 +66 -27
- 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 +193 -13
- 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/status-line/host.ts +1 -1
- package/src/status-line/segments.ts +2 -2
- 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
|
@@ -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
|
|
@@ -4,6 +4,7 @@ import type { EditorInlineReplacement, EditorTextAssistProvider, EditorWordRepla
|
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { isMagicKeyword } from "./magic-keywords";
|
|
6
6
|
import { maskNonProse } from "./markdown-prose";
|
|
7
|
+
import { isProseWord, lineContext, ProseSource, type SpellingDecorationContext } from "./prose-gate";
|
|
7
8
|
|
|
8
9
|
/** Styled underline: red curly undercurl via colon-subparameter SGR (4:3 + SGR 58 color). */
|
|
9
10
|
const STYLED_TYPO_MARKS = { start: "\x1b[4:3m\x1b[58:2::255:95:95m", end: "\x1b[4:0m\x1b[59m" } as const;
|
|
@@ -14,35 +15,20 @@ const STYLED_TYPO_MARKS = { start: "\x1b[4:3m\x1b[58:2::255:95:95m", end: "\x1b[
|
|
|
14
15
|
*/
|
|
15
16
|
const FLAT_TYPO_MARKS = { start: "\x1b[4m", end: "\x1b[24m" } as const;
|
|
16
17
|
|
|
17
|
-
const WORD_SUFFIX = /[\p{L}\p{M}']+$/u;
|
|
18
18
|
const COMPLETED_WORD = /([\p{L}\p{M}']+)([\s.,;:!?"\])}])$/u;
|
|
19
|
-
const CODEISH_CHARACTERS = "\\/@_=:{}[]<>";
|
|
20
|
-
const CAMEL_CASE = /\p{Ll}\p{Lu}/u;
|
|
21
19
|
const CACHE_LIMIT = 256;
|
|
22
|
-
const MAX_SPELLING_BUFFER_LENGTH = 20_000;
|
|
23
|
-
const MAX_SPELLING_LINE_LENGTH = 1_000;
|
|
24
20
|
const WORD_BOUNDARY = /[\s.,;:!?"\])}]/u;
|
|
25
21
|
|
|
26
22
|
/** Independently switchable macOS prose-assistance features. */
|
|
27
|
-
export interface
|
|
23
|
+
export interface MacOSSpellingFeatures {
|
|
28
24
|
typoDetection: boolean;
|
|
29
|
-
autocomplete: boolean;
|
|
30
25
|
autocorrect: boolean;
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
/** Logical source location for one rendered editor segment. */
|
|
34
|
-
export interface SpellingDecorationContext {
|
|
35
|
-
editorText: string;
|
|
36
|
-
lines: readonly string[];
|
|
37
|
-
line: number;
|
|
38
|
-
startCol: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
28
|
/** Native spelling operations used by {@link MacOSSpellingProvider}. */
|
|
42
29
|
export interface SpellingBackend {
|
|
43
30
|
isAvailable(): boolean;
|
|
44
31
|
checkSpelling(text: string): Promise<readonly native.SpellingRange[]>;
|
|
45
|
-
completeWord(text: string, start: number, length: number): Promise<readonly string[]>;
|
|
46
32
|
autocorrectWord(text: string, start: number, length: number): Promise<string | null>;
|
|
47
33
|
spellingGuesses(text: string, start: number, length: number): Promise<readonly string[]>;
|
|
48
34
|
}
|
|
@@ -51,41 +37,21 @@ const NATIVE_BACKEND: SpellingBackend = {
|
|
|
51
37
|
isAvailable: () =>
|
|
52
38
|
typeof native.macOSSpellCheckerAvailable === "function" &&
|
|
53
39
|
typeof native.macOSCheckSpelling === "function" &&
|
|
54
|
-
typeof native.macOSCompleteWord === "function" &&
|
|
55
40
|
typeof native.macOSAutocorrectWord === "function" &&
|
|
56
41
|
typeof native.macOSSpellingGuesses === "function" &&
|
|
57
42
|
native.macOSSpellCheckerAvailable(),
|
|
58
43
|
checkSpelling: text => native.macOSCheckSpelling(text),
|
|
59
|
-
completeWord: (text, start, length) => native.macOSCompleteWord(text, start, length),
|
|
60
44
|
autocorrectWord: (text, start, length) => native.macOSAutocorrectWord(text, start, length),
|
|
61
45
|
spellingGuesses: (text, start, length) => native.macOSSpellingGuesses(text, start, length),
|
|
62
46
|
};
|
|
63
47
|
|
|
64
|
-
function tokenAt(text: string, start: number, end: number): string {
|
|
65
|
-
let tokenStart = start;
|
|
66
|
-
while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1] ?? "")) tokenStart--;
|
|
67
|
-
let tokenEnd = end;
|
|
68
|
-
while (tokenEnd < text.length && !/\s/.test(text[tokenEnd] ?? "")) tokenEnd++;
|
|
69
|
-
return text.slice(tokenStart, tokenEnd);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function isProseWord(text: string, masked: string, start: number, end: number): boolean {
|
|
73
|
-
if (start < 0 || end <= start || end > text.length) return false;
|
|
74
|
-
if (masked.slice(start, end).trim().length === 0) return false;
|
|
75
|
-
const token = tokenAt(text, start, end);
|
|
76
|
-
for (const char of token) {
|
|
77
|
-
if (CODEISH_CHARACTERS.includes(char)) return false;
|
|
78
|
-
}
|
|
79
|
-
if (CAMEL_CASE.test(token) || /\d/.test(token)) return false;
|
|
80
|
-
return !text.trimStart().startsWith("/") && !text.startsWith("->") && !text.startsWith("=>");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
48
|
/**
|
|
84
|
-
* Bridges Apple's spelling service into the editor's
|
|
85
|
-
*
|
|
49
|
+
* Bridges Apple's spelling service into the editor's typo and autocorrection
|
|
50
|
+
* paths. Word completion is the cross-platform `WordCompletionProvider`
|
|
51
|
+
* (`word-completion.ts`).
|
|
86
52
|
*/
|
|
87
53
|
export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
88
|
-
#features:
|
|
54
|
+
#features: MacOSSpellingFeatures = { typoDetection: false, autocorrect: false };
|
|
89
55
|
#available: boolean;
|
|
90
56
|
#availabilityChecked = false;
|
|
91
57
|
#cacheGeneration = 0;
|
|
@@ -93,12 +59,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
93
59
|
#typoInFlight = new Map<string, Promise<readonly native.SpellingRange[]>>();
|
|
94
60
|
#automaticTypoActive = false;
|
|
95
61
|
#automaticTypoQueue = new Map<string, string>();
|
|
96
|
-
#
|
|
97
|
-
#completionActiveKey: string | undefined;
|
|
98
|
-
#completionQueued: { key: string; line: string; start: number; prefix: string } | undefined;
|
|
99
|
-
#sourceText = "";
|
|
100
|
-
#sourceMask = "";
|
|
101
|
-
#sourceLineOffsets: number[] = [];
|
|
62
|
+
#prose = new ProseSource();
|
|
102
63
|
/** Underline open/close pair, chosen once from the terminal's styled-underline capability. */
|
|
103
64
|
readonly #marks: { start: string; end: string };
|
|
104
65
|
|
|
@@ -113,17 +74,16 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
113
74
|
this.#marks = styledUnderlines ? STYLED_TYPO_MARKS : FLAT_TYPO_MARKS;
|
|
114
75
|
}
|
|
115
76
|
|
|
116
|
-
/** Apply
|
|
117
|
-
setFeatures(features:
|
|
77
|
+
/** Apply both independent feature gates and invalidate rendered typo ranges. */
|
|
78
|
+
setFeatures(features: MacOSSpellingFeatures): void {
|
|
118
79
|
if (
|
|
119
80
|
this.#features.typoDetection === features.typoDetection &&
|
|
120
|
-
this.#features.autocomplete === features.autocomplete &&
|
|
121
81
|
this.#features.autocorrect === features.autocorrect
|
|
122
82
|
) {
|
|
123
83
|
return;
|
|
124
84
|
}
|
|
125
85
|
this.#features = { ...features };
|
|
126
|
-
if (!this.#availabilityChecked && (features.typoDetection || features.
|
|
86
|
+
if (!this.#availabilityChecked && (features.typoDetection || features.autocorrect)) {
|
|
127
87
|
this.#availabilityChecked = true;
|
|
128
88
|
this.#available = typeof this.backend.isAvailable === "function" && this.backend.isAvailable();
|
|
129
89
|
}
|
|
@@ -137,7 +97,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
137
97
|
decorate: (span: string) => string = value => value,
|
|
138
98
|
): string {
|
|
139
99
|
if (!this.#available || !this.#features.typoDetection || text.length === 0) return decorate(text);
|
|
140
|
-
if (!this.#
|
|
100
|
+
if (!this.#prose.isProse(context, context.startCol, context.startCol + text.length)) {
|
|
141
101
|
return decorate(text);
|
|
142
102
|
}
|
|
143
103
|
const lane = `${context.line}:${context.startCol}`;
|
|
@@ -160,7 +120,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
160
120
|
// must never re-emit already-rendered text: that doubles it on screen and
|
|
161
121
|
// desyncs the rendered width from the measured width (cursor drift).
|
|
162
122
|
if (range.start < cursor || end > text.length) continue;
|
|
163
|
-
if (!this.#
|
|
123
|
+
if (!this.#prose.isProse(context, context.startCol + range.start, context.startCol + end)) {
|
|
164
124
|
continue;
|
|
165
125
|
}
|
|
166
126
|
rendered += decorate(text.slice(cursor, range.start));
|
|
@@ -170,26 +130,6 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
170
130
|
return rendered + decorate(text.slice(cursor));
|
|
171
131
|
}
|
|
172
132
|
|
|
173
|
-
/** Return the cached macOS completion suffix for the word ending at the cursor. */
|
|
174
|
-
getWordCompletion(lines: string[], cursorLine: number, cursorCol: number): string | null {
|
|
175
|
-
if (!this.#available || !this.#features.autocomplete) return null;
|
|
176
|
-
const line = lines[cursorLine] ?? "";
|
|
177
|
-
if (/^[\p{L}\p{M}']/u.test(line.slice(cursorCol))) return null;
|
|
178
|
-
const match = WORD_SUFFIX.exec(line.slice(0, cursorCol));
|
|
179
|
-
if (!match || match[0].length < 2) return null;
|
|
180
|
-
const start = cursorCol - match[0].length;
|
|
181
|
-
const masked = maskNonProse(line);
|
|
182
|
-
if (!isProseWord(line, masked, start, cursorCol)) return null;
|
|
183
|
-
const context = this.#context(lines, cursorLine);
|
|
184
|
-
if (!this.#sourceRangeIsProse(context, start, cursorCol)) return null;
|
|
185
|
-
|
|
186
|
-
const prefix = match[0];
|
|
187
|
-
const key = `${start}:${prefix.length}:${line}`;
|
|
188
|
-
if (this.#completionCache.has(key)) return this.#completionCache.get(key) ?? null;
|
|
189
|
-
this.#scheduleWordCompletion(key, line, start, prefix);
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
133
|
/** Return the confident macOS correction after a completed prose word. */
|
|
194
134
|
async tryAutocorrect(
|
|
195
135
|
lines: string[],
|
|
@@ -207,8 +147,8 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
207
147
|
const start = match.index;
|
|
208
148
|
const masked = maskNonProse(textBeforeCursor);
|
|
209
149
|
if (!isProseWord(textBeforeCursor, masked, start, start + word.length)) return null;
|
|
210
|
-
const context =
|
|
211
|
-
if (!this.#
|
|
150
|
+
const context = lineContext(lines, cursorLine);
|
|
151
|
+
if (!this.#prose.isProse(context, start, start + word.length)) return null;
|
|
212
152
|
try {
|
|
213
153
|
const correction = await this.backend.autocorrectWord(textBeforeCursor, start, word.length);
|
|
214
154
|
if (!correction || correction === word) return null;
|
|
@@ -228,15 +168,15 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
228
168
|
if (!this.#available || !this.#features.typoDetection) return null;
|
|
229
169
|
|
|
230
170
|
const line = lines[cursorLine] ?? "";
|
|
231
|
-
const context =
|
|
232
|
-
if (!this.#
|
|
171
|
+
const context = lineContext(lines, cursorLine);
|
|
172
|
+
if (!this.#prose.isProse(context, 0, line.length)) return null;
|
|
233
173
|
const ranges = this.#typoCache.get(line) ?? (await this.#loadTypoRanges(line));
|
|
234
174
|
const range = ranges.find(candidate => {
|
|
235
175
|
const end = candidate.start + candidate.length;
|
|
236
176
|
return (
|
|
237
177
|
cursorCol >= candidate.start &&
|
|
238
178
|
(cursorCol <= end || (cursorCol === end + 1 && WORD_BOUNDARY.test(line[end] ?? ""))) &&
|
|
239
|
-
this.#
|
|
179
|
+
this.#prose.isProse(context, candidate.start, end)
|
|
240
180
|
);
|
|
241
181
|
});
|
|
242
182
|
if (!range || !this.#available || !this.#features.typoDetection) return null;
|
|
@@ -365,92 +305,11 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
365
305
|
return ranges;
|
|
366
306
|
}
|
|
367
307
|
|
|
368
|
-
#scheduleWordCompletion(key: string, line: string, start: number, prefix: string): void {
|
|
369
|
-
if (this.#completionActiveKey === key || this.#completionQueued?.key === key) return;
|
|
370
|
-
if (this.#completionActiveKey !== undefined) {
|
|
371
|
-
this.#completionQueued = { key, line, start, prefix };
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
this.#startWordCompletion(key, line, start, prefix);
|
|
375
|
-
}
|
|
376
|
-
#startWordCompletion(key: string, line: string, start: number, prefix: string): void {
|
|
377
|
-
this.#completionActiveKey = key;
|
|
378
|
-
const generation = this.#cacheGeneration;
|
|
379
|
-
const request = this.#fetchWordCompletion(key, line, start, prefix, generation);
|
|
380
|
-
const finished = (): void => {
|
|
381
|
-
if (this.#completionActiveKey === key) this.#completionActiveKey = undefined;
|
|
382
|
-
const queued = this.#completionQueued;
|
|
383
|
-
this.#completionQueued = undefined;
|
|
384
|
-
if (queued && this.#available && this.#features.autocomplete && !this.#completionCache.has(queued.key)) {
|
|
385
|
-
this.#startWordCompletion(queued.key, queued.line, queued.start, queued.prefix);
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
void request.then(finished, finished);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async #fetchWordCompletion(
|
|
392
|
-
key: string,
|
|
393
|
-
line: string,
|
|
394
|
-
start: number,
|
|
395
|
-
prefix: string,
|
|
396
|
-
generation: number,
|
|
397
|
-
): Promise<void> {
|
|
398
|
-
try {
|
|
399
|
-
const completions = await this.backend.completeWord(line, start, prefix.length);
|
|
400
|
-
if (generation !== this.#cacheGeneration || !this.#available) return;
|
|
401
|
-
const lowerPrefix = prefix.toLocaleLowerCase();
|
|
402
|
-
let suffix: string | null = null;
|
|
403
|
-
for (const completion of completions) {
|
|
404
|
-
if (completion.length > prefix.length && completion.toLocaleLowerCase().startsWith(lowerPrefix)) {
|
|
405
|
-
suffix = completion.slice(prefix.length);
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (this.#completionCache.size >= CACHE_LIMIT) this.#completionCache.clear();
|
|
410
|
-
this.#completionCache.set(key, suffix);
|
|
411
|
-
// A null suffix means no ghost text; the current paint is already right.
|
|
412
|
-
if (suffix !== null && this.#completionQueued === undefined) this.onUpdate?.();
|
|
413
|
-
} catch (error) {
|
|
414
|
-
this.#disable(error);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
#context(lines: readonly string[], line: number): SpellingDecorationContext {
|
|
419
|
-
return { editorText: lines.join("\n"), lines, line, startCol: 0 };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
#sourceRangeIsProse(context: SpellingDecorationContext, startCol: number, endCol: number): boolean {
|
|
423
|
-
if (context.editorText.length > MAX_SPELLING_BUFFER_LENGTH || startCol < 0 || endCol <= startCol) {
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
const line = context.lines[context.line];
|
|
427
|
-
if (line === undefined || line.length > MAX_SPELLING_LINE_LENGTH || endCol > line.length) return false;
|
|
428
|
-
this.#prepareSource(context);
|
|
429
|
-
const lineOffset = this.#sourceLineOffsets[context.line];
|
|
430
|
-
if (lineOffset === undefined) return false;
|
|
431
|
-
return this.#sourceMask.slice(lineOffset + startCol, lineOffset + endCol).trim().length > 0;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
#prepareSource(context: SpellingDecorationContext): void {
|
|
435
|
-
if (this.#sourceText === context.editorText) return;
|
|
436
|
-
this.#sourceText = context.editorText;
|
|
437
|
-
this.#sourceMask = maskNonProse(context.editorText);
|
|
438
|
-
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
439
|
-
this.#sourceLineOffsets = new Array<number>(context.lines.length);
|
|
440
|
-
let offset = 0;
|
|
441
|
-
for (let line = 0; line < context.lines.length; line++) {
|
|
442
|
-
this.#sourceLineOffsets[line] = offset;
|
|
443
|
-
offset += (context.lines[line]?.length ?? 0) + 1;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
308
|
#clearCaches(): void {
|
|
448
309
|
this.#cacheGeneration++;
|
|
449
310
|
this.#typoCache.clear();
|
|
450
311
|
this.#typoInFlight.clear();
|
|
451
312
|
this.#automaticTypoQueue.clear();
|
|
452
|
-
this.#completionCache.clear();
|
|
453
|
-
this.#completionQueued = undefined;
|
|
454
313
|
}
|
|
455
314
|
|
|
456
315
|
#disable(error: unknown): void {
|
|
@@ -135,6 +135,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
135
135
|
cursorLine: number,
|
|
136
136
|
cursorCol: number,
|
|
137
137
|
signal?: AbortSignal,
|
|
138
|
+
onPartial?: (suggestions: { items: AutocompleteItem[]; prefix: string }) => void,
|
|
138
139
|
): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
|
|
139
140
|
if (signal?.aborted) return null;
|
|
140
141
|
const currentLine = lines[cursorLine] || "";
|
|
@@ -150,7 +151,13 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
150
151
|
const commandName = commandText.slice(1, spaceIndex);
|
|
151
152
|
const command = this.#commands.find(cmd => cmd.name === commandName || cmd.aliases?.includes(commandName));
|
|
152
153
|
if (command && (!("allowArgs" in command) || command.allowArgs !== false)) {
|
|
153
|
-
const argumentSuggestions = await this.#baseProvider.getSuggestions(
|
|
154
|
+
const argumentSuggestions = await this.#baseProvider.getSuggestions(
|
|
155
|
+
lines,
|
|
156
|
+
cursorLine,
|
|
157
|
+
cursorCol,
|
|
158
|
+
signal,
|
|
159
|
+
onPartial,
|
|
160
|
+
);
|
|
154
161
|
if (argumentSuggestions) return argumentSuggestions;
|
|
155
162
|
const modelMentionSuggestions = getModelMentionSuggestions(textBeforeCursor, this.#modelMentions);
|
|
156
163
|
if (modelMentionSuggestions) return modelMentionSuggestions;
|
|
@@ -205,7 +212,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
205
212
|
if (emojiSuggestions) return emojiSuggestions;
|
|
206
213
|
}
|
|
207
214
|
|
|
208
|
-
return this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol, signal);
|
|
215
|
+
return this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol, signal, onPartial);
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
applyCompletion(
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { maskNonProse } from "./markdown-prose";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Client-side gates shared by the prose assistants (macOS typo/autocorrect and
|
|
5
|
+
* word completion): only plain prose words in modest buffers are ever sent to
|
|
6
|
+
* a spelling or prediction backend.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const CODEISH_CHARACTERS = "\\/@_=:{}[]<>";
|
|
10
|
+
const CAMEL_CASE = /\p{Ll}\p{Lu}/u;
|
|
11
|
+
/** Buffers above this many UTF-16 units get no prose assistance at all. */
|
|
12
|
+
const MAX_PROSE_BUFFER_LENGTH = 20_000;
|
|
13
|
+
/** Lines above this many UTF-16 units get no prose assistance at all. */
|
|
14
|
+
const MAX_PROSE_LINE_LENGTH = 1_000;
|
|
15
|
+
|
|
16
|
+
/** Logical source location for one rendered editor segment. */
|
|
17
|
+
export interface SpellingDecorationContext {
|
|
18
|
+
editorText: string;
|
|
19
|
+
lines: readonly string[];
|
|
20
|
+
line: number;
|
|
21
|
+
startCol: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tokenAt(text: string, start: number, end: number): string {
|
|
25
|
+
let tokenStart = start;
|
|
26
|
+
while (tokenStart > 0 && !/\s/.test(text[tokenStart - 1] ?? "")) tokenStart--;
|
|
27
|
+
let tokenEnd = end;
|
|
28
|
+
while (tokenEnd < text.length && !/\s/.test(text[tokenEnd] ?? "")) tokenEnd++;
|
|
29
|
+
return text.slice(tokenStart, tokenEnd);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether `text[start, end)` is a prose word: unmasked by {@link maskNonProse},
|
|
34
|
+
* not inside a path/identifier-like token, and not on a slash-command or
|
|
35
|
+
* queue-shorthand line.
|
|
36
|
+
*/
|
|
37
|
+
export function isProseWord(text: string, masked: string, start: number, end: number): boolean {
|
|
38
|
+
if (start < 0 || end <= start || end > text.length) return false;
|
|
39
|
+
if (masked.slice(start, end).trim().length === 0) return false;
|
|
40
|
+
const token = tokenAt(text, start, end);
|
|
41
|
+
for (const char of token) {
|
|
42
|
+
if (CODEISH_CHARACTERS.includes(char)) return false;
|
|
43
|
+
}
|
|
44
|
+
if (CAMEL_CASE.test(token) || /\d/.test(token)) return false;
|
|
45
|
+
return !text.trimStart().startsWith("/") && !text.startsWith("->") && !text.startsWith("=>");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whole-buffer context for `line`, starting at column 0. */
|
|
49
|
+
export function lineContext(lines: readonly string[], line: number): SpellingDecorationContext {
|
|
50
|
+
return { editorText: lines.join("\n"), lines, line, startCol: 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Memoized whole-buffer prose mask: answers whether a line range is prose in
|
|
55
|
+
* the context of the full editor text (fenced code spans lines).
|
|
56
|
+
*/
|
|
57
|
+
export class ProseSource {
|
|
58
|
+
#text = "";
|
|
59
|
+
#mask = "";
|
|
60
|
+
#lineOffsets: number[] = [];
|
|
61
|
+
|
|
62
|
+
/** True when `[startCol, endCol)` of `context.line` is prose and the buffer is within the caps. */
|
|
63
|
+
isProse(context: SpellingDecorationContext, startCol: number, endCol: number): boolean {
|
|
64
|
+
if (context.editorText.length > MAX_PROSE_BUFFER_LENGTH || startCol < 0 || endCol <= startCol) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const line = context.lines[context.line];
|
|
68
|
+
if (line === undefined || line.length > MAX_PROSE_LINE_LENGTH || endCol > line.length) return false;
|
|
69
|
+
this.#prepare(context);
|
|
70
|
+
const lineOffset = this.#lineOffsets[context.line];
|
|
71
|
+
if (lineOffset === undefined) return false;
|
|
72
|
+
return this.#mask.slice(lineOffset + startCol, lineOffset + endCol).trim().length > 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#prepare(context: SpellingDecorationContext): void {
|
|
76
|
+
if (this.#text === context.editorText) return;
|
|
77
|
+
this.#text = context.editorText;
|
|
78
|
+
this.#mask = maskNonProse(context.editorText);
|
|
79
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
80
|
+
this.#lineOffsets = new Array<number>(context.lines.length);
|
|
81
|
+
let offset = 0;
|
|
82
|
+
for (let line = 0; line < context.lines.length; line++) {
|
|
83
|
+
this.#lineOffsets[line] = offset;
|
|
84
|
+
offset += (context.lines[line]?.length ?? 0) + 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/prompt/tips.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Tired of typing "keep going"? Just send a '.'
|
|
2
2
|
You can /btw to ask a side question
|
|
3
3
|
Use /tan to fork the current conversation into a background agent
|
|
4
|
-
With default keybindings,
|
|
4
|
+
With default keybindings, {key:ctrl+d} deletes the character under the cursor instead of quitting your draft — see /hotkeys for your exit key
|
|
5
5
|
Find out which model you emotionally abuse the most with `omp stats`
|
|
6
6
|
Try task isolation to create CoW worktrees
|
|
7
7
|
Need a cheap nested model call? Use `completion(x...)`. Have a big batch of tasks? Ask clanker to use it!
|
|
@@ -13,17 +13,17 @@ Say `workflowz` in your message to drive the task with parallel subagents in eva
|
|
|
13
13
|
Bulk data to classify or triage? Say `jevify` in your message — the eval kernel's `judge()` processes the batch and you read only what it flags — watch it glow as you type
|
|
14
14
|
Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically
|
|
15
15
|
Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
|
|
16
|
-
Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc
|
|
17
|
-
Press ctrl+r to search your prompt history and reuse a past message
|
|
16
|
+
Press {key:alt+p} (or /switch) to switch provider, and {key:ctrl+p} to cycle role models smol -> slow -> etc
|
|
17
|
+
Press {key:ctrl+r} to search your prompt history and reuse a past message
|
|
18
18
|
`/force read` pins the next turn to one specific tool when the model keeps reaching for the wrong one
|
|
19
19
|
`/copy code` grabs the last code block to your clipboard — `/copy cmd` grabs the last shell/python command
|
|
20
20
|
`/shake` rips heavy tool results out of context to reclaim tokens without a full /compact — `/shake images` drops just images
|
|
21
21
|
Pair up live: `/collab` shares your session through an end-to-end encrypted relay link — a teammate runs `/join <link>` to watch tool calls stream and prompt the agent from their own omp
|
|
22
|
-
Press
|
|
22
|
+
Press {tap:left} to drill into a running or finished agent and inspect its tool calls and transcript
|
|
23
23
|
Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
|
|
24
24
|
No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
|
|
25
25
|
Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
|
|
26
26
|
Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
|
|
27
|
-
Press shift+tab to cycle through reasoning effort levels
|
|
27
|
+
Press {key:shift+tab} to cycle through reasoning effort levels
|
|
28
28
|
omp can reconfigure and extend itself — ask any questions you have!
|
|
29
|
-
Lint/type errors piling up? `omp cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents —
|
|
29
|
+
Lint/type errors piling up? `omp cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
|
package/src/prompt/welcome.ts
CHANGED
|
@@ -3,9 +3,13 @@ import type { Component } from "../tui";
|
|
|
3
3
|
import { padding, replaceTabs, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
|
|
4
4
|
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
|
|
5
5
|
import { theme } from "../theme/theme";
|
|
6
|
+
import { formatDoubleTap, formatKeyHint, formatKeyHints, type KeyName } from "../app-keybindings";
|
|
7
|
+
import { editorKey } from "../chrome/keybinding-hints";
|
|
8
|
+
import { getKeybindings, type Keybinding } from "../keybindings";
|
|
6
9
|
import tipsText from "./tips.txt" with { type: "text" };
|
|
7
10
|
|
|
8
|
-
/** Tips embedded at build time, one per line; blanks dropped.
|
|
11
|
+
/** Tips embedded at build time, one per line; blanks dropped. Key placeholders
|
|
12
|
+
* (see {@link expandTipKeys}) stay raw until render time. */
|
|
9
13
|
const TIPS: readonly string[] = tipsText
|
|
10
14
|
.split("\n")
|
|
11
15
|
.map(line => line.trim())
|
|
@@ -76,6 +80,35 @@ function renderNewTag(phase: number, encoding: ColorEncoding): string {
|
|
|
76
80
|
}
|
|
77
81
|
return out + reset;
|
|
78
82
|
}
|
|
83
|
+
|
|
84
|
+
/** Key placeholders in tips.txt: `{key:shift+tab}`, `{keys:up,down}`, `{tap:left}`, `{action:tui.editor.undo}`. */
|
|
85
|
+
const TIP_KEY_PLACEHOLDER = /\{(key|keys|tap|action):([^}]+)\}/g;
|
|
86
|
+
|
|
87
|
+
const MODIFIER_NAMES: Record<string, true | undefined> = { ctrl: true, shift: true, alt: true, super: true };
|
|
88
|
+
|
|
89
|
+
/** A `+`-joined chord whose leading parts are modifiers (`ctrl+o`, `shift`, `left`). */
|
|
90
|
+
function isKeyName(key: string): key is KeyName {
|
|
91
|
+
const parts = key.split("+");
|
|
92
|
+
return parts.every((part, i) => part.length > 0 && (i === parts.length - 1 || MODIFIER_NAMES[part] === true));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isKeybinding(action: string): action is Keybinding {
|
|
96
|
+
return action in getKeybindings().getResolvedBindings();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Expand tip key placeholders through the key formatter; malformed ones stay verbatim. */
|
|
100
|
+
function expandTipKeys(tip: string): string {
|
|
101
|
+
return tip.replace(TIP_KEY_PLACEHOLDER, (placeholder, kind: string, value: string) => {
|
|
102
|
+
if (kind === "action") return isKeybinding(value) ? editorKey(value) : placeholder;
|
|
103
|
+
const keys = value.split(",");
|
|
104
|
+
if (!keys.every(isKeyName)) return placeholder;
|
|
105
|
+
if (kind === "keys") return formatKeyHints(keys);
|
|
106
|
+
const [key] = keys;
|
|
107
|
+
if (key === undefined) return placeholder;
|
|
108
|
+
return kind === "tap" ? formatDoubleTap(key) : formatKeyHint(key);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
79
112
|
export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): string[] {
|
|
80
113
|
const label = "Tip: ";
|
|
81
114
|
const labelWidth = visibleWidth(label);
|
|
@@ -83,7 +116,7 @@ export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): stri
|
|
|
83
116
|
if (bodyBudget < 8) return [];
|
|
84
117
|
|
|
85
118
|
const isNew = NEW_TIP_MARKER.test(tip);
|
|
86
|
-
const body = isNew ? tip.replace(NEW_TIP_MARKER, "") : tip;
|
|
119
|
+
const body = expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip);
|
|
87
120
|
|
|
88
121
|
const wrappedBody = wrapTextWithAnsi(replaceTabs(body), bodyBudget);
|
|
89
122
|
if (wrappedBody.length === 0) return [];
|