@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
|
@@ -19,12 +19,15 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Keys: Up/Down step through rendered items in transcript order (within the
|
|
21
21
|
* active column when a strip is open), Left/Right slide between branch
|
|
22
|
-
* variants at a fork and jump between user turns elsewhere,
|
|
23
|
-
* the
|
|
22
|
+
* variants at a fork and jump between user turns elsewhere, `f` opens a
|
|
23
|
+
* filter (typing narrows the current path to matching items; Esc leaves the
|
|
24
|
+
* filter with the selection kept), Enter rewinds to the outlined item, Esc
|
|
25
|
+
* cancels.
|
|
24
26
|
*/
|
|
25
27
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
26
28
|
import {
|
|
27
29
|
type Component,
|
|
30
|
+
extractPrintableText,
|
|
28
31
|
matchesKey,
|
|
29
32
|
padding,
|
|
30
33
|
routeSgrMouseInput,
|
|
@@ -39,6 +42,9 @@ import { matchesAppToolsExpand, matchesSelectCancel, matchesSelectDown, matchesS
|
|
|
39
42
|
import { ChatTranscriptBuilder } from "../chat/chat-transcript-builder";
|
|
40
43
|
import { TranscriptBrowser, type TranscriptBrowserFrame } from "../chat/transcript-browser";
|
|
41
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";
|
|
42
48
|
import {
|
|
43
49
|
appendOutlineEntries,
|
|
44
50
|
type ComposedColumn,
|
|
@@ -110,6 +116,13 @@ export class RewindSelectorComponent implements Component {
|
|
|
110
116
|
#slide: { from: number; to: number; startedAt: number } | undefined;
|
|
111
117
|
#slideTimer: NodeJS.Timeout | undefined;
|
|
112
118
|
|
|
119
|
+
/** Filter query while the filter prompt is open; undefined shows the full transcript. */
|
|
120
|
+
#filter: string | undefined;
|
|
121
|
+
/** Main transcript rows from the last frame; the filter matches what is on screen. */
|
|
122
|
+
#mainRows: readonly (readonly string[])[] = [];
|
|
123
|
+
/** Lowercased plain text per rendered child row array (row arrays are cached, so identity is stable). */
|
|
124
|
+
#rowText = new WeakMap<readonly string[], string>();
|
|
125
|
+
|
|
113
126
|
constructor(
|
|
114
127
|
entries: TranscriptEntry[],
|
|
115
128
|
private readonly deps: RewindSelectorDeps,
|
|
@@ -235,19 +248,26 @@ export class RewindSelectorComponent implements Component {
|
|
|
235
248
|
});
|
|
236
249
|
return;
|
|
237
250
|
}
|
|
251
|
+
if (this.#filter !== undefined) {
|
|
252
|
+
this.#handleFilterInput(data);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
238
255
|
if (matchesSelectCancel(data) || matchesKey(data, "escape")) {
|
|
239
256
|
this.deps.onCancel();
|
|
240
257
|
return;
|
|
241
258
|
}
|
|
242
|
-
if (
|
|
243
|
-
this.#
|
|
244
|
-
this.#
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
259
|
+
if (matchesKey(data, "f")) {
|
|
260
|
+
this.#filter = "";
|
|
261
|
+
this.#activeVariant = 0;
|
|
262
|
+
this.#siblingSelected = 0;
|
|
263
|
+
this.#stopSlide();
|
|
248
264
|
this.deps.requestRender();
|
|
249
265
|
return;
|
|
250
266
|
}
|
|
267
|
+
if (matchesAppToolsExpand(data)) {
|
|
268
|
+
this.#toggleExpanded();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
251
271
|
if (matchesSelectUp(data)) {
|
|
252
272
|
this.#moveVertical(-1);
|
|
253
273
|
return;
|
|
@@ -282,6 +302,115 @@ export class RewindSelectorComponent implements Component {
|
|
|
282
302
|
}
|
|
283
303
|
}
|
|
284
304
|
|
|
305
|
+
#toggleExpanded(): void {
|
|
306
|
+
this.#expanded = !this.#expanded;
|
|
307
|
+
this.#builder.setExpanded(this.#expanded);
|
|
308
|
+
for (const columns of this.#variantCache.values()) {
|
|
309
|
+
for (const column of columns) column.builder.setExpanded(this.#expanded);
|
|
310
|
+
}
|
|
311
|
+
this.deps.requestRender();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ========================================================================
|
|
315
|
+
// Filter
|
|
316
|
+
// ========================================================================
|
|
317
|
+
|
|
318
|
+
#handleFilterInput(data: string): void {
|
|
319
|
+
if (matchesSelectCancel(data) || matchesKey(data, "escape")) {
|
|
320
|
+
this.#closeFilter();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
324
|
+
const target = this.#filterMatches().includes(this.#selected) ? this.#targets[this.#selected] : undefined;
|
|
325
|
+
if (target) this.deps.onSelect(target.entryId);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (matchesAppToolsExpand(data)) {
|
|
329
|
+
this.#toggleExpanded();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (matchesSelectUp(data) || matchesKey(data, "left")) {
|
|
333
|
+
const userTurnsOnly = !matchesSelectUp(data);
|
|
334
|
+
this.#stepFiltered(-1, userTurnsOnly);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (matchesSelectDown(data) || matchesKey(data, "right")) {
|
|
338
|
+
const userTurnsOnly = !matchesSelectDown(data);
|
|
339
|
+
this.#stepFiltered(1, userTurnsOnly);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (matchesKey(data, "backspace")) {
|
|
343
|
+
if (this.#filter!.length === 0) {
|
|
344
|
+
this.#closeFilter();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.#setFilter(this.#filter!.slice(0, -1));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const printable = extractPrintableText(data);
|
|
351
|
+
if (printable) {
|
|
352
|
+
this.#setFilter(this.#filter! + printable);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (this.#browser.handleScrollKey(data)) {
|
|
356
|
+
this.deps.requestRender();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Leave the filter, keeping the selected item outlined in the full transcript. */
|
|
361
|
+
#closeFilter(): void {
|
|
362
|
+
this.#filter = undefined;
|
|
363
|
+
this.deps.requestRender();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Update the query; keep the selection when it still matches, else rest on the newest match above it. */
|
|
367
|
+
#setFilter(query: string): void {
|
|
368
|
+
this.#filter = query;
|
|
369
|
+
const matches = this.#filterMatches();
|
|
370
|
+
if (!matches.includes(this.#selected)) {
|
|
371
|
+
this.#selected = matches.findLast(index => index < this.#selected) ?? matches.at(-1) ?? this.#selected;
|
|
372
|
+
}
|
|
373
|
+
this.deps.requestRender();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#stepFiltered(delta: -1 | 1, userTurnsOnly: boolean): void {
|
|
377
|
+
const matches = this.#filterMatches().filter(index => !userTurnsOnly || this.#targets[index]!.isUserTurn);
|
|
378
|
+
const next =
|
|
379
|
+
delta < 0 ? matches.findLast(index => index < this.#selected) : matches.find(index => index > this.#selected);
|
|
380
|
+
if (next === undefined) return;
|
|
381
|
+
this.#selected = next;
|
|
382
|
+
this.deps.requestRender();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Visible main-path target indices whose rendered text contains every
|
|
387
|
+
* whitespace-separated query word as a whole word (case-insensitive: `ls`
|
|
388
|
+
* matches `ls -la` but not `tools`); all visible targets for an empty query.
|
|
389
|
+
* Matching the rendered rows keeps results honest: collapsed tool output
|
|
390
|
+
* only matches once Ctrl+O expands it.
|
|
391
|
+
*/
|
|
392
|
+
#filterMatches(): number[] {
|
|
393
|
+
const words = (this.#filter ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
394
|
+
const patterns = words.map(
|
|
395
|
+
word => new RegExp(`(?<![\\p{L}\\p{N}_])${RegExp.escape(word)}(?![\\p{L}\\p{N}_])`, "u"),
|
|
396
|
+
);
|
|
397
|
+
const matches: number[] = [];
|
|
398
|
+
for (let index = 0; index < this.#targets.length; index++) {
|
|
399
|
+
if (!this.#isMainSelectable(index)) continue;
|
|
400
|
+
const target = this.#targets[index]!;
|
|
401
|
+
const texts = this.#mainRows.slice(target.start, target.end).map(rows => {
|
|
402
|
+
let text = this.#rowText.get(rows);
|
|
403
|
+
if (text === undefined) {
|
|
404
|
+
text = Bun.stripANSI(rows.join("\n")).toLowerCase();
|
|
405
|
+
this.#rowText.set(rows, text);
|
|
406
|
+
}
|
|
407
|
+
return text;
|
|
408
|
+
});
|
|
409
|
+
if (patterns.every(pattern => texts.some(text => pattern.test(text)))) matches.push(index);
|
|
410
|
+
}
|
|
411
|
+
return matches;
|
|
412
|
+
}
|
|
413
|
+
|
|
285
414
|
/** Up/Down: step within the active column; leaving a sibling column's top exits the strip. */
|
|
286
415
|
#moveVertical(delta: -1 | 1): void {
|
|
287
416
|
if (this.#activeVariant > 0) {
|
|
@@ -337,6 +466,7 @@ export class RewindSelectorComponent implements Component {
|
|
|
337
466
|
const children = this.#builder.container.children;
|
|
338
467
|
const prepared = this.#browser.prepareOutline(children, this.#targets, this.#selected, contentWidth);
|
|
339
468
|
this.#mainVisible = prepared.visible;
|
|
469
|
+
this.#mainRows = prepared.childRows;
|
|
340
470
|
if (prepared.selected !== this.#selected) {
|
|
341
471
|
// The current target collapsed (e.g. expansion toggle): rest on the
|
|
342
472
|
// nearest visible one and leave the strip.
|
|
@@ -345,6 +475,8 @@ export class RewindSelectorComponent implements Component {
|
|
|
345
475
|
this.#siblingSelected = 0;
|
|
346
476
|
}
|
|
347
477
|
|
|
478
|
+
if (this.#filter !== undefined) return this.#filterFrame(this.#filter, prepared.childRows, contentWidth);
|
|
479
|
+
|
|
348
480
|
const columns = this.#stripColumns();
|
|
349
481
|
const composed =
|
|
350
482
|
columns.length > 0
|
|
@@ -357,16 +489,64 @@ export class RewindSelectorComponent implements Component {
|
|
|
357
489
|
prepared,
|
|
358
490
|
}).column;
|
|
359
491
|
const position = this.#targets.length > 0 ? `${this.#selected + 1}/${this.#targets.length} ` : "";
|
|
360
|
-
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`;
|
|
361
496
|
return {
|
|
362
|
-
header: [
|
|
363
|
-
`${theme.icon.rewind} ${theme.bold("Rewind")}${theme.sep.dot}${theme.fg("dim", "pick the point to continue from")}`,
|
|
364
|
-
],
|
|
497
|
+
header: [this.#header()],
|
|
365
498
|
body: {
|
|
366
499
|
lines: composed.lines,
|
|
367
500
|
anchor: this.#outlineAnchor(composed),
|
|
368
501
|
},
|
|
369
|
-
footer: [theme.fg("dim", `${position}
|
|
502
|
+
footer: [theme.fg("dim", `${position}${keys}`)],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#header(): string {
|
|
507
|
+
return `${theme.icon.rewind} ${theme.bold("Rewind")}${theme.sep.dot}${theme.fg("dim", "pick the point to continue from")}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Only the matching items of the current path, concatenated; no branch strip. */
|
|
511
|
+
#filterFrame(
|
|
512
|
+
query: string,
|
|
513
|
+
childRows: readonly (readonly string[])[],
|
|
514
|
+
contentWidth: number,
|
|
515
|
+
): TranscriptBrowserFrame {
|
|
516
|
+
const matches = this.#filterMatches();
|
|
517
|
+
const rows: (readonly string[])[] = [];
|
|
518
|
+
const targets: OutlineTarget[] = [];
|
|
519
|
+
for (const index of matches) {
|
|
520
|
+
const target = this.#targets[index]!;
|
|
521
|
+
const start = rows.length;
|
|
522
|
+
rows.push(...childRows.slice(target.start, target.end));
|
|
523
|
+
targets.push({ ...target, start, end: rows.length });
|
|
524
|
+
}
|
|
525
|
+
const selected = matches.indexOf(this.#selected);
|
|
526
|
+
const composed = composeOutlineColumn(rows, 0, rows.length, targets, selected, contentWidth, undefined);
|
|
527
|
+
const lines = matches.length > 0 ? composed.lines : [theme.fg("muted", ` No items match "${query}"`)];
|
|
528
|
+
const count =
|
|
529
|
+
matches.length === 0
|
|
530
|
+
? theme.fg("error", "no matches")
|
|
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`;
|
|
534
|
+
return {
|
|
535
|
+
header: [this.#header()],
|
|
536
|
+
body: {
|
|
537
|
+
lines,
|
|
538
|
+
anchor:
|
|
539
|
+
composed.selStart >= 0
|
|
540
|
+
? {
|
|
541
|
+
id: `rewind:filter:${query}:${this.#targets[this.#selected]?.turnId ?? this.#selected}`,
|
|
542
|
+
start: composed.selStart,
|
|
543
|
+
end: composed.selEnd,
|
|
544
|
+
}
|
|
545
|
+
: undefined,
|
|
546
|
+
},
|
|
547
|
+
footer: [
|
|
548
|
+
`${theme.fg("accent", "filter:")} ${query}${theme.fg("accent", "▏")} ${count} ${theme.fg("dim", keys)}`,
|
|
549
|
+
],
|
|
370
550
|
};
|
|
371
551
|
}
|
|
372
552
|
|
|
@@ -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
|
|