@oh-my-pi/pi-tui 18.8.3 → 18.8.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 +11 -0
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/components/markdown.d.ts +11 -0
- package/dist/types/native/overlay.d.ts +5 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +2 -2
- package/dist/types/overlays/agent-transcript-viewer.d.ts +3 -1
- package/package.json +9 -9
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/git/help.ts +29 -20
- package/src/chat/assistant-message.ts +20 -5
- package/src/components/markdown.ts +114 -0
- package/src/native/overlay.ts +13 -0
- package/src/overlays/agent-hub-projection.ts +7 -1
- package/src/overlays/agent-hub.ts +29 -24
- package/src/overlays/agent-transcript-viewer.ts +25 -10
- package/src/overlays/annotation-overlay.ts +25 -3
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/plan-review-overlay.ts +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.4] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Agent Hub keeps existing agents in place while open; new agents appear first in the flat roster or within their tree sibling group ([#13066](https://github.com/can1357/oh-my-pi/pull/13066) by [@kmccleary3301](https://github.com/kmccleary3301))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed relative file links in Tern assistant replies opening against the folder omp was started in; they now open the file in the session's working directory ([#14879](https://github.com/can1357/oh-my-pi/pull/14879) by [@H4vC](https://github.com/H4vC))
|
|
14
|
+
- Fixed Tern modals with no clickable exit: BTW history, git shortcuts and autoresearch gain a Close button, plan review a Cancel button, the agent transcript viewer and `/annotate` review a top-right `esc` that runs Esc, and the `/move` dialog Accept, Cancel and Confirm buttons; new `escCloseButton()` builds the `esc` keycap button ([#14894](https://github.com/can1357/oh-my-pi/pull/14894) by [@H4vC](https://github.com/H4vC))
|
|
15
|
+
|
|
5
16
|
## [18.8.2] - 2026-10-07
|
|
6
17
|
|
|
7
18
|
### Removed
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NativeNode } from "../../native/node.js";
|
|
1
|
+
import type { NativeNode, NativeUiEvent } from "../../native/node.js";
|
|
2
2
|
import type { Component } from "../../tui.js";
|
|
3
3
|
/** The shortcuts overlay; any of `?`, escape or `q` closes it. */
|
|
4
4
|
export declare class GitHelpSheet implements Component {
|
|
@@ -10,6 +10,8 @@ export declare class GitHelpSheet implements Component {
|
|
|
10
10
|
};
|
|
11
11
|
constructor(close: () => void);
|
|
12
12
|
handleInput(data: string): void;
|
|
13
|
+
/** Close runs what Esc runs. */
|
|
14
|
+
handleNativeEvent(event: NativeUiEvent): void;
|
|
13
15
|
describe(): NativeNode;
|
|
14
16
|
render(width: number): readonly string[];
|
|
15
17
|
invalidate(): void;
|
|
@@ -33,6 +33,17 @@ export interface MarkdownLink {
|
|
|
33
33
|
* Duplicate hrefs are kept; callers decide how to fold them.
|
|
34
34
|
*/
|
|
35
35
|
export declare function extractMarkdownLinks(text: string): MarkdownLink[];
|
|
36
|
+
/**
|
|
37
|
+
* Rewrite the destinations of inline links and link reference definitions in
|
|
38
|
+
* `text` to `resolve(href)`, leaving everything else byte-for-byte intact.
|
|
39
|
+
* Hosts that render the Markdown source themselves (a native terminal's `md`
|
|
40
|
+
* node) resolve relative destinations against their own idea of the working
|
|
41
|
+
* directory; handing them the session-resolved target keeps links pointing at
|
|
42
|
+
* what the author meant. Links inside code, images, autolinks and reference
|
|
43
|
+
* uses (`[x][ref]`, rewritten through their definition) are untouched; a link
|
|
44
|
+
* whose source cannot be located exactly is left as written.
|
|
45
|
+
*/
|
|
46
|
+
export declare function rewriteMarkdownLinkDestinations(text: string, resolve: (href: string) => string | undefined): string;
|
|
36
47
|
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
37
48
|
export declare function clearRenderCache(): void;
|
|
38
49
|
/**
|
|
@@ -58,6 +58,11 @@ export interface ActionButtonOptions {
|
|
|
58
58
|
* which the component routes to the same code path as the key.
|
|
59
59
|
*/
|
|
60
60
|
export declare function actionButton(label: string, act: string, options?: ActionButtonOptions): NativeNode;
|
|
61
|
+
/**
|
|
62
|
+
* A bare `esc` keycap button for a view's top-right corner: a click sends
|
|
63
|
+
* `act` (default `close`), which the component routes to its Esc path.
|
|
64
|
+
*/
|
|
65
|
+
export declare function escCloseButton(act?: string, key?: string): NativeNode;
|
|
61
66
|
/** A row of {@link actionButton}s (role `omp.actions`); `null` entries become the spacer that end-aligns what follows. */
|
|
62
67
|
export declare function actionBar(buttons: readonly (NativeNode | null)[], key?: string): NativeNode;
|
|
63
68
|
/** Spans of an annotation callout: a dim `label:` then the note. */
|
|
@@ -37,6 +37,6 @@ export declare function aggregateMetrics<TRecord extends AgentRecordLike>(args:
|
|
|
37
37
|
metrics: AggregateMetrics;
|
|
38
38
|
hasFallbackLiveSessions: boolean;
|
|
39
39
|
};
|
|
40
|
-
/** Parent-before-child projection preserving
|
|
41
|
-
export declare function projectAgentTree<TRecord extends AgentRecordLike>(refs: readonly TRecord[]): AgentTreeProjection<TRecord>;
|
|
40
|
+
/** Parent-before-child projection preserving initial subtree rank and prepending new siblings. */
|
|
41
|
+
export declare function projectAgentTree<TRecord extends AgentRecordLike>(refs: readonly TRecord[], rosterRank: ReadonlyMap<TRecord, number> | undefined): AgentTreeProjection<TRecord>;
|
|
42
42
|
export {};
|
|
@@ -23,7 +23,7 @@ import type { AgentHubRegistry } from "./agent-hub-types.js";
|
|
|
23
23
|
import type { SessionMessageEntryLike } from "../chat/transcript-entry.js";
|
|
24
24
|
import type { SessionObserverRegistry } from "./session-observer-registry.js";
|
|
25
25
|
import type { AgentHubRemote } from "./agent-hub.js";
|
|
26
|
-
import type { DescribeContext, NativeNode } from "../native/node.js";
|
|
26
|
+
import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node.js";
|
|
27
27
|
/** Parsed message and model metadata relevant to a transcript viewer. */
|
|
28
28
|
export type AgentTranscriptEntry = SessionMessageEntryLike | {
|
|
29
29
|
type: "model_change";
|
|
@@ -77,6 +77,8 @@ export declare class AgentTranscriptViewer implements Component {
|
|
|
77
77
|
dispose(): void;
|
|
78
78
|
handleInput(data: string): void;
|
|
79
79
|
render(width: number): readonly string[];
|
|
80
|
+
/** The top-right `esc` runs Esc. */
|
|
81
|
+
handleNativeEvent(event: NativeUiEvent): void;
|
|
80
82
|
/**
|
|
81
83
|
* Header, transcript body (the builder's container, which describes its own
|
|
82
84
|
* blocks), notice, message editor, stats and key hints. Scrolling is the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.8.
|
|
4
|
+
"version": "18.8.4",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.8.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.8.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.8.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.8.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.8.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.8.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.8.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.8.
|
|
44
|
+
"@oh-my-pi/omptype": "18.8.4",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.8.4",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.8.4",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.8.4",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.8.4",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.8.4",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.8.4",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.8.4"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
import { formatNum, type ExperimentResult, type ExperimentState } from "../tools/autoresearch";
|
|
18
18
|
import type { TspSpan, TspTableColumn, TspText } from "@oh-my-pi/pi-wire";
|
|
19
19
|
import { card, col, elapsed, keyed, node, row, span, text } from "../native/describe";
|
|
20
|
-
import type { DescribeContext, NativeNode } from "../native/node";
|
|
21
|
-
import { hintsRow } from "../native/overlay";
|
|
20
|
+
import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
|
|
21
|
+
import { actionBar, actionButton, hintsRow } from "../native/overlay";
|
|
22
22
|
import { Memo } from "../native/memo";
|
|
23
23
|
import { isNativeRendering } from "../native/state";
|
|
24
24
|
|
|
@@ -190,9 +190,9 @@ export function createDashboardController(): DashboardController {
|
|
|
190
190
|
{ keys: ["up", "down", "j", "k"], label: "scroll" },
|
|
191
191
|
{ keys: ["pageUp", "pageDown"], label: "page" },
|
|
192
192
|
{ keys: ["g", "shift+g"], label: "top/bottom" },
|
|
193
|
-
{ keys: ["escape"], label: "close" },
|
|
194
193
|
]),
|
|
195
194
|
);
|
|
195
|
+
children.push(actionBar([null, actionButton("Close", "close", { keys: "escape" })]));
|
|
196
196
|
return col(children, { gap: "md", role: "omp.app.autoresearch" });
|
|
197
197
|
},
|
|
198
198
|
);
|
|
@@ -214,6 +214,10 @@ export function createDashboardController(): DashboardController {
|
|
|
214
214
|
scrollView.setHeight(viewportRows);
|
|
215
215
|
return [header, ...scrollView.render(width), renderOverlayFooter(width, theme)];
|
|
216
216
|
},
|
|
217
|
+
/** The Close button runs Esc's path. */
|
|
218
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
219
|
+
if (event.type === "action" && event.act === "close") done(undefined);
|
|
220
|
+
},
|
|
217
221
|
handleInput(data: string): void {
|
|
218
222
|
if (matchesKey(data, "escape") || matchesKey(data, "esc") || data === "q") {
|
|
219
223
|
done(undefined);
|
package/src/apps/git/help.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { formatKeyHint } from "../../app-keybindings";
|
|
|
11
11
|
import type { KeyName } from "../../key-hint-format";
|
|
12
12
|
import { matchesKey } from "../../keys";
|
|
13
13
|
import { col, node, span } from "../../native/describe";
|
|
14
|
-
import type { NativeNode } from "../../native/node";
|
|
14
|
+
import type { NativeNode, NativeUiEvent } from "../../native/node";
|
|
15
|
+
import { actionBar, actionButton } from "../../native/overlay";
|
|
15
16
|
import { theme } from "../../theme/theme";
|
|
16
17
|
import type { Component } from "../../tui";
|
|
17
18
|
import { truncateToWidth, visibleWidth } from "../../utils";
|
|
@@ -79,28 +80,36 @@ export class GitHelpSheet implements Component {
|
|
|
79
80
|
if (data === "?" || data === "q" || matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) this.#close();
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/** Close runs what Esc runs. */
|
|
84
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
85
|
+
if (event.type === "action" && event.act === "close") this.#close();
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
describe(): NativeNode {
|
|
83
89
|
this.#native ??= col(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
[
|
|
91
|
+
...GROUPS.map(group =>
|
|
92
|
+
node(
|
|
93
|
+
"section",
|
|
94
|
+
{ head: group.title, role: "omp.hotkeys.group" },
|
|
95
|
+
[
|
|
96
|
+
node("table", {
|
|
97
|
+
role: "omp.hotkeys.table",
|
|
98
|
+
cols: [
|
|
99
|
+
{ id: "keys", head: "Keys" },
|
|
100
|
+
{ id: "does", head: "Action", grow: 1 },
|
|
101
|
+
],
|
|
102
|
+
rows: group.rows.map((row, index) => ({
|
|
103
|
+
id: String(index),
|
|
104
|
+
cells: { keys: row.keys.map(key => span(formatKeyHint(key), "key")), does: row.label },
|
|
105
|
+
})),
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
group.title,
|
|
109
|
+
),
|
|
102
110
|
),
|
|
103
|
-
|
|
111
|
+
actionBar([null, actionButton("Close", "close", { keys: "escape" })]),
|
|
112
|
+
],
|
|
104
113
|
{ gap: "lg", role: "omp.app.git.help" },
|
|
105
114
|
);
|
|
106
115
|
return this.#native;
|
|
@@ -2,7 +2,7 @@ import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-a
|
|
|
2
2
|
import { type Component, Container } from "../tui";
|
|
3
3
|
import { Image, type ImageBudget } from "../components/image";
|
|
4
4
|
import { ImageProtocol, TERMINAL } from "../terminal-capabilities";
|
|
5
|
-
import { Markdown, type MarkdownTheme } from "../components/markdown";
|
|
5
|
+
import { Markdown, type MarkdownTheme, rewriteMarkdownLinkDestinations } from "../components/markdown";
|
|
6
6
|
import { Spacer } from "../components/spacer";
|
|
7
7
|
import { Text } from "../components/text";
|
|
8
8
|
import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
@@ -364,6 +364,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
364
364
|
#textColorTransform?: (text: string) => string;
|
|
365
365
|
#linkTargets: ReadonlyMap<string, string> = EMPTY_LINK_TARGETS;
|
|
366
366
|
#markdownTheme: MarkdownTheme | undefined;
|
|
367
|
+
/** Text-block sources with {@link #linkTargets} applied, for the native `md` nodes; reset with the targets. */
|
|
368
|
+
#nativeLinkSources = new Map<string, string>();
|
|
367
369
|
/** Block this reply reacts to; undefined when the preceding block takes no reactions. */
|
|
368
370
|
#reactionTarget: ReactionTarget | undefined;
|
|
369
371
|
/** Reaction lifted from the reply's opening emoji, once resolved. */
|
|
@@ -415,6 +417,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
415
417
|
this.#fastPathKey = undefined;
|
|
416
418
|
this.#fastPathItems = undefined;
|
|
417
419
|
for (const block of this.#figureBlocks.values()) block.restyle();
|
|
420
|
+
this.#nativeLinkSources.clear();
|
|
421
|
+
this.#nativeViewVersion++;
|
|
418
422
|
if (this.#lastMessage) {
|
|
419
423
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
420
424
|
}
|
|
@@ -842,8 +846,20 @@ export class AssistantMessageComponent extends Container {
|
|
|
842
846
|
const streaming = live && index === tailIndex;
|
|
843
847
|
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
844
848
|
const source = content.text.trim();
|
|
849
|
+
// The terminal renders this source itself and would resolve a relative
|
|
850
|
+
// link against its own idea of the cwd: hand it the session-resolved
|
|
851
|
+
// targets (resolved once the segment closes, so never while streaming).
|
|
852
|
+
const linked = (text: string, live: boolean): string => {
|
|
853
|
+
if (live || this.#linkTargets.size === 0) return text;
|
|
854
|
+
const targets = this.#linkTargets;
|
|
855
|
+
const resolved =
|
|
856
|
+
this.#nativeLinkSources.get(text) ??
|
|
857
|
+
rewriteMarkdownLinkDestinations(text, href => targets.get(href));
|
|
858
|
+
this.#nativeLinkSources.set(text, resolved);
|
|
859
|
+
return resolved;
|
|
860
|
+
};
|
|
845
861
|
if (!this.#showImages || !this.#showTableCharts || !hasChartTable(source)) {
|
|
846
|
-
children.push(markdown(`t${index}`, `t${index}`, source, streaming));
|
|
862
|
+
children.push(markdown(`t${index}`, `t${index}`, linked(source, streaming), streaming));
|
|
847
863
|
continue;
|
|
848
864
|
}
|
|
849
865
|
// A chart follows its table as an SVG image node; the prose splits around it.
|
|
@@ -851,9 +867,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
851
867
|
segments.forEach((segment, part) => {
|
|
852
868
|
const slot = part === 0 ? `t${index}` : `t${index}.${part}`;
|
|
853
869
|
if (segment.kind === "markdown") {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
);
|
|
870
|
+
const live = streaming && part === segments.length - 1;
|
|
871
|
+
children.push(markdown(slot, slot, linked(segment.text.trim(), live), live));
|
|
857
872
|
return;
|
|
858
873
|
}
|
|
859
874
|
const chart = lookupTableChart(segment.table);
|
|
@@ -1173,6 +1173,120 @@ export function extractMarkdownLinks(text: string): MarkdownLink[] {
|
|
|
1173
1173
|
return links;
|
|
1174
1174
|
}
|
|
1175
1175
|
|
|
1176
|
+
/**
|
|
1177
|
+
* Offset just past the destination that starts at or after `start` in `raw`
|
|
1178
|
+
* (CommonMark link destination grammar), plus where it begins. `null` when no
|
|
1179
|
+
* destination is there.
|
|
1180
|
+
*/
|
|
1181
|
+
function scanLinkDestination(raw: string, start: number): { start: number; end: number } | null {
|
|
1182
|
+
let i = start;
|
|
1183
|
+
while (i < raw.length && (raw[i] === " " || raw[i] === "\t" || raw[i] === "\n" || raw[i] === "\r")) i++;
|
|
1184
|
+
if (i >= raw.length) return null;
|
|
1185
|
+
if (raw[i] === "<") {
|
|
1186
|
+
for (let j = i + 1; j < raw.length; j++) {
|
|
1187
|
+
if (raw[j] === "\\") j++;
|
|
1188
|
+
else if (raw[j] === ">") return { start: i, end: j + 1 };
|
|
1189
|
+
else if (raw[j] === "<" || raw[j] === "\n") return null;
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
let depth = 0;
|
|
1194
|
+
let j = i;
|
|
1195
|
+
for (; j < raw.length; j++) {
|
|
1196
|
+
const ch = raw[j]!;
|
|
1197
|
+
if (ch === "\\") {
|
|
1198
|
+
j++;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch.charCodeAt(0) < 0x20) break;
|
|
1202
|
+
if (ch === "(") depth++;
|
|
1203
|
+
else if (ch === ")") {
|
|
1204
|
+
if (depth === 0) break;
|
|
1205
|
+
depth--;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return j > i ? { start: i, end: j } : null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** A destination that stays one destination whatever bytes the target holds. */
|
|
1212
|
+
function formatLinkDestination(target: string): string {
|
|
1213
|
+
return /[\s()<>\\]/.test(target) ? `<${target.replaceAll(/[<>\\]/g, ch => encodeURIComponent(ch))}>` : target;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function isListToken(token: Token): token is Tokens.List {
|
|
1217
|
+
return token.type === "list";
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function isTableToken(token: Token): token is Tokens.Table {
|
|
1221
|
+
return token.type === "table";
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Rewrite the destinations of inline links and link reference definitions in
|
|
1226
|
+
* `text` to `resolve(href)`, leaving everything else byte-for-byte intact.
|
|
1227
|
+
* Hosts that render the Markdown source themselves (a native terminal's `md`
|
|
1228
|
+
* node) resolve relative destinations against their own idea of the working
|
|
1229
|
+
* directory; handing them the session-resolved target keeps links pointing at
|
|
1230
|
+
* what the author meant. Links inside code, images, autolinks and reference
|
|
1231
|
+
* uses (`[x][ref]`, rewritten through their definition) are untouched; a link
|
|
1232
|
+
* whose source cannot be located exactly is left as written.
|
|
1233
|
+
*/
|
|
1234
|
+
export function rewriteMarkdownLinkDestinations(text: string, resolve: (href: string) => string | undefined): string {
|
|
1235
|
+
const edits: Array<{ start: number; end: number; target: string }> = [];
|
|
1236
|
+
let cursor = 0;
|
|
1237
|
+
const rewrite = (token: Token, labelEnd: number | undefined): void => {
|
|
1238
|
+
const at = text.indexOf(token.raw, cursor);
|
|
1239
|
+
if (at < 0) return;
|
|
1240
|
+
cursor = at + token.raw.length;
|
|
1241
|
+
const href = "href" in token && typeof token.href === "string" ? token.href : "";
|
|
1242
|
+
if (!href || labelEnd === undefined) return;
|
|
1243
|
+
const target = resolve(href);
|
|
1244
|
+
if (!target || target === href) return;
|
|
1245
|
+
const dest = scanLinkDestination(token.raw, labelEnd);
|
|
1246
|
+
if (dest) edits.push({ start: at + dest.start, end: at + dest.end, target });
|
|
1247
|
+
};
|
|
1248
|
+
const walk = (tokens: readonly Token[] | undefined): void => {
|
|
1249
|
+
if (!tokens) return;
|
|
1250
|
+
for (const token of tokens) {
|
|
1251
|
+
if (token.type === "link") {
|
|
1252
|
+
const label = "text" in token && typeof token.text === "string" ? `[${token.text}](` : undefined;
|
|
1253
|
+
rewrite(token, label && token.raw.startsWith(label) ? label.length : undefined);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (token.type === "def") {
|
|
1257
|
+
const close = token.raw.indexOf("]:");
|
|
1258
|
+
rewrite(token, close < 0 ? undefined : close + 2);
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const children = "tokens" in token && Array.isArray(token.tokens) ? token.tokens : undefined;
|
|
1262
|
+
const items = isListToken(token) ? token.items : undefined;
|
|
1263
|
+
const table = isTableToken(token) ? token : undefined;
|
|
1264
|
+
if (token.type === "image" || (!children && !items && !table)) {
|
|
1265
|
+
// A leaf (text, code span, fenced code, html, image…): step past it so a
|
|
1266
|
+
// later link's source is never matched inside it.
|
|
1267
|
+
const at = token.raw ? text.indexOf(token.raw, cursor) : -1;
|
|
1268
|
+
if (at >= 0) cursor = at + token.raw.length;
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
walk(children);
|
|
1272
|
+
walk(items);
|
|
1273
|
+
if (table) {
|
|
1274
|
+
for (const cell of table.header) walk(cell.tokens);
|
|
1275
|
+
for (const row of table.rows) for (const cell of row) walk(cell.tokens);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
walk(markdownParser.lexer(text));
|
|
1280
|
+
if (edits.length === 0) return text;
|
|
1281
|
+
let out = "";
|
|
1282
|
+
let last = 0;
|
|
1283
|
+
for (const edit of edits) {
|
|
1284
|
+
out += text.slice(last, edit.start) + formatLinkDestination(edit.target);
|
|
1285
|
+
last = edit.end;
|
|
1286
|
+
}
|
|
1287
|
+
return out + text.slice(last);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1176
1290
|
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
1177
1291
|
export function clearRenderCache(): void {
|
|
1178
1292
|
renderCache.clear();
|
package/src/native/overlay.ts
CHANGED
|
@@ -119,6 +119,19 @@ export function actionButton(label: string, act: string, options: ActionButtonOp
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A bare `esc` keycap button for a view's top-right corner: a click sends
|
|
124
|
+
* `act` (default `close`), which the component routes to its Esc path.
|
|
125
|
+
*/
|
|
126
|
+
export function escCloseButton(act = "close", key = "esc-close"): NativeNode {
|
|
127
|
+
return node(
|
|
128
|
+
"row",
|
|
129
|
+
{ role: "omp.btn", align: "center", actions: { click: act }, title: `Close ${formatTooltipKey("escape")}` },
|
|
130
|
+
[kbd("escape")],
|
|
131
|
+
key,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
/** A row of {@link actionButton}s (role `omp.actions`); `null` entries become the spacer that end-aligns what follows. */
|
|
123
136
|
export function actionBar(buttons: readonly (NativeNode | null)[], key = "actions"): NativeNode {
|
|
124
137
|
const children = buttons.map(button => button ?? node("spacer", { grow: 1 }));
|
|
@@ -251,9 +251,10 @@ export function aggregateMetrics<TRecord extends AgentRecordLike>(args: {
|
|
|
251
251
|
return { metrics: total, hasFallbackLiveSessions };
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/** Parent-before-child projection preserving
|
|
254
|
+
/** Parent-before-child projection preserving initial subtree rank and prepending new siblings. */
|
|
255
255
|
export function projectAgentTree<TRecord extends AgentRecordLike>(
|
|
256
256
|
refs: readonly TRecord[],
|
|
257
|
+
rosterRank: ReadonlyMap<TRecord, number> | undefined,
|
|
257
258
|
): AgentTreeProjection<TRecord> {
|
|
258
259
|
const ids = new Set<string>();
|
|
259
260
|
const operationalIndex = new Map<string, number>();
|
|
@@ -286,7 +287,12 @@ export function projectAgentTree<TRecord extends AgentRecordLike>(
|
|
|
286
287
|
if (!current) continue;
|
|
287
288
|
if (current.expanded) {
|
|
288
289
|
let order = operationalIndex.get(current.ref.id) ?? Number.MAX_SAFE_INTEGER;
|
|
290
|
+
const parentRank = rosterRank?.get(current.ref);
|
|
289
291
|
for (const child of children.get(current.ref.id) ?? []) {
|
|
292
|
+
const childRank = rosterRank?.get(child);
|
|
293
|
+
// A child spawned later cannot move an existing parent's group.
|
|
294
|
+
if (parentRank !== undefined && childRank !== undefined && childRank < 0 && childRank < parentRank)
|
|
295
|
+
continue;
|
|
290
296
|
order = Math.min(order, subtreeOrder.get(child.id) ?? Number.MAX_SAFE_INTEGER);
|
|
291
297
|
}
|
|
292
298
|
subtreeOrder.set(current.ref.id, order);
|
|
@@ -200,6 +200,12 @@ const DATA_CHANGE_RENDER_COALESCE_MS = 100;
|
|
|
200
200
|
/** Double-tap window for the table's left-left "close hub" gesture. */
|
|
201
201
|
const LEFT_TAP_WINDOW_MS = 500;
|
|
202
202
|
|
|
203
|
+
function compareRosterAgents(a: AgentRecordLike, b: AgentRecordLike): number {
|
|
204
|
+
return (
|
|
205
|
+
STATUS_ORDER[a.status] - STATUS_ORDER[b.status] || b.lastActivity - a.lastActivity || a.id.localeCompare(b.id)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
203
209
|
function activityGlyph(row: AgentActivityRow): string {
|
|
204
210
|
if (row.status === "error") return theme.fg("error", theme.status.error);
|
|
205
211
|
if (row.status === "aborted") return theme.fg("warning", theme.status.aborted);
|
|
@@ -358,11 +364,10 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
358
364
|
#rows: TRecord[] = [];
|
|
359
365
|
#statusCounts: Record<AgentStatus, number> = { running: 0, idle: 0, parked: 0, aborted: 0 };
|
|
360
366
|
#selectedRow = 0;
|
|
361
|
-
/**
|
|
362
|
-
*
|
|
363
|
-
* while the hub is open; newly appearing generations append at the end. */
|
|
367
|
+
/** Initial status/recency rank stays fixed while open; new generations
|
|
368
|
+
* prepend without changing the relative order of existing rows. */
|
|
364
369
|
#rowOrder: Map<TRecord, number> | undefined;
|
|
365
|
-
#
|
|
370
|
+
#nextNewRowOrder = 0;
|
|
366
371
|
#hoveredRow: number | null = null;
|
|
367
372
|
/** Per-render screen-line to agent-row map, shared by click and hover routing. */
|
|
368
373
|
#hitRows: Array<number | undefined> = [];
|
|
@@ -1524,35 +1529,35 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
1524
1529
|
const refs = this.#registry.list().filter(ref => ref.id !== MAIN_AGENT_ID);
|
|
1525
1530
|
this.#observedById = new Map();
|
|
1526
1531
|
for (const session of this.#observers.getSessions()) this.#observedById.set(session.id, session);
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
// Defer the capture until persisted-subagent discovery settles so a
|
|
1531
|
-
// mid-scan refresh cannot freeze a partial roster (the remaining agents
|
|
1532
|
-
// would otherwise append in readdir order instead of being ranked).
|
|
1532
|
+
// Capture the status+recency ranking once so keyboard navigation does
|
|
1533
|
+
// not jump on heartbeats. Defer until persisted-subagent discovery
|
|
1534
|
+
// settles so a partial roster is not frozen in readdir order.
|
|
1533
1535
|
const rowOrder = this.#rowOrder;
|
|
1534
1536
|
let ordered: TRecord[];
|
|
1535
1537
|
if (!rowOrder) {
|
|
1536
|
-
ordered = refs.sort(
|
|
1537
|
-
(a, b) =>
|
|
1538
|
-
STATUS_ORDER[a.status] - STATUS_ORDER[b.status] ||
|
|
1539
|
-
b.lastActivity - a.lastActivity ||
|
|
1540
|
-
a.id.localeCompare(b.id),
|
|
1541
|
-
);
|
|
1538
|
+
ordered = refs.sort(compareRosterAgents);
|
|
1542
1539
|
if (!this.#loadingPersistedSubagents && ordered.length > 0) {
|
|
1543
|
-
|
|
1544
|
-
for (const ref of ordered)
|
|
1540
|
+
const rowOrder = new Map<TRecord, number>();
|
|
1541
|
+
for (const [index, ref] of ordered.entries()) rowOrder.set(ref, index);
|
|
1542
|
+
this.#rowOrder = rowOrder;
|
|
1545
1543
|
}
|
|
1546
1544
|
} else {
|
|
1547
1545
|
for (const rankedRef of rowOrder.keys()) {
|
|
1548
1546
|
if (!refs.includes(rankedRef)) rowOrder.delete(rankedRef);
|
|
1549
1547
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
for (const ref of
|
|
1554
|
-
if (!rowOrder.has(ref))
|
|
1548
|
+
// Each batch of newly appearing generations uses the initial sort,
|
|
1549
|
+
// then takes ranks above every agent already on screen.
|
|
1550
|
+
let newcomers: TRecord[] | undefined;
|
|
1551
|
+
for (const ref of refs) {
|
|
1552
|
+
if (!rowOrder.has(ref)) (newcomers ??= []).push(ref);
|
|
1553
|
+
}
|
|
1554
|
+
if (newcomers) {
|
|
1555
|
+
newcomers.sort(compareRosterAgents);
|
|
1556
|
+
for (let i = newcomers.length - 1; i >= 0; i--) {
|
|
1557
|
+
rowOrder.set(newcomers[i]!, --this.#nextNewRowOrder);
|
|
1558
|
+
}
|
|
1555
1559
|
}
|
|
1560
|
+
ordered = refs.sort((a, b) => rowOrder.get(a)! - rowOrder.get(b)!);
|
|
1556
1561
|
}
|
|
1557
1562
|
const query = this.#agentFilter.getValue().trim();
|
|
1558
1563
|
const rosterRows =
|
|
@@ -1561,7 +1566,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
1561
1566
|
: ordered;
|
|
1562
1567
|
|
|
1563
1568
|
if (this.#viewMode === "tree") {
|
|
1564
|
-
const tree = projectAgentTree(rosterRows);
|
|
1569
|
+
const tree = projectAgentTree(rosterRows, rowOrder);
|
|
1565
1570
|
this.#rows = tree.rows;
|
|
1566
1571
|
this.#treeDepthById = tree.depthById;
|
|
1567
1572
|
this.#treeParentById = tree.parentById;
|
|
@@ -41,8 +41,8 @@ import { sanitizeErrorLine } from "../chrome/error-block";
|
|
|
41
41
|
import type { ScrollRangeAnchor } from "../components/scroll-view";
|
|
42
42
|
import { formatContextUsage } from "../chrome/context-thresholds";
|
|
43
43
|
import { node, span, text } from "../native/describe";
|
|
44
|
-
import type { DescribeContext, NativeChild, NativeNode } from "../native/node";
|
|
45
|
-
import { actionHint, hintsRow, overlayCard } from "../native/overlay";
|
|
44
|
+
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
45
|
+
import { actionHint, escCloseButton, hintsRow, overlayCard } from "../native/overlay";
|
|
46
46
|
|
|
47
47
|
/** Parsed message and model metadata relevant to a transcript viewer. */
|
|
48
48
|
export type AgentTranscriptEntry = SessionMessageEntryLike | { type: "model_change"; model: string };
|
|
@@ -559,12 +559,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
if (matchesKey(data, "escape")) {
|
|
562
|
-
|
|
563
|
-
this.#editor.setText("");
|
|
564
|
-
this.#deps.requestRender();
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
this.#deps.onClose();
|
|
562
|
+
this.#escape();
|
|
568
563
|
return;
|
|
569
564
|
}
|
|
570
565
|
|
|
@@ -647,6 +642,21 @@ export class AgentTranscriptViewer implements Component {
|
|
|
647
642
|
return lines;
|
|
648
643
|
}
|
|
649
644
|
|
|
645
|
+
/** The top-right `esc` runs Esc. */
|
|
646
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
647
|
+
if (event.type === "action" && event.act === "close") this.#escape();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Esc: clear a non-empty draft first, else close the viewer. */
|
|
651
|
+
#escape(): void {
|
|
652
|
+
if (this.#editor && this.#editor.getText().trim() !== "") {
|
|
653
|
+
this.#editor.setText("");
|
|
654
|
+
this.#deps.requestRender();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
this.#deps.onClose();
|
|
658
|
+
}
|
|
659
|
+
|
|
650
660
|
/**
|
|
651
661
|
* Header, transcript body (the builder's container, which describes its own
|
|
652
662
|
* blocks), notice, message editor, stats and key hints. Scrolling is the
|
|
@@ -675,6 +685,8 @@ export class AgentTranscriptViewer implements Component {
|
|
|
675
685
|
|
|
676
686
|
const id = this.#deps.agentId;
|
|
677
687
|
const children: NativeChild[] = [];
|
|
688
|
+
// Top row: the agent's meta on the left, a clickable `esc` (close) on the right.
|
|
689
|
+
const top: NativeChild[] = [];
|
|
678
690
|
if (ref) {
|
|
679
691
|
const kindTag = ref.parentId ? `${ref.kind} ${theme.sep.dot} of ${ref.parentId}` : ref.kind;
|
|
680
692
|
const meta: NativeChild[] = [
|
|
@@ -683,8 +695,12 @@ export class AgentTranscriptViewer implements Component {
|
|
|
683
695
|
text([span(kindTag, "dim")], { truncate: "end" }),
|
|
684
696
|
];
|
|
685
697
|
if (this.#model) meta.push(text([span(this.#model, "muted")], { truncate: "end" }));
|
|
686
|
-
|
|
698
|
+
top.push(node("row", { gap: "sm", align: "center", grow: 1, min: { w: 0 } }, meta, "meta"));
|
|
699
|
+
} else {
|
|
700
|
+
top.push(node("spacer", { grow: 1 }));
|
|
687
701
|
}
|
|
702
|
+
top.push(escCloseButton());
|
|
703
|
+
children.push(node("row", { gap: "md", align: "center" }, top, "top"));
|
|
688
704
|
children.push(
|
|
689
705
|
placeholder === undefined
|
|
690
706
|
? node("col", { grow: 1 }, [this.#builder.container], "transcript")
|
|
@@ -747,7 +763,6 @@ export class AgentTranscriptViewer implements Component {
|
|
|
747
763
|
children.push(
|
|
748
764
|
hintsRow([
|
|
749
765
|
this.#editor ? actionHint("tui.input.submit", "send") : undefined,
|
|
750
|
-
{ keys: ["escape"], label: "close" },
|
|
751
766
|
{ keys: [this.#deps.expandKeys[0] ?? "ctrl+o"], label: "expand" },
|
|
752
767
|
]),
|
|
753
768
|
);
|
|
@@ -42,7 +42,15 @@ import { matchesAppExternalEditor } from "../keybinding-matchers";
|
|
|
42
42
|
import type { KeyName } from "../key-hint-format";
|
|
43
43
|
import { item, keyed, node, row as rowNode, span, text } from "../native/describe";
|
|
44
44
|
import { leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
escCloseButton,
|
|
47
|
+
hintsRow,
|
|
48
|
+
itemIndex,
|
|
49
|
+
type NativeHint,
|
|
50
|
+
noteSpans,
|
|
51
|
+
overlayCard,
|
|
52
|
+
selectList,
|
|
53
|
+
} from "../native/overlay";
|
|
46
54
|
|
|
47
55
|
function fit(text: string, width: number): string {
|
|
48
56
|
if (width <= 0) return "";
|
|
@@ -317,7 +325,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
317
325
|
return;
|
|
318
326
|
}
|
|
319
327
|
if (this.#keybindings.matches(data, "tui.select.cancel")) {
|
|
320
|
-
this.#
|
|
328
|
+
this.#escape();
|
|
321
329
|
return;
|
|
322
330
|
}
|
|
323
331
|
if (data === "A" && (this.#focus === "files" || this.#focus === "diff")) {
|
|
@@ -369,6 +377,13 @@ export class AnnotationOverlay implements Focusable {
|
|
|
369
377
|
}
|
|
370
378
|
}
|
|
371
379
|
|
|
380
|
+
/** Esc, by state: leave the note chooser, drop the note draft, else close the review. */
|
|
381
|
+
#escape(): void {
|
|
382
|
+
if (this.#annotationChooser) this.#annotationChooser = undefined;
|
|
383
|
+
else if (this.#annotating) this.#cancelAnnotation();
|
|
384
|
+
else this.#finish(undefined);
|
|
385
|
+
}
|
|
386
|
+
|
|
372
387
|
#cycleFocus(direction: number): void {
|
|
373
388
|
const regions: FocusRegion[] = this.#sidebarShown ? ["files", "diff", "actions"] : ["diff", "actions"];
|
|
374
389
|
const current = regions.indexOf(this.#focus);
|
|
@@ -1263,7 +1278,10 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1263
1278
|
role: this.#textSource ? "omp.overlay.annotateText.lines" : "omp.overlay.codeReview.diff",
|
|
1264
1279
|
tone: this.#focus === "diff" ? "accent" : undefined,
|
|
1265
1280
|
});
|
|
1266
|
-
|
|
1281
|
+
// The header row ends in a clickable `esc` at the top right.
|
|
1282
|
+
const title = node("col", { grow: 1, min: { w: 0 } }, [this.#describeHeader()], "title");
|
|
1283
|
+
const head = keyed(rowNode([title, escCloseButton()], { gap: "md", align: "center" }), "head");
|
|
1284
|
+
const main = node("col", { grow: 1 }, [head, lines], "main");
|
|
1267
1285
|
if (this.#sidebarShown) {
|
|
1268
1286
|
const files = selectList(
|
|
1269
1287
|
"files",
|
|
@@ -1312,6 +1330,10 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1312
1330
|
|
|
1313
1331
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
1314
1332
|
if (this.#finished || this.#externalOperation) return;
|
|
1333
|
+
if (event.type === "action" && event.act === "close") {
|
|
1334
|
+
this.#escape();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1315
1337
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
1316
1338
|
const activate = event.type === "activate";
|
|
1317
1339
|
const leaf = leafKey(event.key);
|
|
@@ -37,7 +37,7 @@ import { clampSelection, contentRowWidth, padLinesToHeight, renderScrollableList
|
|
|
37
37
|
import type { TspScrollBy, TspSpan } from "@oh-my-pi/pi-wire";
|
|
38
38
|
import type { NativeChild, NativeNode, NativeScroll, NativeUiEvent } from "../native/node";
|
|
39
39
|
import { col, md, node, span, text } from "../native/describe";
|
|
40
|
-
import { actionHint, hintsRow, type NativeHint, statusHintsRow } from "../native/overlay";
|
|
40
|
+
import { actionBar, actionButton, actionHint, hintsRow, type NativeHint, statusHintsRow } from "../native/overlay";
|
|
41
41
|
|
|
42
42
|
interface BtwHistoryPanelOptions {
|
|
43
43
|
records: readonly BtwHistoryRecord[];
|
|
@@ -485,6 +485,13 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
485
485
|
children.push(node("col", undefined, lines, "composer"));
|
|
486
486
|
}
|
|
487
487
|
children.push({ ...this.#describeFooter(record, canFollowUp, copied), key: "footer" });
|
|
488
|
+
// Clicking Close always dismisses the sheet; the Esc keycap only shows where Esc does too.
|
|
489
|
+
const running = record !== undefined && getBtwLatestTurn(record).status === "running";
|
|
490
|
+
const escapeCloses = !composer && !(running && !this.#options.escapeHides);
|
|
491
|
+
children.push({
|
|
492
|
+
...actionBar([null, actionButton("Close", "close", escapeCloses ? { keys: "escape" } : undefined)]),
|
|
493
|
+
key: "actions",
|
|
494
|
+
});
|
|
488
495
|
// The sheet is the frame (`nativeOverlay`): a borderless column.
|
|
489
496
|
const described = col(children, { gap: "md" });
|
|
490
497
|
this.#native = {
|
|
@@ -504,6 +511,10 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
504
511
|
}
|
|
505
512
|
|
|
506
513
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
514
|
+
if (event.type === "action" && event.act === "close") {
|
|
515
|
+
this.#options.onClose();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
507
518
|
if ((event.type !== "select" && event.type !== "activate") || this.#composer) return;
|
|
508
519
|
const index = this.#records.findIndex(record => record.id === event.item);
|
|
509
520
|
if (index === -1) return;
|
|
@@ -15,7 +15,8 @@ import { formatKeyHint } from "../app-keybindings";
|
|
|
15
15
|
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
16
16
|
import type { NativeNode, NativeUiEvent } from "../native/node";
|
|
17
17
|
import { node, span } from "../native/describe";
|
|
18
|
-
import { actionHint, overlayCard, statusHintsRow } from "../native/overlay";
|
|
18
|
+
import { actionBar, actionButton, actionHint, overlayCard, statusHintsRow } from "../native/overlay";
|
|
19
|
+
import { getKeybindings } from "../keybindings";
|
|
19
20
|
|
|
20
21
|
export interface MoveOverlayResult {
|
|
21
22
|
directory: string;
|
|
@@ -122,12 +123,7 @@ export class MoveOverlay implements Component, Focusable {
|
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
if (matchesKey(data, Key.tab)) {
|
|
125
|
-
|
|
126
|
-
if (selected) {
|
|
127
|
-
this.#field.setValue(selected.value);
|
|
128
|
-
this.#selectedIndex = 0;
|
|
129
|
-
this.#updateResults();
|
|
130
|
-
}
|
|
126
|
+
this.#accept();
|
|
131
127
|
return;
|
|
132
128
|
}
|
|
133
129
|
const before = this.#field.getValue();
|
|
@@ -192,19 +188,28 @@ export class MoveOverlay implements Component, Focusable {
|
|
|
192
188
|
);
|
|
193
189
|
const hints = statusHintsRow(
|
|
194
190
|
[span("Type to filter", "dim")],
|
|
195
|
-
[
|
|
196
|
-
actionHint(["tui.select.up", "tui.select.down"], "navigate"),
|
|
197
|
-
{ keys: ["tab"], label: "accept" },
|
|
198
|
-
{ keys: ["enter"], label: "confirm" },
|
|
199
|
-
actionHint("tui.select.cancel", "cancel"),
|
|
200
|
-
],
|
|
191
|
+
[actionHint(["tui.select.up", "tui.select.down"], "navigate")],
|
|
201
192
|
);
|
|
202
|
-
|
|
193
|
+
// Accept, Confirm and Cancel run what Tab, Enter and Esc run.
|
|
194
|
+
const actions = actionBar([
|
|
195
|
+
actionButton("Accept", "accept", { keys: "tab" }),
|
|
196
|
+
null,
|
|
197
|
+
// The bound cancel key; Esc also cancels when it is unbound.
|
|
198
|
+
actionButton("Cancel", "cancel", { keys: getKeybindings().getKeys("tui.select.cancel")[0] ?? "escape" }),
|
|
199
|
+
actionButton("Confirm", "confirm", { keys: "enter", tone: "accent" }),
|
|
200
|
+
]);
|
|
201
|
+
const described = overlayCard("omp.dialog.move", "Move to directory", [this.#field, list, hints, actions]);
|
|
203
202
|
this.#native = { revision: this.#revision, node: described };
|
|
204
203
|
return described;
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
207
|
+
if (event.type === "action") {
|
|
208
|
+
if (event.act === "accept") this.#accept();
|
|
209
|
+
else if (event.act === "confirm") this.#field.submit();
|
|
210
|
+
else if (event.act === "cancel") this.#done(undefined);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
208
213
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
209
214
|
const index = this.#results.findIndex(result => result.value === event.item);
|
|
210
215
|
if (index === -1) return;
|
|
@@ -216,6 +221,15 @@ export class MoveOverlay implements Component, Focusable {
|
|
|
216
221
|
if (event.type === "activate") this.#field.submit();
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
/** Tab: fill the field with the highlighted suggestion. */
|
|
225
|
+
#accept(): void {
|
|
226
|
+
const selected = this.#results[this.#selectedIndex];
|
|
227
|
+
if (!selected) return;
|
|
228
|
+
this.#field.setValue(selected.value);
|
|
229
|
+
this.#selectedIndex = 0;
|
|
230
|
+
this.#updateResults();
|
|
231
|
+
}
|
|
232
|
+
|
|
219
233
|
invalidate(): void {
|
|
220
234
|
this.#field.input.prompt = theme.fg("dim", "Path: ");
|
|
221
235
|
this.#field.invalidate();
|
|
@@ -1607,7 +1607,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
1607
1607
|
return this.#nativeRoot;
|
|
1608
1608
|
}
|
|
1609
1609
|
|
|
1610
|
-
/** Head tools: Copy `c
|
|
1610
|
+
/** Head tools: Copy `c`, the external editor and Cancel, mirroring their keys. */
|
|
1611
1611
|
#describeTools(): NativeNode | undefined {
|
|
1612
1612
|
if (this.#committed) return undefined;
|
|
1613
1613
|
const buttons: NativeNode[] = [];
|
|
@@ -1618,6 +1618,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
1618
1618
|
if (this.callbacks.onExternalEditor) {
|
|
1619
1619
|
buttons.push(actionButton("Edit in $EDITOR", "externalEditor", editorKeyId ? { keys: editorKeyId } : {}));
|
|
1620
1620
|
}
|
|
1621
|
+
// Cancel the review the way the cancel key does; while annotating that key
|
|
1622
|
+
// only leaves the draft/chooser, so the button is hidden there.
|
|
1623
|
+
if (!this.#annotating && !this.#annotationChooser) {
|
|
1624
|
+
const cancelKeyId = getKeybindings().getKeys("tui.select.cancel")[0];
|
|
1625
|
+
buttons.push(actionButton("Cancel", "cancel", cancelKeyId ? { keys: cancelKeyId } : {}));
|
|
1626
|
+
}
|
|
1621
1627
|
if (buttons.length === 0) return undefined;
|
|
1622
1628
|
return node("row", { role: "omp.plan.tools", gap: "sm", align: "center", justify: "end" }, buttons, "tools");
|
|
1623
1629
|
}
|
|
@@ -1625,12 +1631,15 @@ export class PlanReviewOverlay implements Component {
|
|
|
1625
1631
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
1626
1632
|
if (this.#committed) return;
|
|
1627
1633
|
if (event.type === "action") {
|
|
1628
|
-
// Head tools run exactly what `c
|
|
1634
|
+
// Head tools run exactly what `c`, the external-editor key and Esc run.
|
|
1629
1635
|
if (this.#annotating || this.#annotationChooser) return;
|
|
1630
1636
|
if (event.act === "copyPlan" && this.callbacks.onCopyPlan) {
|
|
1631
1637
|
void this.callbacks.onCopyPlan(joinPlanSections(this.#sections));
|
|
1632
1638
|
} else if (event.act === "externalEditor") {
|
|
1633
1639
|
this.callbacks.onExternalEditor?.();
|
|
1640
|
+
} else if (event.act === "cancel") {
|
|
1641
|
+
this.#committed = true;
|
|
1642
|
+
this.callbacks.onCancel();
|
|
1634
1643
|
}
|
|
1635
1644
|
return;
|
|
1636
1645
|
}
|