@oh-my-pi/pi-tui 18.10.2 → 18.10.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 +28 -43
- package/dist/types/app-keybindings.d.ts +5 -0
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/keybinding-matchers.d.ts +4 -0
- package/dist/types/overlays/approval-dialog.d.ts +51 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +17 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/read.d.ts +7 -0
- package/package.json +9 -9
- package/src/app-keybindings.ts +7 -0
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/chat/read-tool-group.ts +73 -31
- package/src/hotkeys-markdown.ts +1 -0
- package/src/keybinding-matchers.ts +14 -0
- package/src/overlays/approval-dialog.ts +230 -0
- package/src/overlays/btw-history-panel.ts +2 -2
- package/src/overlays/model-browser.ts +9 -6
- package/src/overlays/model-hub.ts +1 -0
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/thinking.ts +9 -1
- package/src/tmux.ts +32 -0
- package/src/tools/bash.ts +6 -1
- package/src/tools/read.ts +21 -14
- package/src/tui.ts +5 -0
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.10.4] - 2026-10-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `ApprovalDialogComponent` and the `app.approval.approve` keybinding (default `Ctrl+Enter`, `Ctrl+Q`) ([#15548](https://github.com/can1357/oh-my-pi/pull/15548) by [@H4vC](https://github.com/H4vC))
|
|
10
|
+
- Plan review overlay can take a remote answer: it shows the remotely chosen execution tier and locks further input like a terminal approval ([#15552](https://github.com/can1357/oh-my-pi/pull/15552) by [@Shadorain](https://github.com/Shadorain))
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The model browser shows the price a model is billed at now, not the one current when its row was loaded: DeepSeek V4 Pro shows its $0.30 / $1.20 card in effect since 2026-09-14 instead of $1.32 / $3.96, and Gemini 4 Argon switches to $4 / $20 on 2026-12-01 ([#15620](https://github.com/can1357/oh-my-pi/pull/15620) by [@eggpeat](https://github.com/eggpeat))
|
|
15
|
+
- Clicking Copy answer in the `/btw` history (and Copy raw in the raw SSE viewer) on native terminals copies the answer instead of the button's label ([#15547](https://github.com/can1357/oh-my-pi/pull/15547) by [@H4vC](https://github.com/H4vC))
|
|
16
|
+
- Fixed image previews inside tmux showing as `SIXEL IMAGE (WxH)` placeholder text when the outer terminal lacks SIXEL; SIXEL is now enabled from tmux's DA1 reply only when tmux's terminal description for the attached client has `Sxl` ([#15513](https://github.com/can1357/oh-my-pi/issues/15513))
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- Removed `tools/eval-format/code-mode-declarations` (`generateCodeModeDeclarations`), which only served Codex Code Mode.
|
|
21
|
+
|
|
22
|
+
## [18.10.3] - 2026-10-12
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Updated model picker to offer manual effort selection for unverified models instead of automatic mode
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed the `snapcompact.shape` settings preview labelling variants `5x8-sent` and repeating the first variant's sample when the frame edge differs from the variant table (Claude and GPT on Cursor; `auto` on high-resolution Claude) ([#15448](https://github.com/can1357/oh-my-pi/pull/15448) by [@will-bogusz](https://github.com/will-bogusz))
|
|
31
|
+
|
|
5
32
|
## [18.10.2] - 2026-10-12
|
|
6
33
|
|
|
7
34
|
### Added
|
|
@@ -2730,46 +2757,4 @@
|
|
|
2730
2757
|
- Migrated key parsing and matching logic to native implementation for improved performance
|
|
2731
2758
|
- Simplified `isKeyRelease()` and `isKeyRepeat()` to use regex pattern matching instead of string inclusion checks
|
|
2732
2759
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
### Changed
|
|
2736
|
-
|
|
2737
|
-
- Renamed `EllipsisKind` enum to `Ellipsis` for clearer API naming
|
|
2738
|
-
- Changed hardcoded ellipsis character from theme-configurable to literal "…" in editor truncation
|
|
2739
|
-
- Refactored `visibleWidth` function to use caching wrapper around new `visibleWidthRaw` implementation for improved performance
|
|
2740
|
-
|
|
2741
|
-
### Removed
|
|
2742
|
-
|
|
2743
|
-
- Removed `truncateToWidth`, `sliceWithWidth`, and `extractSegments` functions from public API (now re-exported directly from @oh-my-pi/pi-natives)
|
|
2744
|
-
- Removed `ellipsis` property from `SymbolTheme` interface
|
|
2745
|
-
- Removed `extractAnsiCode` function from public API
|
|
2746
|
-
|
|
2747
|
-
## [9.6.1] - 2026-02-01
|
|
2748
|
-
|
|
2749
|
-
### Changed
|
|
2750
|
-
|
|
2751
|
-
- Improved performance of key ID parsing with optimized cache lookup strategy
|
|
2752
|
-
- Simplified `visibleWidth` calculation to use consistent Bun.stringWidth approach for all string lengths
|
|
2753
|
-
|
|
2754
|
-
### Removed
|
|
2755
|
-
|
|
2756
|
-
- Removed `visibleWidth` benchmark file in favor of Kitty sequence benchmarking
|
|
2757
|
-
|
|
2758
|
-
## [9.5.0] - 2026-02-01
|
|
2759
|
-
|
|
2760
|
-
### Changed
|
|
2761
|
-
|
|
2762
|
-
- Improved fuzzy file search performance by using native implementation instead of spawning external process
|
|
2763
|
-
- Replaced external `fd` binary with native fuzzy path search for `@`-prefixed autocomplete
|
|
2764
|
-
|
|
2765
|
-
## [9.4.0] - 2026-01-31
|
|
2766
|
-
|
|
2767
|
-
### Added
|
|
2768
|
-
|
|
2769
|
-
- Exported `padding` utility function for creating space-padded strings efficiently
|
|
2770
|
-
|
|
2771
|
-
### Changed
|
|
2772
|
-
|
|
2773
|
-
- Optimized padding operations across all components to use pre-allocated space buffer for better performance
|
|
2774
|
-
|
|
2775
|
-
Older entries are archived in [packages/tui/CHANGELOG.md@e19bb3cb2344](https://github.com/can1357/oh-my-pi/blob/e19bb3cb2344a8953dc04aa398b810230f0e24c3/packages/tui/CHANGELOG.md).
|
|
2760
|
+
Older entries are archived in [packages/tui/CHANGELOG.md@e6b8074bbe4b](https://github.com/can1357/oh-my-pi/blob/e6b8074bbe4b0d459aa812948822c2a634c8e62d/packages/tui/CHANGELOG.md).
|
|
@@ -18,6 +18,7 @@ interface AppKeybindings {
|
|
|
18
18
|
"app.tools.expand": true;
|
|
19
19
|
"app.tools.toggleVisibility": true;
|
|
20
20
|
"app.ask.hide": true;
|
|
21
|
+
"app.approval.approve": true;
|
|
21
22
|
"app.editor.external": true;
|
|
22
23
|
"app.message.followUp": true;
|
|
23
24
|
"app.retry": true;
|
|
@@ -243,6 +244,10 @@ export declare const KEYBINDINGS: {
|
|
|
243
244
|
readonly defaultKeys: "alt+h";
|
|
244
245
|
readonly description: "Hide or show a pending ask dialog";
|
|
245
246
|
};
|
|
247
|
+
readonly "app.approval.approve": {
|
|
248
|
+
readonly defaultKeys: ["ctrl+enter", "ctrl+q"];
|
|
249
|
+
readonly description: "Approve a pending tool call";
|
|
250
|
+
};
|
|
246
251
|
readonly "app.editor.external": {
|
|
247
252
|
readonly defaultKeys: "ctrl+g";
|
|
248
253
|
readonly description: "Open external editor";
|
|
@@ -74,8 +74,8 @@ export declare class ReadToolGroupComponent extends Container implements ToolExe
|
|
|
74
74
|
* The read run as one inline `tool` on terminals that list the kind, else
|
|
75
75
|
* the fallback: a bare `card` (role `omp.tool.read`) with `Read <path>` or
|
|
76
76
|
* `Read (N)` in the head, a `list` of path items (status tone, link,
|
|
77
|
-
* correction and conflict detail, nested usage) and
|
|
78
|
-
*
|
|
77
|
+
* correction and conflict detail, nested usage) and a clamped preview per
|
|
78
|
+
* read when content previews are on.
|
|
79
79
|
*/
|
|
80
80
|
describe(cx?: DescribeContext): NativeNode;
|
|
81
81
|
}
|
|
@@ -36,3 +36,7 @@ export declare function matchesAppExternalEditor(data: string): boolean;
|
|
|
36
36
|
* effective follow-up binding.
|
|
37
37
|
*/
|
|
38
38
|
export declare function matchesAppFollowUp(data: string): boolean;
|
|
39
|
+
/** Effective tool-approval keys: the `app.approval.approve` bindings, or Ctrl+Enter/Ctrl+Q when unbound. */
|
|
40
|
+
export declare function appApproveKeys(): readonly KeyId[];
|
|
41
|
+
/** Match the tool-approval key, recognizing modifier-tagged LF as Ctrl+Enter like {@link matchesAppFollowUp}. */
|
|
42
|
+
export declare function matchesAppApprove(data: string): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-approval prompt in the composer's place, presented like the ask
|
|
3
|
+
* dialog: the approval title and its details over an action bar. Approving
|
|
4
|
+
* takes a deliberate chord (`app.approval.approve`, Ctrl+Enter by default),
|
|
5
|
+
* so an Enter typed for the composer as the prompt appears never approves a
|
|
6
|
+
* call, and the chord itself only arms {@link APPROVE_ARM_MS} after mount: it
|
|
7
|
+
* is also the follow-up key, pressed mid-run exactly when prompts appear.
|
|
8
|
+
* Esc denies.
|
|
9
|
+
*/
|
|
10
|
+
import { type Component, type TUI } from "../index.js";
|
|
11
|
+
import type { NativeNode, NativeUiEvent } from "../native/node.js";
|
|
12
|
+
/** The approve key is ignored this long after mount, so a follow-up sent as the prompt appears cannot approve it. */
|
|
13
|
+
export declare const APPROVE_ARM_MS = 500;
|
|
14
|
+
interface ApprovalDialogCallbacks {
|
|
15
|
+
onApprove(): void;
|
|
16
|
+
onDeny(): void;
|
|
17
|
+
}
|
|
18
|
+
interface ApprovalDialogOptions {
|
|
19
|
+
tui?: TUI;
|
|
20
|
+
/** Wrap long details instead of clipping them to a row: set when the dialog is the only place they show. */
|
|
21
|
+
wrapDetails?: boolean;
|
|
22
|
+
/** Deny once this many ms pass without input (any key restarts the countdown). */
|
|
23
|
+
timeout?: number;
|
|
24
|
+
/** Called when {@link timeout} expires, before the dialog denies. */
|
|
25
|
+
onTimeout?: () => void;
|
|
26
|
+
/** Called when the countdown starts, as the dialog mounts. */
|
|
27
|
+
onTimeoutStart?: () => void;
|
|
28
|
+
/** Called each time a key restarts the countdown. */
|
|
29
|
+
onTimeoutReset?: () => void;
|
|
30
|
+
}
|
|
31
|
+
export declare class ApprovalDialogComponent implements Component {
|
|
32
|
+
#private;
|
|
33
|
+
private readonly callbacks;
|
|
34
|
+
private readonly options;
|
|
35
|
+
/** `prompt`: the approval title on its first line, the details after it. */
|
|
36
|
+
constructor(prompt: string, callbacks: ApprovalDialogCallbacks, options?: ApprovalDialogOptions);
|
|
37
|
+
invalidate(): void;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
handleInput(keyData: string): void;
|
|
40
|
+
render(width: number): readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* In the dock, in the composer's place and framed as the composer is (an
|
|
43
|
+
* `omp.editor.*` root role), like the ask dialog, rather than a card over
|
|
44
|
+
* the transcript: the title, the details, then Deny and the primary
|
|
45
|
+
* Approve, whose keys show in their tooltips.
|
|
46
|
+
*/
|
|
47
|
+
describe(): NativeNode;
|
|
48
|
+
/** Deny and Approve run exactly what Esc and the approve key run. */
|
|
49
|
+
handleNativeEvent(event: NativeUiEvent): void;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -86,6 +86,10 @@ export declare class PlanReviewOverlay implements Component {
|
|
|
86
86
|
* reset scroll/focus so the operator starts at the top. Does not emit
|
|
87
87
|
* `onPlanEdited` (the editor round-trip already persisted the file). */
|
|
88
88
|
setPlanContent(planContent: string): void;
|
|
89
|
+
/** Synchronize an external tier pick with the displayed slider before committing. */
|
|
90
|
+
setSliderIndex(index: number): void;
|
|
91
|
+
/** Commit an external answer through the same input-locking path as Enter; false when nothing was committed. */
|
|
92
|
+
selectOption(label: string): boolean;
|
|
89
93
|
handleInput(keyData: string): void;
|
|
90
94
|
/** The native sheet: large glass, titled by the plan's own title heading (the body never repeats it). */
|
|
91
95
|
get nativeOverlay(): NonNullable<Component["nativeOverlay"]>;
|
package/dist/types/thinking.d.ts
CHANGED
|
@@ -37,6 +37,11 @@ export declare function shouldDisableReasoning(level: ThinkingLevel | undefined)
|
|
|
37
37
|
* Resolves a selector against the current model while preserving explicit "off".
|
|
38
38
|
*/
|
|
39
39
|
export declare function resolveThinkingLevelForModel(model: Model | undefined, level: ThinkingLevel | undefined): ResolvedThinkingLevel | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Level applied to `model` when the user has not picked one for it (carried,
|
|
42
|
+
* global default, auto). Unverified thinking stays unset until the user picks.
|
|
43
|
+
*/
|
|
44
|
+
export declare function automaticThinkingLevel<T>(model: Model | undefined, fallback: T): T | undefined;
|
|
40
45
|
/**
|
|
41
46
|
* Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
|
|
42
47
|
* inside the coding-agent layer: it is never an {@link Effort} or
|
package/dist/types/tmux.d.ts
CHANGED
|
@@ -19,3 +19,20 @@ export declare function setTmuxPaneApcTitles(on: boolean, env?: NodeJS.ProcessEn
|
|
|
19
19
|
* query runs once per process and degrades to `null` when unavailable.
|
|
20
20
|
*/
|
|
21
21
|
export declare function resolveTmuxClientTerminalName(env?: NodeJS.ProcessEnv): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Whether tmux draws SIXEL to the client attached to this session: its
|
|
24
|
+
* effective terminal description for that client carries `Sxl`.
|
|
25
|
+
*
|
|
26
|
+
* A tmux built with SIXEL answers every pane's DA1 with attribute 4 and draws a
|
|
27
|
+
* `SIXEL IMAGE (WxH)` text placeholder for clients without SIXEL, so the pane's
|
|
28
|
+
* DA1 says nothing about the outer terminal. tmux itself renders SIXEL only
|
|
29
|
+
* when the client's terminal has `Sxl`, whether from terminfo,
|
|
30
|
+
* `terminal-overrides`, or the `sixel` feature (DA1 or `terminal-features`),
|
|
31
|
+
* and `show-messages -T` reports that merged description. The TUI consults
|
|
32
|
+
* this before trusting the pane's DA1 attribute. Queried once per tmux
|
|
33
|
+
* session; `false` outside tmux, with no attached client, or when the query
|
|
34
|
+
* fails. Nested tmux usually reports no `Sxl`: the inner client's terminal is
|
|
35
|
+
* the outer tmux, whose DA1 starts with level 1, which tmux does not parse
|
|
36
|
+
* for features.
|
|
37
|
+
*/
|
|
38
|
+
export declare function tmuxClientRendersSixel(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -18,6 +18,8 @@ export interface BashToolDetails {
|
|
|
18
18
|
requestedTimeoutSeconds?: number;
|
|
19
19
|
timeoutDisabled?: boolean;
|
|
20
20
|
wallTimeMs?: number;
|
|
21
|
+
/** False when no wall-time footer was emitted; absent on older results. */
|
|
22
|
+
wallTimeNoticeEmitted?: boolean;
|
|
21
23
|
/** Exit code of a command that ran to completion but failed (non-zero). */
|
|
22
24
|
exitCode?: number;
|
|
23
25
|
/** True when the command was killed by its timeout deadline (not a failure). */
|
|
@@ -38,6 +40,8 @@ export interface BashToolDetails {
|
|
|
38
40
|
type: "bash";
|
|
39
41
|
};
|
|
40
42
|
}
|
|
43
|
+
/** Minimum duration for a model-facing wall-time notice; shorter runs keep timing only in details. */
|
|
44
|
+
export declare const WALL_TIME_NOTICE_MIN_MS = 1000;
|
|
41
45
|
/** Formats the model-facing command duration notice. */
|
|
42
46
|
export declare function formatWallTimeNotice(wallTimeMs: number): string;
|
|
43
47
|
/** Formats the model-facing command exit status notice. */
|
|
@@ -114,6 +114,13 @@ export declare function pendingFileLinkPath(inputPath: string): string;
|
|
|
114
114
|
* image reads only record the path in `meta.source`). URL/internal sources are
|
|
115
115
|
* not fs paths, so only `type: "path"` qualifies. */
|
|
116
116
|
export declare function readSourceFsPath(details: ReadToolDetails | undefined): string | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* Whether a read result renders as a Markdown preview rather than numbered source:
|
|
119
|
+
* the read tool tagged it `text/markdown` (`read.renderMarkdown`, or a protocol
|
|
120
|
+
* resource) and the call did not ask for `:raw` source. Shared by the standalone
|
|
121
|
+
* read card and the read group's content previews so both honor the setting.
|
|
122
|
+
*/
|
|
123
|
+
export declare function readResultIsMarkdown(details: ReadToolDetails | undefined, args: ReadRenderArgs | undefined): boolean;
|
|
117
124
|
/**
|
|
118
125
|
* Read content as numbered `code` (no path header: the head names the file).
|
|
119
126
|
* Numbers come from `lineNumbers` (`displayContent`'s per-line numbers,
|
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.10.
|
|
4
|
+
"version": "18.10.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.10.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.10.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.10.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.10.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.10.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.10.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.10.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.10.
|
|
44
|
+
"@oh-my-pi/omptype": "18.10.4",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.10.4",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.10.4",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.10.4",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.10.4",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.10.4",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.10.4",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.10.4"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/app-keybindings.ts
CHANGED
|
@@ -38,6 +38,7 @@ interface AppKeybindings {
|
|
|
38
38
|
"app.tools.expand": true;
|
|
39
39
|
"app.tools.toggleVisibility": true;
|
|
40
40
|
"app.ask.hide": true;
|
|
41
|
+
"app.approval.approve": true;
|
|
41
42
|
"app.editor.external": true;
|
|
42
43
|
"app.message.followUp": true;
|
|
43
44
|
"app.retry": true;
|
|
@@ -143,6 +144,12 @@ export const KEYBINDINGS = {
|
|
|
143
144
|
defaultKeys: "alt+h",
|
|
144
145
|
description: "Hide or show a pending ask dialog",
|
|
145
146
|
},
|
|
147
|
+
"app.approval.approve": {
|
|
148
|
+
// A chord, not Enter: an Enter typed for the composer as the prompt appears must not approve.
|
|
149
|
+
// Ctrl+Q backs up Ctrl+Enter, which Windows Terminal cannot deliver (#1903).
|
|
150
|
+
defaultKeys: ["ctrl+enter", "ctrl+q"],
|
|
151
|
+
description: "Approve a pending tool call",
|
|
152
|
+
},
|
|
146
153
|
"app.editor.external": {
|
|
147
154
|
defaultKeys: "ctrl+g",
|
|
148
155
|
description: "Open external editor",
|
|
@@ -245,7 +245,7 @@ export class RawSseViewerComponent implements Component {
|
|
|
245
245
|
/** The action bar's buttons run the same code as their keys. */
|
|
246
246
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
247
247
|
if (event.type !== "action") return;
|
|
248
|
-
if (event.act === "copy") this.#copyAll();
|
|
248
|
+
if (event.act === "copy-raw") this.#copyAll();
|
|
249
249
|
else if (event.act === "close") this.#close();
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -305,7 +305,7 @@ export class RawSseViewerComponent implements Component {
|
|
|
305
305
|
"status",
|
|
306
306
|
),
|
|
307
307
|
actionBar([
|
|
308
|
-
actionButton("Copy raw", "copy", { keys: "ctrl+c", tone: "accent" }),
|
|
308
|
+
actionButton("Copy raw", "copy-raw", { keys: "ctrl+c", tone: "accent" }),
|
|
309
309
|
null,
|
|
310
310
|
actionButton("Close", "close", { keys: "escape" }),
|
|
311
311
|
]),
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
type ReadRenderArgs,
|
|
11
11
|
type ReadToolDetails,
|
|
12
12
|
readContentCode,
|
|
13
|
+
readResultIsMarkdown,
|
|
13
14
|
readSourceFsPath,
|
|
14
15
|
splitPathAndSel,
|
|
15
16
|
} from "../tools/read";
|
|
16
17
|
import { PREVIEW_LIMITS, shortenPath } from "../render/render-utils";
|
|
17
|
-
import { fileHyperlink, renderCodeCell } from "../render";
|
|
18
|
+
import { fileHyperlink, renderCodeCell, renderMarkdownCell } from "../render";
|
|
18
19
|
import { canonicalizeMessage } from "./thinking-display";
|
|
19
20
|
import { internalUrlSchemeSpec, splitUrlScheme } from "../tools/url-scheme-host";
|
|
20
21
|
import type { ToolExecutionHandle } from "./tool-execution";
|
|
@@ -22,7 +23,7 @@ import { formatUsageRow } from "../overlays/usage-row";
|
|
|
22
23
|
import { formatCount } from "@oh-my-pi/pi-utils";
|
|
23
24
|
import type { TspCardStatus, TspSpan, TspText } from "@oh-my-pi/pi-wire";
|
|
24
25
|
import type { NativeToolHead } from "../tools/renderer";
|
|
25
|
-
import { card, code, keyed, node, span, text, withHidden } from "../native/describe";
|
|
26
|
+
import { card, code, keyed, md, node, span, text, withHidden } from "../native/describe";
|
|
26
27
|
import {
|
|
27
28
|
type DescribeContext,
|
|
28
29
|
type NativeChild,
|
|
@@ -121,11 +122,23 @@ type ReadEntry = {
|
|
|
121
122
|
status: "pending" | "success" | "warning" | "error";
|
|
122
123
|
correctedFrom?: string;
|
|
123
124
|
contentText?: string;
|
|
125
|
+
/** The result's `details.contentType`; see {@link previewsMarkdown}. */
|
|
126
|
+
contentType?: string;
|
|
127
|
+
/** The call's `raw` arg, which keeps Markdown previews as source. */
|
|
128
|
+
raw?: boolean;
|
|
124
129
|
conflictCount?: number;
|
|
125
130
|
codeStartLine?: number;
|
|
126
131
|
codeLineNumbers?: Array<number | null>;
|
|
127
132
|
};
|
|
128
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether an entry previews as rendered Markdown instead of numbered source.
|
|
136
|
+
* Derived on each render so `raw`/path args that land after the result still apply.
|
|
137
|
+
*/
|
|
138
|
+
function previewsMarkdown(entry: ReadEntry): boolean {
|
|
139
|
+
return readResultIsMarkdown({ contentType: entry.contentType }, { path: entry.path, raw: entry.raw });
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
type ReadUsageRow = {
|
|
130
143
|
toolCallIds: readonly string[];
|
|
131
144
|
usage: Usage;
|
|
@@ -135,13 +148,12 @@ type ReadUsageRow = {
|
|
|
135
148
|
turnElapsedMs?: number;
|
|
136
149
|
};
|
|
137
150
|
|
|
138
|
-
/** Number of
|
|
151
|
+
/** Number of preview rows to show per read while collapsed. */
|
|
139
152
|
const COLLAPSED_PREVIEW_LINES = PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
140
153
|
|
|
141
154
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* body, so collapsed shows every file and the head of each preview.
|
|
155
|
+
* The outer data-first group must not cut off later files: code is trimmed by
|
|
156
|
+
* source line and Markdown is clamped by each file's native card after rendering.
|
|
145
157
|
*/
|
|
146
158
|
const TRIMMED_BODY_PREVIEW = { lines: 1000 } as const;
|
|
147
159
|
|
|
@@ -429,6 +441,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
429
441
|
status: "pending",
|
|
430
442
|
};
|
|
431
443
|
entry.path = rawPath;
|
|
444
|
+
entry.raw = args.raw === true;
|
|
432
445
|
this.#entries.set(toolCallId, entry);
|
|
433
446
|
this.#updateDisplay();
|
|
434
447
|
}
|
|
@@ -485,6 +498,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
485
498
|
typeof details?.conflictCount === "number" && details.conflictCount > 0 ? details.conflictCount : undefined;
|
|
486
499
|
entry.conflictCount = conflictCount;
|
|
487
500
|
entry.status = result.isError ? "error" : suffixResolution ? "warning" : "success";
|
|
501
|
+
entry.contentType = details?.contentType;
|
|
488
502
|
// Store clean display content for preview/expanded display when the read
|
|
489
503
|
// tool provides it; fall back to model-facing text for legacy results.
|
|
490
504
|
const displayContent = details?.displayContent;
|
|
@@ -578,8 +592,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
578
592
|
* The read run as one inline `tool` on terminals that list the kind, else
|
|
579
593
|
* the fallback: a bare `card` (role `omp.tool.read`) with `Read <path>` or
|
|
580
594
|
* `Read (N)` in the head, a `list` of path items (status tone, link,
|
|
581
|
-
* correction and conflict detail, nested usage) and
|
|
582
|
-
*
|
|
595
|
+
* correction and conflict detail, nested usage) and a clamped preview per
|
|
596
|
+
* read when content previews are on.
|
|
583
597
|
*/
|
|
584
598
|
override describe(cx?: DescribeContext): NativeNode {
|
|
585
599
|
const dataFirst = cx?.supports("tool") === true;
|
|
@@ -593,8 +607,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
593
607
|
* The data-first read (§7.3 read, read group, read error): one inline
|
|
594
608
|
* `tool` (role `omp.tool.read`). One file: `Read path:13-36`, a failure's
|
|
595
609
|
* message in the head. Several: `Read 3 files` over one 22px row per file
|
|
596
|
-
* (glyph, dim dir, strong name, range).
|
|
597
|
-
*
|
|
610
|
+
* (glyph, dim dir, strong name, range). Each content preview is numbered
|
|
611
|
+
* code or rendered Markdown, clamped per file while collapsed.
|
|
598
612
|
*/
|
|
599
613
|
#describeTool(): NativeNode {
|
|
600
614
|
const entries = [...this.#entries.values()];
|
|
@@ -715,8 +729,21 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
715
729
|
);
|
|
716
730
|
}
|
|
717
731
|
|
|
718
|
-
/** One read's content
|
|
732
|
+
/** One read's content, clamped per file while the group is collapsed. */
|
|
719
733
|
#nativePreview(entry: ReadEntry): NativeChild[] {
|
|
734
|
+
if (previewsMarkdown(entry)) {
|
|
735
|
+
return [
|
|
736
|
+
card(
|
|
737
|
+
{
|
|
738
|
+
variant: "bare",
|
|
739
|
+
collapsible: true,
|
|
740
|
+
collapsed: !this.#expanded,
|
|
741
|
+
preview: { lines: COLLAPSED_PREVIEW_LINES },
|
|
742
|
+
},
|
|
743
|
+
[md(entry.contentText ?? "")],
|
|
744
|
+
),
|
|
745
|
+
];
|
|
746
|
+
}
|
|
720
747
|
const split = splitPathAndSel(entry.path);
|
|
721
748
|
const lines = (entry.contentText ?? "").split("\n");
|
|
722
749
|
const shown = this.#expanded ? lines.length : Math.min(lines.length, COLLAPSED_PREVIEW_LINES);
|
|
@@ -796,11 +823,13 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
796
823
|
preview: { lines: COLLAPSED_PREVIEW_LINES },
|
|
797
824
|
},
|
|
798
825
|
[
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
826
|
+
previewsMarkdown(entry)
|
|
827
|
+
? md(entry.contentText ?? "")
|
|
828
|
+
: code(entry.contentText ?? "", {
|
|
829
|
+
lang: getLanguageFromPath(split.path),
|
|
830
|
+
start: entry.codeStartLine,
|
|
831
|
+
numbers: true,
|
|
832
|
+
}),
|
|
804
833
|
],
|
|
805
834
|
`p${entry.toolCallId}`,
|
|
806
835
|
),
|
|
@@ -1124,7 +1153,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
1124
1153
|
}
|
|
1125
1154
|
|
|
1126
1155
|
/**
|
|
1127
|
-
* Add a code-cell content preview below the entry summary.
|
|
1156
|
+
* Add a code-cell (or Markdown-cell) content preview below the entry summary.
|
|
1128
1157
|
* When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with a "… N more lines ⟨<key>: Expand⟩" hint.
|
|
1129
1158
|
* When expanded: shows full content.
|
|
1130
1159
|
*/
|
|
@@ -1151,20 +1180,33 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
1151
1180
|
const component: Component = {
|
|
1152
1181
|
render: (width: number) => {
|
|
1153
1182
|
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1183
|
+
const status = entry.status === "success" ? "complete" : entry.status;
|
|
1184
|
+
cachedLines = previewsMarkdown(entry)
|
|
1185
|
+
? renderMarkdownCell(
|
|
1186
|
+
{
|
|
1187
|
+
content: entry.contentText ?? "",
|
|
1188
|
+
title,
|
|
1189
|
+
status,
|
|
1190
|
+
expanded,
|
|
1191
|
+
contentMaxLines: COLLAPSED_PREVIEW_LINES,
|
|
1192
|
+
width,
|
|
1193
|
+
},
|
|
1194
|
+
theme,
|
|
1195
|
+
)
|
|
1196
|
+
: renderCodeCell(
|
|
1197
|
+
{
|
|
1198
|
+
code: entry.contentText ?? "",
|
|
1199
|
+
language: lang,
|
|
1200
|
+
title,
|
|
1201
|
+
status,
|
|
1202
|
+
expanded,
|
|
1203
|
+
codeMaxLines: expanded ? undefined : COLLAPSED_PREVIEW_LINES,
|
|
1204
|
+
codeStartLine: entry.codeStartLine,
|
|
1205
|
+
codeLineNumbers: entry.codeLineNumbers,
|
|
1206
|
+
width,
|
|
1207
|
+
},
|
|
1208
|
+
theme,
|
|
1209
|
+
);
|
|
1168
1210
|
cachedWidth = width;
|
|
1169
1211
|
return cachedLines;
|
|
1170
1212
|
},
|
package/src/hotkeys-markdown.ts
CHANGED
|
@@ -136,6 +136,7 @@ function hotkeyGroups(bindings: HotkeysMarkdownBindings): HotkeyGroup[] {
|
|
|
136
136
|
{ keys: [act("app.tools.expand")], action: "Toggle tool output expansion" },
|
|
137
137
|
{ keys: [act("app.tools.toggleVisibility")], action: "Toggle tool activity visibility" },
|
|
138
138
|
{ keys: [act("app.ask.hide")], action: "Hide/show a pending ask question" },
|
|
139
|
+
{ keys: [act("app.approval.approve")], action: "Approve a pending tool call" },
|
|
139
140
|
{ keys: [act("app.thinking.toggle")], action: "Toggle thinking block visibility" },
|
|
140
141
|
{ keys: [act("app.editor.external")], action: "Edit message in external editor" },
|
|
141
142
|
{ keys: [act("app.retry")], action: "Retry last failed assistant turn" },
|
|
@@ -101,3 +101,17 @@ export function matchesAppFollowUp(data: string): boolean {
|
|
|
101
101
|
}
|
|
102
102
|
return matchesEffectiveKeys(data, ["ctrl+enter", "ctrl+q"]);
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/** Approve-key defaults when `app.approval.approve` is unbound: approval must stay reachable by keyboard. */
|
|
106
|
+
const APPROVE_DEFAULT_KEYS: readonly KeyId[] = ["ctrl+enter", "ctrl+q"];
|
|
107
|
+
|
|
108
|
+
/** Effective tool-approval keys: the `app.approval.approve` bindings, or Ctrl+Enter/Ctrl+Q when unbound. */
|
|
109
|
+
export function appApproveKeys(): readonly KeyId[] {
|
|
110
|
+
const keys = getKeybindings().getKeys("app.approval.approve");
|
|
111
|
+
return keys.length > 0 ? keys : APPROVE_DEFAULT_KEYS;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Match the tool-approval key, recognizing modifier-tagged LF as Ctrl+Enter like {@link matchesAppFollowUp}. */
|
|
115
|
+
export function matchesAppApprove(data: string): boolean {
|
|
116
|
+
return matchesEffectiveKeys(data, appApproveKeys());
|
|
117
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-approval prompt in the composer's place, presented like the ask
|
|
3
|
+
* dialog: the approval title and its details over an action bar. Approving
|
|
4
|
+
* takes a deliberate chord (`app.approval.approve`, Ctrl+Enter by default),
|
|
5
|
+
* so an Enter typed for the composer as the prompt appears never approves a
|
|
6
|
+
* call, and the chord itself only arms {@link APPROVE_ARM_MS} after mount: it
|
|
7
|
+
* is also the follow-up key, pressed mid-run exactly when prompts appear.
|
|
8
|
+
* Esc denies.
|
|
9
|
+
*/
|
|
10
|
+
import { type Component, Ellipsis, ScrollView, type TUI, truncateToWidth, wrapTextWithAnsi } from "../index";
|
|
11
|
+
import { formatKeyHints, formatTooltipKey } from "../app-keybindings";
|
|
12
|
+
import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
|
|
13
|
+
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
14
|
+
import { getKeybindings } from "../keybindings";
|
|
15
|
+
import {
|
|
16
|
+
appApproveKeys,
|
|
17
|
+
matchesAppApprove,
|
|
18
|
+
matchesSelectCancel,
|
|
19
|
+
matchesSelectDown,
|
|
20
|
+
matchesSelectPageDown,
|
|
21
|
+
matchesSelectPageUp,
|
|
22
|
+
matchesSelectUp,
|
|
23
|
+
} from "../keybinding-matchers";
|
|
24
|
+
import { col, node } from "../native/describe";
|
|
25
|
+
import type { NativeNode, NativeUiEvent } from "../native/node";
|
|
26
|
+
import { actionBar, actionButton } from "../native/overlay";
|
|
27
|
+
import { PREVIEW_LIMITS, sanitizeDisplayLines } from "../render/render-utils";
|
|
28
|
+
import { theme } from "../theme/theme";
|
|
29
|
+
import { CountdownTimer } from "../chrome/countdown-timer";
|
|
30
|
+
|
|
31
|
+
/** Detail rows shown before the rest scrolls: the prompt stays compact, the transcript has the call in full. */
|
|
32
|
+
const MAX_DETAIL_ROWS: number = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
33
|
+
/** The approve key is ignored this long after mount, so a follow-up sent as the prompt appears cannot approve it. */
|
|
34
|
+
export const APPROVE_ARM_MS = 500;
|
|
35
|
+
|
|
36
|
+
interface ApprovalDialogCallbacks {
|
|
37
|
+
onApprove(): void;
|
|
38
|
+
onDeny(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ApprovalDialogOptions {
|
|
42
|
+
tui?: TUI;
|
|
43
|
+
/** Wrap long details instead of clipping them to a row: set when the dialog is the only place they show. */
|
|
44
|
+
wrapDetails?: boolean;
|
|
45
|
+
/** Deny once this many ms pass without input (any key restarts the countdown). */
|
|
46
|
+
timeout?: number;
|
|
47
|
+
/** Called when {@link timeout} expires, before the dialog denies. */
|
|
48
|
+
onTimeout?: () => void;
|
|
49
|
+
/** Called when the countdown starts, as the dialog mounts. */
|
|
50
|
+
onTimeoutStart?: () => void;
|
|
51
|
+
/** Called each time a key restarts the countdown. */
|
|
52
|
+
onTimeoutReset?: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ApprovalDialogComponent implements Component {
|
|
56
|
+
readonly #title: string;
|
|
57
|
+
readonly #details: readonly string[];
|
|
58
|
+
readonly #panel: OverlayPanel;
|
|
59
|
+
readonly #body = new PanelRows();
|
|
60
|
+
readonly #footer = new PanelRows();
|
|
61
|
+
#scrollOffset = 0;
|
|
62
|
+
#bodyRows = MAX_DETAIL_ROWS;
|
|
63
|
+
#closed = false;
|
|
64
|
+
#native: NativeNode | undefined;
|
|
65
|
+
readonly #armedAt = performance.now() + APPROVE_ARM_MS;
|
|
66
|
+
readonly #countdown: CountdownTimer | undefined;
|
|
67
|
+
|
|
68
|
+
/** `prompt`: the approval title on its first line, the details after it. */
|
|
69
|
+
constructor(
|
|
70
|
+
prompt: string,
|
|
71
|
+
private readonly callbacks: ApprovalDialogCallbacks,
|
|
72
|
+
private readonly options: ApprovalDialogOptions = {},
|
|
73
|
+
) {
|
|
74
|
+
const [title = "", ...details] = sanitizeDisplayLines(prompt);
|
|
75
|
+
while (details.length > 0 && details[details.length - 1]!.trim() === "") details.pop();
|
|
76
|
+
this.#title = title;
|
|
77
|
+
this.#details = details;
|
|
78
|
+
this.#panel = new OverlayPanel(title, "omp.overlay.approval");
|
|
79
|
+
this.#footer.setHeight(1);
|
|
80
|
+
this.#panel.addChild(this.#body);
|
|
81
|
+
this.#panel.addChild(new PanelDivider());
|
|
82
|
+
this.#panel.addChild(this.#footer);
|
|
83
|
+
if (options.timeout && options.timeout > 0) {
|
|
84
|
+
options.onTimeoutStart?.();
|
|
85
|
+
this.#countdown = new CountdownTimer(
|
|
86
|
+
options.timeout,
|
|
87
|
+
options.tui,
|
|
88
|
+
seconds => {
|
|
89
|
+
this.#panel.title = `${title} (${seconds}s)`;
|
|
90
|
+
},
|
|
91
|
+
() => {
|
|
92
|
+
options.onTimeout?.();
|
|
93
|
+
this.#finish(false);
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
invalidate(): void {
|
|
100
|
+
this.#native = undefined;
|
|
101
|
+
this.#panel.invalidate();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
dispose(): void {
|
|
105
|
+
this.#closed = true;
|
|
106
|
+
this.#countdown?.dispose();
|
|
107
|
+
this.#panel.dispose();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
handleInput(keyData: string): void {
|
|
111
|
+
if (this.#closed) return;
|
|
112
|
+
if (this.#countdown) {
|
|
113
|
+
this.#countdown.reset();
|
|
114
|
+
this.options.onTimeoutReset?.();
|
|
115
|
+
// The described countdown restarts with it.
|
|
116
|
+
this.#native = undefined;
|
|
117
|
+
}
|
|
118
|
+
if (matchesAppApprove(keyData)) {
|
|
119
|
+
if (performance.now() >= this.#armedAt) this.#finish(true);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (matchesSelectCancel(keyData)) {
|
|
123
|
+
this.#finish(false);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const page = Math.max(1, this.#bodyRows - 1);
|
|
127
|
+
if (matchesSelectUp(keyData)) this.#scroll(-1);
|
|
128
|
+
else if (matchesSelectDown(keyData)) this.#scroll(1);
|
|
129
|
+
else if (matchesSelectPageUp(keyData)) this.#scroll(-page);
|
|
130
|
+
else if (matchesSelectPageDown(keyData)) this.#scroll(page);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
render(width: number): readonly string[] {
|
|
134
|
+
const innerWidth = Math.max(1, width - 4);
|
|
135
|
+
let lines = this.#detailLines(innerWidth);
|
|
136
|
+
const rows = Math.max(1, Math.min(lines.length, MAX_DETAIL_ROWS));
|
|
137
|
+
const overflows = lines.length > rows;
|
|
138
|
+
// Leave a column for the scrollbar once the details overflow.
|
|
139
|
+
if (overflows && innerWidth > 1) lines = this.#detailLines(innerWidth - 1);
|
|
140
|
+
this.#bodyRows = rows;
|
|
141
|
+
this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, lines.length - rows));
|
|
142
|
+
if (overflows) {
|
|
143
|
+
const view = new ScrollView(lines, {
|
|
144
|
+
height: rows,
|
|
145
|
+
scrollbar: "auto",
|
|
146
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
147
|
+
});
|
|
148
|
+
view.setScrollOffset(this.#scrollOffset);
|
|
149
|
+
this.#body.setLines(view.render(innerWidth));
|
|
150
|
+
} else {
|
|
151
|
+
this.#body.setLines(lines);
|
|
152
|
+
}
|
|
153
|
+
this.#body.setHeight(rows);
|
|
154
|
+
this.#footer.setLines([theme.fg("dim", this.#footerHint(overflows))]);
|
|
155
|
+
return this.#panel.render(width);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* In the dock, in the composer's place and framed as the composer is (an
|
|
160
|
+
* `omp.editor.*` root role), like the ask dialog, rather than a card over
|
|
161
|
+
* the transcript: the title, the details, then Deny and the primary
|
|
162
|
+
* Approve, whose keys show in their tooltips.
|
|
163
|
+
*/
|
|
164
|
+
describe(): NativeNode {
|
|
165
|
+
if (this.#native) return this.#native;
|
|
166
|
+
const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0];
|
|
167
|
+
const approveKey = appApproveKeys()[0];
|
|
168
|
+
const deny = actionButton("Deny", "deny", {
|
|
169
|
+
...(cancelKey ? { keys: cancelKey } : {}),
|
|
170
|
+
tone: "error",
|
|
171
|
+
title: `Deny this call${cancelKey ? ` ${formatTooltipKey(cancelKey)}` : ""}`,
|
|
172
|
+
});
|
|
173
|
+
const approve = actionButton("Approve", "approve", {
|
|
174
|
+
...(approveKey ? { keys: approveKey } : {}),
|
|
175
|
+
tone: "accent",
|
|
176
|
+
title: `Approve this call${approveKey ? ` ${formatTooltipKey(approveKey)}` : ""}`,
|
|
177
|
+
});
|
|
178
|
+
const children: NativeNode[] = [
|
|
179
|
+
node("text", { text: this.#title, role: "omp.approval.title", tone: "accent" }, undefined, "title"),
|
|
180
|
+
];
|
|
181
|
+
if (this.#details.length > 0) {
|
|
182
|
+
// One clipped row per detail like the text view, unless wrapping was asked for.
|
|
183
|
+
const wrap = this.options.wrapDetails === true;
|
|
184
|
+
const rows = this.#details.map((line, index) =>
|
|
185
|
+
node("text", wrap ? { text: line } : { text: line, truncate: "end" }, undefined, String(index)),
|
|
186
|
+
);
|
|
187
|
+
children.push(node("col", { role: "omp.approval.details" }, rows, "details"));
|
|
188
|
+
}
|
|
189
|
+
// The countdown, when one runs, leads the bar like the ask dialog's.
|
|
190
|
+
children.push(actionBar([...(this.#countdown ? [this.#countdown.describe()] : []), null, deny, approve]));
|
|
191
|
+
// Under the `omp.editor.ask` prefix: Tern's console layout sizes an ask's rows to the
|
|
192
|
+
// composer column by that prefix, where a bare `omp.editor.*` role spans the whole dock.
|
|
193
|
+
this.#native = col(children, { role: "omp.editor.ask.approval", gap: "md" });
|
|
194
|
+
return this.#native;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Deny and Approve run exactly what Esc and the approve key run. */
|
|
198
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
199
|
+
if (this.#closed || event.type !== "action") return;
|
|
200
|
+
if (event.act === "approve") this.#finish(true);
|
|
201
|
+
else if (event.act === "deny") this.#finish(false);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Detail rows at `width`: one clipped row per detail, or wrapped rows with {@link ApprovalDialogOptions.wrapDetails}. */
|
|
205
|
+
#detailLines(width: number): string[] {
|
|
206
|
+
if (!this.options.wrapDetails) {
|
|
207
|
+
return this.#details.map(line => truncateToWidth(theme.fg("accent", line), width, Ellipsis.Unicode));
|
|
208
|
+
}
|
|
209
|
+
return this.#details.flatMap(line => (line === "" ? [""] : wrapTextWithAnsi(theme.fg("accent", line), width)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#footerHint(overflows: boolean): string {
|
|
213
|
+
const approve = formatKeyHints(appApproveKeys());
|
|
214
|
+
const scroll = overflows ? ` · ${editorKeys("tui.select.up", "tui.select.down")} scroll` : "";
|
|
215
|
+
return `${approve} approve · ${editorKey("tui.select.cancel")} deny${scroll}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#scroll(delta: number): void {
|
|
219
|
+
this.#scrollOffset = Math.max(0, this.#scrollOffset + delta);
|
|
220
|
+
this.options.tui?.requestRender();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#finish(approved: boolean): void {
|
|
224
|
+
if (this.#closed) return;
|
|
225
|
+
this.#closed = true;
|
|
226
|
+
this.#countdown?.dispose();
|
|
227
|
+
if (approved) this.callbacks.onApprove();
|
|
228
|
+
else this.callbacks.onDeny();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -514,7 +514,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
514
514
|
case "close":
|
|
515
515
|
this.#options.onClose();
|
|
516
516
|
return;
|
|
517
|
-
case "copy":
|
|
517
|
+
case "copy-answer":
|
|
518
518
|
if (record && getBtwCopyText(record) !== undefined) this.#options.onCopy(record);
|
|
519
519
|
break;
|
|
520
520
|
case "follow-up":
|
|
@@ -661,7 +661,7 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
661
661
|
const buttons: (NativeNode | null)[] = [];
|
|
662
662
|
if (running) buttons.push(actionButton("Stop answer", "stop", { keys: hides ? "x" : "escape" }));
|
|
663
663
|
if (record && getBtwCopyText(record) !== undefined) {
|
|
664
|
-
buttons.push(actionButton(copied ? "Copy again" : "Copy answer", "copy", { keys: "c" }));
|
|
664
|
+
buttons.push(actionButton(copied ? "Copy again" : "Copy answer", "copy-answer", { keys: "c" }));
|
|
665
665
|
if (copied) buttons.push(text([span("✓ Copied", "success")], { role: "omp.btw.copied" }));
|
|
666
666
|
}
|
|
667
667
|
buttons.push(null);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
12
12
|
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
13
|
-
import { getModelPricingStatus, modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
13
|
+
import { getEffectiveTokenCost, getModelPricingStatus, modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
14
14
|
import { modelKind, type ModelKind, type ModelPricingStatus } from "@oh-my-pi/pi-catalog/types";
|
|
15
15
|
import type { Component } from "../tui";
|
|
16
16
|
import { FuzzyCorpus, fuzzyRank } from "../fuzzy";
|
|
@@ -742,7 +742,7 @@ function declaredPricingStatus(model: Model): Exclude<ModelPricingStatus, "fixed
|
|
|
742
742
|
function isFreeModel(model: Model): boolean {
|
|
743
743
|
const declared = declaredPricingStatus(model);
|
|
744
744
|
if (declared !== undefined) return declared === "free";
|
|
745
|
-
const cost = model.cost;
|
|
745
|
+
const cost = model.cost ? getEffectiveTokenCost(model.cost) : undefined;
|
|
746
746
|
const credits = model.factoryDroidCredits;
|
|
747
747
|
return (!cost || (cost.input === 0 && cost.output === 0)) && (credits === undefined || credits === 0);
|
|
748
748
|
}
|
|
@@ -766,7 +766,8 @@ function withCreditBadge(model: Model, price: string): string {
|
|
|
766
766
|
const credits = model.factoryDroidCredits;
|
|
767
767
|
if (credits === undefined) return price;
|
|
768
768
|
const badge = `${formatCostLeg(credits)}×`;
|
|
769
|
-
|
|
769
|
+
const cost = getEffectiveTokenCost(model.cost);
|
|
770
|
+
return cost.input !== 0 || cost.output !== 0 ? `${price} ${badge}` : badge;
|
|
770
771
|
}
|
|
771
772
|
|
|
772
773
|
const PRICING_STATUS_LABELS: Record<Exclude<ModelPricingStatus, "fixed">, { short: string; detail: string }> = {
|
|
@@ -781,7 +782,8 @@ function formatCostPair(model: Model): string {
|
|
|
781
782
|
const declared = declaredPricingStatus(model);
|
|
782
783
|
if (declared !== undefined) return PRICING_STATUS_LABELS[declared].short;
|
|
783
784
|
if (isFreeModel(model)) return "free";
|
|
784
|
-
|
|
785
|
+
const cost = getEffectiveTokenCost(model.cost);
|
|
786
|
+
return withCreditBadge(model, `$${formatCostLeg(cost.input)}/${formatCostLeg(cost.output)}`);
|
|
785
787
|
}
|
|
786
788
|
|
|
787
789
|
/** Detail-pane price fact: `$3/15 per M`, or the declared pricing state in words. */
|
|
@@ -803,14 +805,15 @@ function pickerPrice(model: Model): string {
|
|
|
803
805
|
const declared = declaredPricingStatus(model);
|
|
804
806
|
if (declared !== undefined) return PRICING_STATUS_LABELS[declared].short;
|
|
805
807
|
if (isFreeModel(model)) return "free";
|
|
806
|
-
|
|
808
|
+
const cost = getEffectiveTokenCost(model.cost);
|
|
809
|
+
return withCreditBadge(model, `$${formatCostLeg(cost.input)}·${formatCostLeg(cost.output)}`);
|
|
807
810
|
}
|
|
808
811
|
|
|
809
812
|
/** `$2 in · $10 out · $0.2 cache` for a model preview. */
|
|
810
813
|
function previewPrice(model: Model): string {
|
|
811
814
|
const declared = declaredPricingStatus(model);
|
|
812
815
|
if (declared !== undefined) return PRICING_STATUS_LABELS[declared].detail;
|
|
813
|
-
const cost = model.cost;
|
|
816
|
+
const cost = getEffectiveTokenCost(model.cost);
|
|
814
817
|
const parts = [`$${formatCostLeg(cost.input)} in`, `$${formatCostLeg(cost.output)} out`];
|
|
815
818
|
if (cost.cacheRead > 0) parts.push(`$${formatCostLeg(cost.cacheRead)} cache`);
|
|
816
819
|
return withCreditBadge(model, parts.join(" · "));
|
|
@@ -1328,6 +1328,7 @@ export class ModelHubComponent implements Component {
|
|
|
1328
1328
|
*/
|
|
1329
1329
|
#thinkingOptionsFor(model: Model): ConfiguredThinkingLevel[] {
|
|
1330
1330
|
if (!model.reasoning) return [];
|
|
1331
|
+
if (model.thinking?.unverified) return [ThinkingLevel.Inherit, ThinkingLevel.Off, ...getSupportedEfforts(model)];
|
|
1331
1332
|
return [ThinkingLevel.Inherit, ThinkingLevel.Off, AUTO_THINKING, ...getSupportedEfforts(model)];
|
|
1332
1333
|
}
|
|
1333
1334
|
|
|
@@ -606,6 +606,22 @@ export class PlanReviewOverlay implements Component {
|
|
|
606
606
|
slider.onChange?.(next);
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
/** Synchronize an external tier pick with the displayed slider before committing. */
|
|
610
|
+
setSliderIndex(index: number): void {
|
|
611
|
+
if (this.#committed || !this.#slider) return;
|
|
612
|
+
this.#moveSlider(index - this.#sliderIndex);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Commit an external answer through the same input-locking path as Enter; false when nothing was committed. */
|
|
616
|
+
selectOption(label: string): boolean {
|
|
617
|
+
if (this.#committed) return false;
|
|
618
|
+
const index = this.#options.indexOf(label);
|
|
619
|
+
if (index < 0 || this.#disabled.has(index)) return false;
|
|
620
|
+
this.#selectedIndex = index;
|
|
621
|
+
this.#confirmSelection();
|
|
622
|
+
return this.#committed;
|
|
623
|
+
}
|
|
624
|
+
|
|
609
625
|
#confirmSelection(): void {
|
|
610
626
|
const index = this.#selectedIndex;
|
|
611
627
|
if (index >= 0 && index < this.#options.length && !this.#disabled.has(index)) {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
SHAPE_VARIANT_NAMES,
|
|
25
25
|
SHAPE_VARIANTS,
|
|
26
26
|
type Shape,
|
|
27
|
+
type ShapeGeometry,
|
|
27
28
|
type ShapeTarget,
|
|
28
29
|
type ShapeVariantName,
|
|
29
30
|
} from "@oh-my-pi/snapcompact";
|
|
@@ -90,7 +91,7 @@ export class SnapcompactShapePreview implements Component {
|
|
|
90
91
|
|
|
91
92
|
describe(cx: DescribeContext): NativeNode {
|
|
92
93
|
const shape = resolveShape(this.#model, this.#variant);
|
|
93
|
-
const name = resolvedVariantName(shape);
|
|
94
|
+
const name = this.#variant === "auto" ? resolvedVariantName(shape) : this.#variant;
|
|
94
95
|
const entry = cx.supports("image") ? this.#ensureEntry(name, shape) : undefined;
|
|
95
96
|
const memo = this.#native;
|
|
96
97
|
if (memo !== undefined && memo.variant === this.#variant && memo.entry === entry) return memo.node;
|
|
@@ -128,7 +129,7 @@ export class SnapcompactShapePreview implements Component {
|
|
|
128
129
|
|
|
129
130
|
render(width: number): readonly string[] {
|
|
130
131
|
const shape = resolveShape(this.#model, this.#variant);
|
|
131
|
-
const name = resolvedVariantName(shape);
|
|
132
|
+
const name = this.#variant === "auto" ? resolvedVariantName(shape) : this.#variant;
|
|
132
133
|
const { label, stats } = this.#caption(shape, name);
|
|
133
134
|
const lines: string[] = [theme.fg("muted", ` Sample (zoomed) · ${label} · ${stats}`), ""];
|
|
134
135
|
|
|
@@ -215,17 +216,23 @@ export class SnapcompactShapePreview implements Component {
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* Research name of the geometry `resolveShape` picked for `auto`. The frame
|
|
221
|
+
* edge is not compared: `auto` widens it per model line and hosts cap it
|
|
222
|
+
* (catalog `image-max-edge`), and the sample renders a fixed mini-page anyway.
|
|
223
|
+
*/
|
|
219
224
|
function resolvedVariantName(shape: Shape): ShapeVariantName {
|
|
220
225
|
for (const name of SHAPE_VARIANT_NAMES) {
|
|
221
|
-
const candidate = SHAPE_VARIANTS[name];
|
|
226
|
+
const candidate: ShapeGeometry = SHAPE_VARIANTS[name];
|
|
222
227
|
if (
|
|
223
228
|
candidate.font === shape.font &&
|
|
224
229
|
candidate.cellWidth === shape.cellWidth &&
|
|
225
230
|
candidate.cellHeight === shape.cellHeight &&
|
|
231
|
+
candidate.stretch === shape.stretch &&
|
|
226
232
|
candidate.variant === shape.variant &&
|
|
227
|
-
candidate.
|
|
228
|
-
candidate.
|
|
233
|
+
candidate.stopwordDim === shape.stopwordDim &&
|
|
234
|
+
candidate.columns === shape.columns &&
|
|
235
|
+
candidate.lineRepeat === shape.lineRepeat
|
|
229
236
|
) {
|
|
230
237
|
return name;
|
|
231
238
|
}
|
package/src/thinking.ts
CHANGED
|
@@ -132,6 +132,14 @@ export function resolveThinkingLevelForModel(
|
|
|
132
132
|
return clampThinkingLevelForModel(model, level);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Level applied to `model` when the user has not picked one for it (carried,
|
|
137
|
+
* global default, auto). Unverified thinking stays unset until the user picks.
|
|
138
|
+
*/
|
|
139
|
+
export function automaticThinkingLevel<T>(model: Model | undefined, fallback: T): T | undefined {
|
|
140
|
+
return model?.thinking?.unverified ? undefined : fallback;
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
/**
|
|
136
144
|
* Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
|
|
137
145
|
* inside the coding-agent layer: it is never an {@link Effort} or
|
|
@@ -374,7 +382,7 @@ export function modelSupportsEffortCeiling(model: Model, ceiling: Effort): boole
|
|
|
374
382
|
* models.
|
|
375
383
|
*/
|
|
376
384
|
export function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined {
|
|
377
|
-
if (!model?.reasoning) return undefined;
|
|
385
|
+
if (!model?.reasoning || model.thinking?.unverified) return undefined;
|
|
378
386
|
const preferred = model.thinking?.defaultLevel ?? Effort.High;
|
|
379
387
|
return clampAutoThinkingEffort(model, preferred === Effort.Max ? Effort.XHigh : preferred, Effort.XHigh);
|
|
380
388
|
}
|
package/src/tmux.ts
CHANGED
|
@@ -86,3 +86,35 @@ export function resolveTmuxClientTerminalName(env: NodeJS.ProcessEnv = Bun.env):
|
|
|
86
86
|
if (cachedClientTerminalName === undefined) cachedClientTerminalName = queryTmuxClientTerminalName(env);
|
|
87
87
|
return cachedClientTerminalName;
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
const SIXEL_CAPABILITY = /^\s*\d+: Sxl: \(flag\) true$/mu;
|
|
91
|
+
let cachedClientSixel: { tmux: string; renders: boolean } | undefined;
|
|
92
|
+
|
|
93
|
+
function queryTmuxClientRendersSixel(env: NodeJS.ProcessEnv): boolean {
|
|
94
|
+
const client = runTmux(["display-message", "-p", "#{client_name}"], env)?.trim();
|
|
95
|
+
if (!client) return false;
|
|
96
|
+
return SIXEL_CAPABILITY.test(runTmux(["show-messages", "-T", "-t", client], env) ?? "");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether tmux draws SIXEL to the client attached to this session: its
|
|
101
|
+
* effective terminal description for that client carries `Sxl`.
|
|
102
|
+
*
|
|
103
|
+
* A tmux built with SIXEL answers every pane's DA1 with attribute 4 and draws a
|
|
104
|
+
* `SIXEL IMAGE (WxH)` text placeholder for clients without SIXEL, so the pane's
|
|
105
|
+
* DA1 says nothing about the outer terminal. tmux itself renders SIXEL only
|
|
106
|
+
* when the client's terminal has `Sxl`, whether from terminfo,
|
|
107
|
+
* `terminal-overrides`, or the `sixel` feature (DA1 or `terminal-features`),
|
|
108
|
+
* and `show-messages -T` reports that merged description. The TUI consults
|
|
109
|
+
* this before trusting the pane's DA1 attribute. Queried once per tmux
|
|
110
|
+
* session; `false` outside tmux, with no attached client, or when the query
|
|
111
|
+
* fails. Nested tmux usually reports no `Sxl`: the inner client's terminal is
|
|
112
|
+
* the outer tmux, whose DA1 starts with level 1, which tmux does not parse
|
|
113
|
+
* for features.
|
|
114
|
+
*/
|
|
115
|
+
export function tmuxClientRendersSixel(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
116
|
+
const tmux = env.TMUX;
|
|
117
|
+
if (!tmux) return false;
|
|
118
|
+
if (cachedClientSixel?.tmux !== tmux) cachedClientSixel = { tmux, renders: queryTmuxClientRendersSixel(env) };
|
|
119
|
+
return cachedClientSixel.renders;
|
|
120
|
+
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface BashToolDetails {
|
|
|
62
62
|
requestedTimeoutSeconds?: number;
|
|
63
63
|
timeoutDisabled?: boolean;
|
|
64
64
|
wallTimeMs?: number;
|
|
65
|
+
/** False when no wall-time footer was emitted; absent on older results. */
|
|
66
|
+
wallTimeNoticeEmitted?: boolean;
|
|
65
67
|
/** Exit code of a command that ran to completion but failed (non-zero). */
|
|
66
68
|
exitCode?: number;
|
|
67
69
|
/** True when the command was killed by its timeout deadline (not a failure). */
|
|
@@ -209,6 +211,9 @@ function formatWallTimeSeconds(wallTimeMs: number): string {
|
|
|
209
211
|
return (wallTimeMs / 1000).toFixed(2);
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
/** Minimum duration for a model-facing wall-time notice; shorter runs keep timing only in details. */
|
|
215
|
+
export const WALL_TIME_NOTICE_MIN_MS = 1000;
|
|
216
|
+
|
|
212
217
|
/** Formats the model-facing command duration notice. */
|
|
213
218
|
export function formatWallTimeNotice(wallTimeMs: number): string {
|
|
214
219
|
return `Wall time: ${formatWallTimeSeconds(wallTimeMs)} seconds`;
|
|
@@ -319,7 +324,7 @@ function stripBashNotices(
|
|
|
319
324
|
? strippedOutput
|
|
320
325
|
: stripTrailingNotice(strippedOutput, formatExitCodeNotice(details.exitCode));
|
|
321
326
|
const withoutWall =
|
|
322
|
-
details?.wallTimeMs === undefined
|
|
327
|
+
details?.wallTimeMs === undefined || details.wallTimeNoticeEmitted === false
|
|
323
328
|
? withoutExit
|
|
324
329
|
: stripTrailingNotice(withoutExit, formatWallTimeNotice(details.wallTimeMs));
|
|
325
330
|
return stripRawOutputArtifactNotice(withoutWall);
|
package/src/tools/read.ts
CHANGED
|
@@ -341,6 +341,18 @@ function readRawPath(args: ReadRenderArgs | undefined): string {
|
|
|
341
341
|
return typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Whether a read result renders as a Markdown preview rather than numbered source:
|
|
346
|
+
* the read tool tagged it `text/markdown` (`read.renderMarkdown`, or a protocol
|
|
347
|
+
* resource) and the call did not ask for `:raw` source. Shared by the standalone
|
|
348
|
+
* read card and the read group's content previews so both honor the setting.
|
|
349
|
+
*/
|
|
350
|
+
export function readResultIsMarkdown(details: ReadToolDetails | undefined, args: ReadRenderArgs | undefined): boolean {
|
|
351
|
+
if (details?.contentType !== "text/markdown" || args?.raw === true) return false;
|
|
352
|
+
const sel = splitReadRenderPath(readRawPath(args)).sel;
|
|
353
|
+
return sel?.split(":").some(chunk => chunk.toLowerCase() === "raw") !== true;
|
|
354
|
+
}
|
|
355
|
+
|
|
344
356
|
/**
|
|
345
357
|
* Native read head (§7.3 read): `Read path:13-36` with the path as the
|
|
346
358
|
* target (shortened, plus the selector / legacy offset range), a `file://`
|
|
@@ -600,9 +612,7 @@ export const readToolRenderer = {
|
|
|
600
612
|
const n = details.conflictCount;
|
|
601
613
|
title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
602
614
|
}
|
|
603
|
-
const
|
|
604
|
-
args?.raw === true || renderPath.sel?.split(":").some(chunk => chunk.toLowerCase() === "raw") === true;
|
|
605
|
-
const isMarkdown = details?.contentType === "text/markdown" && !rawRequested;
|
|
615
|
+
const isMarkdown = readResultIsMarkdown(details, args);
|
|
606
616
|
let cachedWidth: number | undefined;
|
|
607
617
|
let cachedExpanded: boolean | undefined;
|
|
608
618
|
let cachedLines: string[] | undefined;
|
|
@@ -719,17 +729,14 @@ export const readToolRenderer = {
|
|
|
719
729
|
}
|
|
720
730
|
|
|
721
731
|
const renderPath = splitReadRenderPath(rawPath);
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
details?.displayContent?.lineNumbers,
|
|
731
|
-
getLanguageFromPath(renderPath.path),
|
|
732
|
-
);
|
|
732
|
+
const content: NativeChild[] = readResultIsMarkdown(details, args)
|
|
733
|
+
? [md(contentText)]
|
|
734
|
+
: readContentCode(
|
|
735
|
+
contentText,
|
|
736
|
+
details?.displayContent?.startLine ?? readRangeStart(rawPath, args),
|
|
737
|
+
details?.displayContent?.lineNumbers,
|
|
738
|
+
getLanguageFromPath(renderPath.path),
|
|
739
|
+
);
|
|
733
740
|
return { tool: head, inline: true, body: compact<NativeChild>([media, ...content, foot]) };
|
|
734
741
|
},
|
|
735
742
|
mergeCallAndResult: true,
|
package/src/tui.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
TERMINAL,
|
|
47
47
|
} from "./terminal-capabilities";
|
|
48
48
|
import { compositeLineAt } from "./render/composite";
|
|
49
|
+
import { tmuxMultiplexer } from "./multiplexers/tmux";
|
|
49
50
|
import {
|
|
50
51
|
Ellipsis,
|
|
51
52
|
getWidthConfigEpoch,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
truncateToWidth,
|
|
57
58
|
visibleWidth,
|
|
58
59
|
} from "./utils";
|
|
60
|
+
import { tmuxClientRendersSixel } from "./tmux";
|
|
59
61
|
|
|
60
62
|
/** Full-attribute reset terminating each rendered content row. */
|
|
61
63
|
export const SEGMENT_RESET = "\x1b[0m";
|
|
@@ -1527,6 +1529,9 @@ export class TUI extends Container {
|
|
|
1527
1529
|
this.terminal.onSixelSupport?.(supported => {
|
|
1528
1530
|
if (!supported || this.#stopped || this.#nativeLive || this.terminal.tspExpected) return;
|
|
1529
1531
|
if (isImageProtocolForced()) return;
|
|
1532
|
+
// tmux answers DA1 for its own emulator; only its record of the
|
|
1533
|
+
// attached client says whether SIXEL reaches the outer terminal.
|
|
1534
|
+
if (tmuxMultiplexer.isInside() && !tmuxClientRendersSixel()) return;
|
|
1530
1535
|
// Keep consuming an already-requested XTSMGRAPHICS reply until it
|
|
1531
1536
|
// arrives or times out, even though DA1 has established support.
|
|
1532
1537
|
this.#enableSixelProtocol();
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate the TypeScript method signatures advertising eval-bridged tools
|
|
3
|
-
* under Codex Code Mode (spliced into the `eval-code-mode.md` prompt),
|
|
4
|
-
* mirroring codex-rs augment_tool_spec_for_code_mode.
|
|
5
|
-
*/
|
|
6
|
-
import { arkToWireSchema, isArkSchema } from "@oh-my-pi/pi-ai/utils/schema/wire";
|
|
7
|
-
|
|
8
|
-
interface JsonSchema {
|
|
9
|
-
type?: string;
|
|
10
|
-
properties?: Record<string, JsonSchema>;
|
|
11
|
-
required?: string[];
|
|
12
|
-
items?: JsonSchema;
|
|
13
|
-
enum?: unknown[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const TS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
17
|
-
|
|
18
|
-
function tsType(schema: JsonSchema | undefined, depth: number): string {
|
|
19
|
-
if (!schema || depth > 2) return "unknown";
|
|
20
|
-
if (Array.isArray(schema.enum) && schema.enum.every(value => typeof value === "string")) {
|
|
21
|
-
return schema.enum.map(value => JSON.stringify(value)).join(" | ");
|
|
22
|
-
}
|
|
23
|
-
switch (schema.type) {
|
|
24
|
-
case "string":
|
|
25
|
-
return "string";
|
|
26
|
-
case "number":
|
|
27
|
-
case "integer":
|
|
28
|
-
return "number";
|
|
29
|
-
case "boolean":
|
|
30
|
-
return "boolean";
|
|
31
|
-
case "array": {
|
|
32
|
-
const item = tsType(schema.items, depth + 1);
|
|
33
|
-
// `"a" | "b"[]` parses as `"a" | ("b"[])`, so a union item needs parens.
|
|
34
|
-
return /[|&]/.test(item) ? `(${item})[]` : `${item}[]`;
|
|
35
|
-
}
|
|
36
|
-
case "object": {
|
|
37
|
-
if (!schema.properties) return "Record<string, unknown>";
|
|
38
|
-
const required = new Set(schema.required ?? []);
|
|
39
|
-
const entries = Object.entries(schema.properties).map(([key, value]) => {
|
|
40
|
-
const printedKey = TS_IDENTIFIER.test(key) ? key : JSON.stringify(key);
|
|
41
|
-
return `${printedKey}${required.has(key) ? "" : "?"}: ${tsType(value, depth + 1)}`;
|
|
42
|
-
});
|
|
43
|
-
return `{ ${entries.join("; ")} }`;
|
|
44
|
-
}
|
|
45
|
-
default:
|
|
46
|
-
return "unknown";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Format tool schemas as TypeScript declarations for code-mode discovery. */
|
|
51
|
-
export function generateCodeModeDeclarations(tools: ReadonlyArray<{ name: string; parameters: unknown }>): string {
|
|
52
|
-
const lines = tools.map(tool => {
|
|
53
|
-
const printedName = TS_IDENTIFIER.test(tool.name) ? tool.name : JSON.stringify(tool.name);
|
|
54
|
-
const wire = isArkSchema(tool.parameters)
|
|
55
|
-
? (arkToWireSchema(tool.parameters) as JsonSchema)
|
|
56
|
-
: (tool.parameters as JsonSchema | undefined);
|
|
57
|
-
const args = wire?.type === "object" && wire.properties ? tsType(wire, 0) : "unknown";
|
|
58
|
-
return ` ${printedName}(args: ${args}): Promise<unknown>;`;
|
|
59
|
-
});
|
|
60
|
-
return lines.join("\n");
|
|
61
|
-
}
|