@oh-my-pi/pi-tui 18.8.3 → 18.8.5
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/apps/git/help.d.ts +3 -1
- package/dist/types/chat/user-message.d.ts +1 -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/dist/types/overlays/model-browser.d.ts +13 -0
- package/dist/types/overlays/model-hub.d.ts +6 -0
- package/dist/types/overlays/usage-dashboard.d.ts +5 -3
- package/dist/types/prompt/custom-editor.d.ts +2 -2
- package/dist/types/prompt/image-source.d.ts +2 -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/chat/user-message.ts +1 -1
- package/src/components/editor.ts +6 -3
- 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/model-browser.ts +26 -1
- package/src/overlays/model-hub.ts +133 -17
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/plan-review-overlay.ts +11 -2
- package/src/overlays/usage-dashboard.ts +18 -7
- package/src/prompt/custom-editor.ts +3 -4
- package/src/prompt/image-source.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.5] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a **Compacts at** fact to the model hub preview and a Roles-view **Compaction limit** action (`k`) that edits the selected row's model limit through the new `ModelBrowserSource.compactionPointFor` and `ModelHubCallbacks.onCompactionPointChange` hooks ([#14952](https://github.com/can1357/oh-my-pi/pull/14952) by [@H4vC](https://github.com/H4vC))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed `/usage` dashboard cards reordering their limit rows by usage; rows now keep the provider's window order (e.g. 5 hour → weekly → monthly) ([#14953](https://github.com/can1357/oh-my-pi/pull/14953) by [@H4vC](https://github.com/H4vC))
|
|
14
|
+
- Fixed Cmd+A, Cmd+C, Cmd+X and Shift+arrow selection doing nothing in the Tern prompt while Vim mode is in Insert mode ([#14954](https://github.com/can1357/oh-my-pi/pull/14954) by [@H4vC](https://github.com/H4vC))
|
|
15
|
+
|
|
16
|
+
## [18.8.4] - 2026-10-08
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- 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))
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 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))
|
|
25
|
+
- 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))
|
|
26
|
+
|
|
5
27
|
## [18.8.2] - 2026-10-07
|
|
6
28
|
|
|
7
29
|
### 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;
|
|
@@ -4,7 +4,7 @@ import type { ReactionTarget } from "./reaction.js";
|
|
|
4
4
|
import { type DescribeContext, type NativeNode, type NativeUiEvent } from "../native/node.js";
|
|
5
5
|
/** How a user bubble styles its prose and chips (see {@link userBubbleColor}). */
|
|
6
6
|
export interface UserBubbleOptions {
|
|
7
|
-
/**
|
|
7
|
+
/** Filesystem paths for attached image chips, indexed by chip number. */
|
|
8
8
|
imageLinks?: readonly (string | undefined)[];
|
|
9
9
|
/** The message's attached images in chip order (`#1` first); a native bubble shows them. */
|
|
10
10
|
images?: readonly ImageContent[];
|
|
@@ -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
|
|
@@ -37,6 +37,17 @@ export interface ResolvedModelRoleValue {
|
|
|
37
37
|
explicitThinkingLevel: boolean;
|
|
38
38
|
warning?: string;
|
|
39
39
|
}
|
|
40
|
+
/** Where auto-compaction triggers for one model, as the model hub's preview shows it (edited from the Roles view). */
|
|
41
|
+
export interface ModelCompactionPoint {
|
|
42
|
+
/** Context tokens that trigger auto-compaction; undefined when auto-compaction is off or the window is unknown. */
|
|
43
|
+
tokens: number | undefined;
|
|
44
|
+
/** The configured percentage of the window, when the trigger is percent-based. */
|
|
45
|
+
percent?: number;
|
|
46
|
+
/** What sets it: the matching `compaction.modelThresholds` key, `global`, or `default`. */
|
|
47
|
+
source: string;
|
|
48
|
+
/** The model's own exact entry as editable text (`90k`, `80%`); absent when it has none. */
|
|
49
|
+
draft?: string;
|
|
50
|
+
}
|
|
40
51
|
/** Host-provided preferences and model-role resolution for the browser. */
|
|
41
52
|
export interface ModelBrowserSource extends ModelRoleLookup {
|
|
42
53
|
/**
|
|
@@ -58,6 +69,8 @@ export interface ModelBrowserSource extends ModelRoleLookup {
|
|
|
58
69
|
getRoleInfo(role: string): ModelBrowserRoleInfo;
|
|
59
70
|
defaultRoleChain(role: string): string[];
|
|
60
71
|
resolveRoleValue(value: string | undefined, models: Model[], roleLookup?: ModelRoleLookup): ResolvedModelRoleValue;
|
|
72
|
+
/** Auto-compaction point for `model`; absent hosts show no compaction row in the preview. */
|
|
73
|
+
compactionPointFor?(model: Model): ModelCompactionPoint | undefined;
|
|
61
74
|
}
|
|
62
75
|
/** Read-only catalog surface consumed by model browsers. */
|
|
63
76
|
export interface ModelBrowserRegistry {
|
|
@@ -64,6 +64,12 @@ export interface ModelHubCallbacks {
|
|
|
64
64
|
onSwitchPreset?: (name: string) => void | Promise<void>;
|
|
65
65
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
66
66
|
onCycleOrderChange?: (order: string[]) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Persist typed text as `model`'s own compaction point: a token count (`90000`,
|
|
69
|
+
* `90k`, `1M`), a percentage (`80%`), or empty to reset. Returns an error
|
|
70
|
+
* message for input it rejects, which keeps the field open.
|
|
71
|
+
*/
|
|
72
|
+
onCompactionPointChange?: (model: Model, input: string) => string | undefined;
|
|
67
73
|
onCancel: () => void;
|
|
68
74
|
}
|
|
69
75
|
export interface ModelHubOptions {
|
|
@@ -33,7 +33,7 @@ export interface ProviderCard {
|
|
|
33
33
|
/** Number of represented accounts, including unavailable usage lookups. */
|
|
34
34
|
accounts: number;
|
|
35
35
|
unavailableAccounts: string[];
|
|
36
|
-
/** Window rows
|
|
36
|
+
/** Window rows in provider-declared order (e.g. 5h → weekly → monthly); the fullest CARD_MAX_WINDOWS lead. */
|
|
37
37
|
windows: CardWindowRow[];
|
|
38
38
|
/** True when every account reports no limits (e.g. enterprise plans). */
|
|
39
39
|
unlimited: boolean;
|
|
@@ -52,8 +52,10 @@ export interface ProviderCard {
|
|
|
52
52
|
* Collapse usage reports into one compact card per provider: limits grouped by
|
|
53
53
|
* quota bucket (label + window), each bucket showing the mean used fraction
|
|
54
54
|
* across accounts (matching the classic report's aggregate "% free") with the
|
|
55
|
-
* most-used account's reset countdown.
|
|
56
|
-
*
|
|
55
|
+
* most-used account's reset countdown. Rows keep the provider's declared window
|
|
56
|
+
* order so 5h → week → month reads the same every time; cards sort by their
|
|
57
|
+
* fullest window so what's burning is on top-left; fully idle providers
|
|
58
|
+
* collapse into a tick.
|
|
57
59
|
*/
|
|
58
60
|
export declare function buildProviderCards(reports: UsageReport[], nowMs: number, unavailableAccounts?: readonly UnavailableUsageAccount[]): ProviderCard[];
|
|
59
61
|
/** GitHub-style week-per-column heatmap grid derived from daily activity. */
|
|
@@ -118,8 +118,8 @@ export declare class CustomEditor extends Editor {
|
|
|
118
118
|
* Numbered by a per-draft monotonic counter so a deleted chip never recycles its number
|
|
119
119
|
* (labels key the atom table). */
|
|
120
120
|
pendingTexts: TextAttachment[];
|
|
121
|
-
/** Host-wired producer of per-image
|
|
122
|
-
* chip tokens for restored drafts (esc-esc, `/tree`, branch). */
|
|
121
|
+
/** Host-wired producer of per-image chip targets (a file on disk, or a session blob copy);
|
|
122
|
+
* drives clickable chip tokens for restored drafts (esc-esc, `/tree`, branch). */
|
|
123
123
|
draftImageLinkMaterializer?: (images: readonly ImageContent[]) => Promise<(string | undefined)[] | undefined>;
|
|
124
124
|
/**
|
|
125
125
|
* The host {@link TUI}, captured when a plugin constructs this editor through
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* to the session's `local://` root, and generated video contact-sheet previews.
|
|
5
5
|
* Symbol metadata stays out of serialized/model-bound image data while traveling
|
|
6
6
|
* with the draft object, until AgentSession creates the hidden companion message
|
|
7
|
-
* that tells the model the path
|
|
7
|
+
* that tells the model the path. The model keeps `local://` references across
|
|
8
|
+
* `/move`; chips for internal URLs instead use a stable blob copy.
|
|
8
9
|
*/
|
|
9
10
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
11
|
/** How an image attachment's backing file entered the session. */
|
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.5",
|
|
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.5",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.8.5",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.8.5",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.8.5",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.8.5",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.8.5",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.8.5",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.8.5"
|
|
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);
|
package/src/chat/user-message.ts
CHANGED
|
@@ -57,7 +57,7 @@ const OSC133_ZONE_CLOSE = OSC133_ZONE_END + OSC133_COMMAND_START + OSC133_COMMAN
|
|
|
57
57
|
|
|
58
58
|
/** How a user bubble styles its prose and chips (see {@link userBubbleColor}). */
|
|
59
59
|
export interface UserBubbleOptions {
|
|
60
|
-
/**
|
|
60
|
+
/** Filesystem paths for attached image chips, indexed by chip number. */
|
|
61
61
|
imageLinks?: readonly (string | undefined)[];
|
|
62
62
|
/** The message's attached images in chip order (`#1` first); a native bubble shows them. */
|
|
63
63
|
images?: readonly ImageContent[];
|
package/src/components/editor.ts
CHANGED
|
@@ -74,9 +74,12 @@ export interface NativeEditorLayout {
|
|
|
74
74
|
readonly caret: string;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Vim mode as the TSP `editor.mode` label. Insert carries none: Insert edits like a plain field,
|
|
79
|
+
* and a host such as Tern stops native editing (selection, ⌘A/⌘C/⌘X) for any labelled mode.
|
|
80
|
+
*/
|
|
81
|
+
const VIM_MODE_LABELS: Record<VimMode, string | undefined> = {
|
|
82
|
+
insert: undefined,
|
|
80
83
|
normal: "NORMAL",
|
|
81
84
|
visual: "VISUAL",
|
|
82
85
|
"visual-line": "VISUAL",
|
|
@@ -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);
|