@oh-my-pi/pi-tui 18.2.7 → 18.2.9
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 +21 -0
- package/dist/types/kitty-graphics.d.ts +1 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +14 -2
- package/dist/types/overlays/usage-dashboard.d.ts +6 -0
- package/dist/types/overlays/usage-display.d.ts +16 -1
- package/dist/types/prompt/image-source.d.ts +26 -0
- package/dist/types/prompt/video.d.ts +7 -11
- package/dist/types/status-line/types.d.ts +7 -0
- package/dist/types/stdin-buffer.d.ts +9 -0
- package/dist/types/terminal-capabilities.d.ts +1 -1
- package/dist/types/tmux.d.ts +8 -0
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/hub.d.ts +2 -0
- package/dist/types/utils.d.ts +2 -0
- package/package.json +9 -9
- package/src/components/editor.ts +34 -14
- package/src/kitty-graphics.ts +2 -2
- package/src/overlays/composer-shape-preview.ts +3 -1
- package/src/overlays/reset-usage-selector.ts +77 -25
- package/src/overlays/usage-dashboard.ts +62 -2
- package/src/overlays/usage-display.ts +74 -1
- package/src/prompt/custom-editor.ts +3 -2
- package/src/prompt/image-source.ts +51 -0
- package/src/prompt/video.ts +10 -20
- package/src/status-line/component.ts +48 -5
- package/src/status-line/segments.ts +18 -1
- package/src/status-line/types.ts +7 -0
- package/src/stdin-buffer.ts +22 -0
- package/src/terminal-capabilities.ts +38 -12
- package/src/terminal.ts +10 -0
- package/src/tmux.ts +38 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/find.ts +18 -5
- package/src/tools/hub.ts +4 -2
- package/src/utils.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.9] - 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Claude and Codex saved-reset counts, availability, and expiry to usage views, with provider-specific confirmation and uniquely identified account options.
|
|
10
|
+
- Added terminal detection and capability support for otty and rio, including Kitty graphics and true-color support where available, plus otty hyperlinks and notifications.
|
|
11
|
+
- Added the public `stripTerminalSequences` export for extensions that need to remove terminal control sequences.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Composer shape previews now use the full available overlay width instead of being clipped at 96 columns.
|
|
16
|
+
- Fixed cursor placement when recalling prompts from history, keeping single-line entries at the end and preserving the appropriate position for multi-line entries.
|
|
17
|
+
- Restored modified-key handling and terminal notifications over SSH sessions running inside tmux.
|
|
18
|
+
- Fixed typed Enter occasionally being interpreted as a literal newline when terminal input events are batched.
|
|
19
|
+
|
|
20
|
+
## [18.2.8] - 2026-09-21
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Improved Bash tool background-task notices by providing completed output as a follow-up and discouraging unnecessary polling.
|
|
25
|
+
|
|
5
26
|
## [18.2.7] - 2026-09-21
|
|
6
27
|
|
|
7
28
|
### Breaking Changes
|
|
@@ -26,7 +26,7 @@ export interface KittyGraphicsFeatures {
|
|
|
26
26
|
* Whether the detected terminal renders Kitty Unicode placeholders (`U=1` +
|
|
27
27
|
* U+10EEEE with row/column diacritics).
|
|
28
28
|
*
|
|
29
|
-
* Kitty and
|
|
29
|
+
* Kitty, Ghostty and Otty advertise placeholder support directly. A multiplexer
|
|
30
30
|
* cannot use cursor-positioned placements because the outer terminal does not
|
|
31
31
|
* know pane scroll/reflow state. An explicit `PI_FORCE_IMAGE_PROTOCOL=kitty`
|
|
32
32
|
* opts into placeholders under any multiplexer — matching `timg -pk`.
|
|
@@ -1,18 +1,30 @@
|
|
|
1
|
+
import type { UsageResetCreditDetail } from "@oh-my-pi/pi-ai";
|
|
1
2
|
import { OverlayPanel } from "../chrome/overlay-box.js";
|
|
2
3
|
/** One account row with its redeemable rate-limit reset credits. */
|
|
3
4
|
export interface ResetUsageAccount {
|
|
4
5
|
label: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
providerLabel: string;
|
|
8
|
+
/** Banked resets, including grants that cannot be spent right now. */
|
|
5
9
|
availableCount: number;
|
|
10
|
+
/** Resets the provider currently permits this account to spend. */
|
|
11
|
+
redeemableCount: number;
|
|
6
12
|
target: {
|
|
7
|
-
credentialId
|
|
13
|
+
credentialId: number;
|
|
14
|
+
provider: string;
|
|
15
|
+
creditId?: string;
|
|
8
16
|
accountId?: string;
|
|
9
17
|
email?: string;
|
|
18
|
+
orgId?: string;
|
|
10
19
|
};
|
|
11
20
|
active: boolean;
|
|
12
21
|
error?: string;
|
|
22
|
+
unavailableReason?: string;
|
|
23
|
+
expiresAt?: string;
|
|
24
|
+
credit?: UsageResetCreditDetail;
|
|
13
25
|
}
|
|
14
26
|
/**
|
|
15
|
-
* Account picker for `/usage reset`. Lists
|
|
27
|
+
* Account picker for `/usage reset`. Lists provider accounts with their saved
|
|
16
28
|
* rate-limit reset counts; selecting one redeems a reset. Because a reset is a
|
|
17
29
|
* scarce, irreversible credit, Enter requires a second press to confirm.
|
|
18
30
|
*/
|
|
@@ -32,6 +32,12 @@ export interface ProviderCard {
|
|
|
32
32
|
unlimited: boolean;
|
|
33
33
|
/** True when nothing is used anywhere (or there are no limits): collapses to a tick. */
|
|
34
34
|
idle: boolean;
|
|
35
|
+
resetCredits?: {
|
|
36
|
+
bankedCount: number;
|
|
37
|
+
redeemableCount: number;
|
|
38
|
+
soonestExpiryMs?: number;
|
|
39
|
+
unavailableReasons: string[];
|
|
40
|
+
};
|
|
35
41
|
}
|
|
36
42
|
/**
|
|
37
43
|
* Collapse usage reports into one compact card per provider: limits grouped by
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import type { UsageLimit, UsageReport, UsageResetCredits } from "@oh-my-pi/pi-ai";
|
|
2
2
|
/** Include the usage tier in a limit title unless its label already names it. */
|
|
3
3
|
export declare function formatLimitTitle(limit: UsageLimit): string;
|
|
4
|
+
/** Human-readable quota scope for a normalized saved-reset window ID. */
|
|
5
|
+
export declare function formatUsageResetWindow(windowId: string): string;
|
|
6
|
+
/** Saved inventory and current eligibility shared by every usage display. */
|
|
7
|
+
export interface UsageResetSummary {
|
|
8
|
+
bankedCount: number;
|
|
9
|
+
redeemableCount: number;
|
|
10
|
+
soonestExpiry?: string;
|
|
11
|
+
unavailableReason?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Normalize old count-only and current provider reset metadata for display.
|
|
15
|
+
* `availableCount` is banked inventory; `redeemableCount` is the subset that
|
|
16
|
+
* may be spent now. Older providers used `availableCount` for both.
|
|
17
|
+
*/
|
|
18
|
+
export declare function summarizeUsageResetCredits(reset: UsageResetCredits | undefined, nowMs?: number): UsageResetSummary | undefined;
|
|
4
19
|
/** Collapse routing-specific copies of a shared quota for user-facing usage views. */
|
|
5
20
|
export declare function collapseSharedUsageReports(reports: UsageReport[]): UsageReport[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Original filesystem source carried on image attachments that entered from a
|
|
3
|
+
* file on disk: path-pasted/drag-and-dropped images and generated video
|
|
4
|
+
* contact-sheet previews. Symbol metadata stays out of serialized/model-bound
|
|
5
|
+
* image data while traveling with the draft object, until AgentSession creates
|
|
6
|
+
* the hidden companion message that tells the model the path (and link
|
|
7
|
+
* materialization prefers it over a blob copy).
|
|
8
|
+
*/
|
|
9
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
|
+
/** How an image attachment's backing file entered the session. */
|
|
11
|
+
export type ImageAttachmentSourceKind = "image" | "video";
|
|
12
|
+
/** Original local file backing an image attachment. */
|
|
13
|
+
export interface ImageAttachmentSource {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly kind: ImageAttachmentSourceKind;
|
|
16
|
+
}
|
|
17
|
+
declare const kImageAttachmentSource: unique symbol;
|
|
18
|
+
/** An image attachment tagged with the original local file it came from. */
|
|
19
|
+
export type SourceTaggedImage = ImageContent & {
|
|
20
|
+
readonly [kImageAttachmentSource]: ImageAttachmentSource;
|
|
21
|
+
};
|
|
22
|
+
/** Create a model-ready image tagged with the local file it was loaded from. */
|
|
23
|
+
export declare function tagImageAttachmentSource(image: ImageContent, path: string, kind: ImageAttachmentSourceKind): SourceTaggedImage;
|
|
24
|
+
/** Return the original local source of an image attachment, or undefined for payloads with no source file (clipboard bitmaps). */
|
|
25
|
+
export declare function imageAttachmentSource(image: ImageContent): ImageAttachmentSource | undefined;
|
|
26
|
+
export {};
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { type SourceTaggedImage } from "./image-source.js";
|
|
2
3
|
/** True when the path names a video container we handle through ffmpeg. */
|
|
3
4
|
export declare function isVideoPath(filePath: string): boolean;
|
|
4
|
-
/**
|
|
5
|
-
* Original local source path stored on a generated contact-sheet image. Symbol
|
|
6
|
-
* metadata stays out of serialized/model-bound image data while traveling with
|
|
7
|
-
* the draft object until AgentSession creates its hidden companion message.
|
|
8
|
-
*/
|
|
9
|
-
declare const kVideoPreviewSource: unique symbol;
|
|
10
5
|
/** A contact-sheet image tagged with the original local video path. */
|
|
11
|
-
export type VideoPreviewImage =
|
|
12
|
-
readonly [kVideoPreviewSource]: string;
|
|
13
|
-
};
|
|
6
|
+
export type VideoPreviewImage = SourceTaggedImage;
|
|
14
7
|
/** Create a model-ready contact-sheet image tagged with its original video path. */
|
|
15
8
|
export declare function createVideoPreviewImage(preview: ImageContent, sourcePath: string): VideoPreviewImage;
|
|
16
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Return the original video path associated with a generated contact-sheet
|
|
11
|
+
* image, via the shared attachment-source tag (see {@link tagImageAttachmentSource}).
|
|
12
|
+
* Returns undefined for untagged images and for image-file (non-video) sources.
|
|
13
|
+
*/
|
|
17
14
|
export declare function videoPreviewSource(preview: ImageContent): string | undefined;
|
|
18
|
-
export {};
|
|
@@ -209,6 +209,13 @@ export interface SegmentContext {
|
|
|
209
209
|
percent: number;
|
|
210
210
|
resetHours?: number;
|
|
211
211
|
};
|
|
212
|
+
resetCredits?: {
|
|
213
|
+
bankedCount: number;
|
|
214
|
+
redeemableCount: number;
|
|
215
|
+
expiryHours?: number;
|
|
216
|
+
expired?: boolean;
|
|
217
|
+
unavailableReason?: string;
|
|
218
|
+
};
|
|
212
219
|
} | null;
|
|
213
220
|
}
|
|
214
221
|
export interface RenderedSegment {
|
|
@@ -62,6 +62,15 @@ export declare class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
62
62
|
#private;
|
|
63
63
|
constructor(options?: StdinBufferOptions);
|
|
64
64
|
process(data: string | Buffer): void;
|
|
65
|
+
/**
|
|
66
|
+
* Enable or disable unbracketed raw-paste classification. Terminal disables
|
|
67
|
+
* it once DECRQM confirms bracketed-paste (mode 2004) support: a genuine
|
|
68
|
+
* paste then always arrives wrapped, so the heuristic can only misfire on
|
|
69
|
+
* keystrokes an event-loop stall batched into one read (issue #12540).
|
|
70
|
+
* Disabling flushes any candidate already held by the classification window
|
|
71
|
+
* as ordinary key events so no buffered input is lost.
|
|
72
|
+
*/
|
|
73
|
+
setRawPasteClassification(enabled: boolean): void;
|
|
65
74
|
flush(): string[];
|
|
66
75
|
clear(): void;
|
|
67
76
|
getBuffer(): string;
|
|
@@ -11,7 +11,7 @@ export declare enum NotifyProtocol {
|
|
|
11
11
|
Osc99 = "\u001B]99;;",
|
|
12
12
|
Osc9 = "\u001B]9;"
|
|
13
13
|
}
|
|
14
|
-
export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "base" | "trueColor";
|
|
14
|
+
export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "otty" | "rio" | "base" | "trueColor";
|
|
15
15
|
/** Terminal capability details used for rendering and protocol selection. */
|
|
16
16
|
export declare class TerminalInfo {
|
|
17
17
|
readonly id: TerminalId;
|
package/dist/types/tmux.d.ts
CHANGED
|
@@ -4,3 +4,11 @@ export declare function isInsideTmux(env?: NodeJS.ProcessEnv): boolean;
|
|
|
4
4
|
export declare function wrapTmuxPassthrough(payload: string): string;
|
|
5
5
|
/** Pass a control sequence through tmux, leaving direct-terminal output unchanged. */
|
|
6
6
|
export declare function wrapTmuxPassthroughIfNeeded(payload: string, env?: NodeJS.ProcessEnv): string;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the terminal emulator name recorded for this tmux client.
|
|
9
|
+
*
|
|
10
|
+
* tmux overwrites pane identity variables with its own values, but retains the
|
|
11
|
+
* attached client's terminal-type reply in `#{client_termtype}`. The local IPC
|
|
12
|
+
* query runs once per process and degrades to `null` when unavailable.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveTmuxClientTerminalName(env?: NodeJS.ProcessEnv): string | null;
|
|
@@ -11,7 +11,7 @@ export interface FindRange {
|
|
|
11
11
|
}
|
|
12
12
|
/** A file whose verified passages cleared the threshold; `ranges` are merged positive spans, strongest first. */
|
|
13
13
|
export interface FindHit {
|
|
14
|
-
/** Display path relative to {@link FindToolDetails.cwd}. */
|
|
14
|
+
/** Display path relative to {@link FindToolDetails.cwd}, or an `omp://` doc URL for docs scopes. */
|
|
15
15
|
rel: string;
|
|
16
16
|
/** Filename judgment, when the name batch answered. */
|
|
17
17
|
nameScore?: number;
|
|
@@ -44,6 +44,8 @@ export interface JobSnapshot {
|
|
|
44
44
|
status: "running" | "completed" | "failed" | "cancelled";
|
|
45
45
|
label: string;
|
|
46
46
|
durationMs: number;
|
|
47
|
+
/** Process exit status when the job reports one. */
|
|
48
|
+
exitCode?: number;
|
|
47
49
|
/** Effective task model selector, including an explicit reasoning suffix when configured. */
|
|
48
50
|
resolvedModel?: string;
|
|
49
51
|
/** Provider/id including routing, with no added thinking suffix. */
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export declare function getSegmenter(): Intl.Segmenter;
|
|
|
49
49
|
* sequences (counted as printable by Bun, actually zero cells).
|
|
50
50
|
*/
|
|
51
51
|
export declare function visibleWidth(str: string): number;
|
|
52
|
+
/** Remove ANSI, OSC, and APC control sequences while preserving visible text. */
|
|
53
|
+
export declare function stripTerminalSequences(str: string): string;
|
|
52
54
|
/**
|
|
53
55
|
* Scaled spans must bypass wrapping/padding and, when scaled up, reserve the
|
|
54
56
|
* terminal rows their multicell glyphs flow into.
|
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.2.
|
|
4
|
+
"version": "18.2.9",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/omptype": "18.2.
|
|
41
|
-
"@oh-my-pi/pi-agent-core": "18.2.
|
|
42
|
-
"@oh-my-pi/pi-ai": "18.2.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
44
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
45
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
46
|
-
"@oh-my-pi/pi-wire": "18.2.
|
|
47
|
-
"@oh-my-pi/snapcompact": "18.2.
|
|
40
|
+
"@oh-my-pi/omptype": "18.2.9",
|
|
41
|
+
"@oh-my-pi/pi-agent-core": "18.2.9",
|
|
42
|
+
"@oh-my-pi/pi-ai": "18.2.9",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "18.2.9",
|
|
44
|
+
"@oh-my-pi/pi-natives": "18.2.9",
|
|
45
|
+
"@oh-my-pi/pi-utils": "18.2.9",
|
|
46
|
+
"@oh-my-pi/pi-wire": "18.2.9",
|
|
47
|
+
"@oh-my-pi/snapcompact": "18.2.9"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/components/editor.ts
CHANGED
|
@@ -951,6 +951,9 @@ export class Editor implements Component, Focusable {
|
|
|
951
951
|
this.restoreHistoryState(entry?.draft?.restore);
|
|
952
952
|
this.#historyDraftActive = entry?.draft !== undefined;
|
|
953
953
|
}
|
|
954
|
+
// Browsing asks for the edge the key came from, so one press still steps one
|
|
955
|
+
// entry: Up opens a multi-row entry at its top, Down at its bottom (#99).
|
|
956
|
+
// #setTextInternal drops that request for single-row entries — see there.
|
|
954
957
|
const cursorAnchor: HistoryCursorAnchor = direction === -1 ? "start" : "end";
|
|
955
958
|
this.#setTextInternal(entry?.text ?? "", cursorAnchor);
|
|
956
959
|
}
|
|
@@ -959,7 +962,13 @@ export class Editor implements Component, Focusable {
|
|
|
959
962
|
this.#undoStack.length = 0;
|
|
960
963
|
const lines = sanitizeLoadedText(text).split("\n");
|
|
961
964
|
this.#state.lines = lines.length === 0 ? [""] : lines;
|
|
962
|
-
|
|
965
|
+
// A single-row entry's top and bottom are the same row, so the directional
|
|
966
|
+
// anchor degenerates to a bare column choice: the caret would sit at the start
|
|
967
|
+
// when Up recalled the entry and at the end when Down reached the same entry,
|
|
968
|
+
// leaving delete/yank commands aimed at column 0 on one path and at the tail on
|
|
969
|
+
// the other. Single-row entries always open at the end — matching what
|
|
970
|
+
// wholesale text replacement (`setText`) and the first-edit anchor do.
|
|
971
|
+
if (cursorAnchor === "start" && this.#spansMultipleVisualRows()) {
|
|
963
972
|
this.#state.cursorLine = 0;
|
|
964
973
|
this.#setCursorCol(0);
|
|
965
974
|
} else {
|
|
@@ -969,6 +978,17 @@ export class Editor implements Component, Focusable {
|
|
|
969
978
|
this.#notifyChange();
|
|
970
979
|
}
|
|
971
980
|
|
|
981
|
+
/** Whether the buffer needs more than one visual row at the last painted layout
|
|
982
|
+
* width. The directional history anchors are load-bearing only for such entries —
|
|
983
|
+
* they keep one keypress stepping one entry instead of walking inside it — so a
|
|
984
|
+
* newline-free line that wraps past the editor width counts as multi-row too. */
|
|
985
|
+
#spansMultipleVisualRows(): boolean {
|
|
986
|
+
if (this.#state.lines.length > 1) return true;
|
|
987
|
+
const width = this.#lastLayoutWidth;
|
|
988
|
+
if (width <= 0) return false;
|
|
989
|
+
return this.#layoutText(width).length > 1;
|
|
990
|
+
}
|
|
991
|
+
|
|
972
992
|
invalidate(): void {
|
|
973
993
|
// No cached state to invalidate currently
|
|
974
994
|
}
|
|
@@ -1114,8 +1134,12 @@ export class Editor implements Component, Focusable {
|
|
|
1114
1134
|
const lastGrapheme = beforeGraphemes[beforeGraphemes.length - 1]?.segment;
|
|
1115
1135
|
const lastGraphemeWidth = lastGrapheme ? visibleWidth(lastGrapheme) : 0;
|
|
1116
1136
|
const builtInCursor = this.#getStyledInputCursor();
|
|
1137
|
+
// The end-of-line cursor borrows the last grapheme's cell, which the
|
|
1138
|
+
// on-character cursor also highlights with reverse video. Underline the
|
|
1139
|
+
// borrowed cell instead so insertion after the last character stays
|
|
1140
|
+
// visually distinct from insertion before it.
|
|
1117
1141
|
const fallbackReplacement = lastGrapheme
|
|
1118
|
-
? { text:
|
|
1142
|
+
? { text: `\x1b[4m${lastGrapheme}\x1b[0m`, width: lastGraphemeWidth }
|
|
1119
1143
|
: builtInCursor;
|
|
1120
1144
|
const clampReplacement = (candidate: { text: string; width: number }): { text: string; width: number } => {
|
|
1121
1145
|
let text = sliceByColumn(candidate.text, 0, maxWidth, true);
|
|
@@ -1136,7 +1160,6 @@ export class Editor implements Component, Focusable {
|
|
|
1136
1160
|
// If even the highlighted trailing grapheme cannot fit, show the built-in single-column cursor.
|
|
1137
1161
|
clampedReplacement = clampReplacement(builtInCursor);
|
|
1138
1162
|
}
|
|
1139
|
-
|
|
1140
1163
|
const replacedSpanWidth = Math.min(maxWidth, Math.max(lastGraphemeWidth, clampedReplacement.width));
|
|
1141
1164
|
const prefixWidth = Math.max(0, maxWidth - replacedSpanWidth);
|
|
1142
1165
|
const beforePrefix = sliceByColumn(before, 0, prefixWidth, true);
|
|
@@ -1152,17 +1175,14 @@ export class Editor implements Component, Focusable {
|
|
|
1152
1175
|
if (visibleWidth(text) < maxWidth) {
|
|
1153
1176
|
return text + marker;
|
|
1154
1177
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
return `${text.slice(0, insertAt)}${marker}${text.slice(insertAt)}`;
|
|
1178
|
+
// The row is exactly full, so the marker lands before the last visible
|
|
1179
|
+
// grapheme instead of after it. The mirrored on-character position renders
|
|
1180
|
+
// the identical string; underline the final grapheme at end-of-line so the
|
|
1181
|
+
// two insertion points stay visually distinct.
|
|
1182
|
+
const graphemes = [...segmenter.segment(text)];
|
|
1183
|
+
const lastGrapheme = graphemes[graphemes.length - 1]?.segment;
|
|
1184
|
+
if (lastGrapheme === undefined) return text + marker;
|
|
1185
|
+
return `${text.slice(0, text.length - lastGrapheme.length)}\x1b[4m${lastGrapheme}\x1b[0m${marker}`;
|
|
1166
1186
|
}
|
|
1167
1187
|
|
|
1168
1188
|
#getPageScrollStep(totalVisualLines: number): number {
|
package/src/kitty-graphics.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface KittyGraphicsFeatures {
|
|
|
63
63
|
* Whether the detected terminal renders Kitty Unicode placeholders (`U=1` +
|
|
64
64
|
* U+10EEEE with row/column diacritics).
|
|
65
65
|
*
|
|
66
|
-
* Kitty and
|
|
66
|
+
* Kitty, Ghostty and Otty advertise placeholder support directly. A multiplexer
|
|
67
67
|
* cannot use cursor-positioned placements because the outer terminal does not
|
|
68
68
|
* know pane scroll/reflow state. An explicit `PI_FORCE_IMAGE_PROTOCOL=kitty`
|
|
69
69
|
* opts into placeholders under any multiplexer — matching `timg -pk`.
|
|
@@ -82,7 +82,7 @@ export function detectKittyUnicodePlaceholdersSupport(terminalId: string, env: N
|
|
|
82
82
|
const insideMultiplexer = isInsideTerminalMultiplexer(env);
|
|
83
83
|
if (insideMultiplexer && env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase() === "kitty") return true;
|
|
84
84
|
if (isInsideHerdr(env)) return false;
|
|
85
|
-
return terminalId === "kitty" || terminalId === "ghostty";
|
|
85
|
+
return terminalId === "kitty" || terminalId === "ghostty" || terminalId === "otty" || terminalId === "rio";
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
let features: KittyGraphicsFeatures = {
|
|
@@ -49,7 +49,9 @@ export function renderComposerShapePreview(
|
|
|
49
49
|
width: number,
|
|
50
50
|
status?: ComposerPreviewStatusSource,
|
|
51
51
|
): readonly string[] {
|
|
52
|
-
|
|
52
|
+
// No upper cap: the hosting overlay already bounds width by the terminal,
|
|
53
|
+
// and capping clips the status band the preview exists to show (#12500).
|
|
54
|
+
const previewWidth = Math.max(24, width);
|
|
53
55
|
const style = getComposerStyle(shape);
|
|
54
56
|
const paddingX = style.defaultPaddingX(undefined);
|
|
55
57
|
const chromeWidth = style.sideChromeWidth(paddingX);
|
|
@@ -1,27 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { UsageResetCreditDetail } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { Container, matchesKey, ScrollView, Spacer, Text, TruncatedText } from "../index";
|
|
3
|
+
import { formatDuration, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
2
4
|
import { theme } from "../theme/theme";
|
|
3
5
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
|
|
4
6
|
import { OverlayPanel } from "../chrome/overlay-box";
|
|
5
7
|
import { MenuSelection } from "../components/menu-selection";
|
|
6
8
|
import { centeredViewportRange } from "../components/scroll-viewport";
|
|
9
|
+
import { formatUsageResetWindow } from "./usage-display";
|
|
7
10
|
|
|
8
11
|
const RESET_SELECTOR_MAX_VISIBLE = 10;
|
|
9
12
|
|
|
10
13
|
/** One account row with its redeemable rate-limit reset credits. */
|
|
11
14
|
export interface ResetUsageAccount {
|
|
12
15
|
label: string;
|
|
16
|
+
provider: string;
|
|
17
|
+
providerLabel: string;
|
|
18
|
+
/** Banked resets, including grants that cannot be spent right now. */
|
|
13
19
|
availableCount: number;
|
|
20
|
+
/** Resets the provider currently permits this account to spend. */
|
|
21
|
+
redeemableCount: number;
|
|
14
22
|
target: {
|
|
15
|
-
credentialId
|
|
23
|
+
credentialId: number;
|
|
24
|
+
provider: string;
|
|
25
|
+
creditId?: string;
|
|
16
26
|
accountId?: string;
|
|
17
27
|
email?: string;
|
|
28
|
+
orgId?: string;
|
|
18
29
|
};
|
|
19
30
|
active: boolean;
|
|
20
31
|
error?: string;
|
|
32
|
+
unavailableReason?: string;
|
|
33
|
+
expiresAt?: string;
|
|
34
|
+
credit?: UsageResetCreditDetail;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
/**
|
|
24
|
-
* Account picker for `/usage reset`. Lists
|
|
38
|
+
* Account picker for `/usage reset`. Lists provider accounts with their saved
|
|
25
39
|
* rate-limit reset counts; selecting one redeems a reset. Because a reset is a
|
|
26
40
|
* scarce, irreversible credit, Enter requires a second press to confirm.
|
|
27
41
|
*/
|
|
@@ -36,18 +50,17 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
|
|
|
36
50
|
super("Spend a saved rate-limit reset");
|
|
37
51
|
this.#onSelectCallback = onSelect;
|
|
38
52
|
this.#onCancelCallback = onCancel;
|
|
39
|
-
const firstRedeemable = accounts.find(account => account.
|
|
53
|
+
const firstRedeemable = accounts.find(account => account.redeemableCount > 0);
|
|
54
|
+
const accountKey = (account: ResetUsageAccount) =>
|
|
55
|
+
`${account.provider}:${account.target.credentialId}:${account.target.creditId ?? ""}`;
|
|
40
56
|
this.#menu = new MenuSelection<ResetUsageAccount>(
|
|
41
57
|
accounts,
|
|
42
58
|
{
|
|
43
|
-
getKey:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
requiresConfirmation: account => account.availableCount > 0,
|
|
59
|
+
getKey: accountKey,
|
|
60
|
+
getSearchText: account => `${account.label} ${account.providerLabel}`,
|
|
61
|
+
requiresConfirmation: account => account.redeemableCount > 0,
|
|
47
62
|
},
|
|
48
|
-
firstRedeemable
|
|
49
|
-
? `${firstRedeemable.target.credentialId ?? ""}:${firstRedeemable.target.accountId ?? ""}:${firstRedeemable.target.email ?? ""}:${firstRedeemable.label}`
|
|
50
|
-
: undefined,
|
|
63
|
+
firstRedeemable ? accountKey(firstRedeemable) : undefined,
|
|
51
64
|
);
|
|
52
65
|
|
|
53
66
|
this.#listContainer = new Container();
|
|
@@ -57,6 +70,7 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
|
|
|
57
70
|
|
|
58
71
|
#updateList(): void {
|
|
59
72
|
this.#listContainer.clear();
|
|
73
|
+
const oneLine = (value: string): string => sanitizeText(value.replace(/[\r\n\t]+/g, " "));
|
|
60
74
|
|
|
61
75
|
const items = this.#menu.visibleItems;
|
|
62
76
|
const total = items.length;
|
|
@@ -68,22 +82,43 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
|
|
|
68
82
|
const account = items[i];
|
|
69
83
|
if (!account) continue;
|
|
70
84
|
const isSelected = i === this.#menu.selectedIndex;
|
|
71
|
-
const redeemable = account.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
const redeemable = account.redeemableCount > 0;
|
|
86
|
+
let countLabel: string;
|
|
87
|
+
if (account.error) {
|
|
88
|
+
countLabel = oneLine(account.error);
|
|
89
|
+
} else {
|
|
90
|
+
countLabel = `${account.availableCount} saved reset${account.availableCount === 1 ? "" : "s"}`;
|
|
91
|
+
if (account.redeemableCount !== account.availableCount) {
|
|
92
|
+
countLabel += ` · ${account.redeemableCount} usable now`;
|
|
93
|
+
}
|
|
94
|
+
if (account.expiresAt) {
|
|
95
|
+
const expiryMs = Date.parse(account.expiresAt);
|
|
96
|
+
if (!Number.isNaN(expiryMs)) {
|
|
97
|
+
countLabel +=
|
|
98
|
+
expiryMs > Date.now() ? ` · expires in ${formatDuration(expiryMs - Date.now())}` : " · expired";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!redeemable && account.unavailableReason) {
|
|
102
|
+
countLabel += ` · ${oneLine(account.unavailableReason)}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
75
105
|
const countText = account.error
|
|
76
106
|
? theme.fg("error", countLabel)
|
|
77
107
|
: redeemable
|
|
78
108
|
? theme.fg("success", countLabel)
|
|
79
109
|
: theme.fg("dim", countLabel);
|
|
80
110
|
const activeTag = account.active ? theme.fg("muted", " (active)") : "";
|
|
111
|
+
const safeLabel = oneLine(account.label);
|
|
112
|
+
const providerTag = theme.fg(
|
|
113
|
+
"muted",
|
|
114
|
+
`[${oneLine(account.providerLabel)} · #${account.target.credentialId}] `,
|
|
115
|
+
);
|
|
81
116
|
if (isSelected) {
|
|
82
|
-
const name = redeemable ? theme.fg("accent",
|
|
83
|
-
rows.push(`${theme.fg("accent", `${theme.nav.cursor} `)}${name}${activeTag} ${countText}`);
|
|
117
|
+
const name = redeemable ? theme.fg("accent", safeLabel) : theme.fg("dim", safeLabel);
|
|
118
|
+
rows.push(`${theme.fg("accent", `${theme.nav.cursor} `)}${providerTag}${name}${activeTag} ${countText}`);
|
|
84
119
|
} else {
|
|
85
|
-
const name = redeemable ?
|
|
86
|
-
rows.push(
|
|
120
|
+
const name = redeemable ? safeLabel : theme.fg("dim", safeLabel);
|
|
121
|
+
rows.push(` ${providerTag}${name}${activeTag} ${countText}`);
|
|
87
122
|
}
|
|
88
123
|
}
|
|
89
124
|
|
|
@@ -100,20 +135,35 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
|
|
|
100
135
|
|
|
101
136
|
if (total === 0) {
|
|
102
137
|
this.#listContainer.addChild(
|
|
103
|
-
new TruncatedText(theme.fg("muted", "No
|
|
138
|
+
new TruncatedText(theme.fg("muted", "No provider accounts with saved resets"), 0, 0),
|
|
104
139
|
);
|
|
105
140
|
}
|
|
106
141
|
|
|
107
142
|
const pending = items.find(item => this.#menu.isPending(item));
|
|
108
143
|
const hint = pending
|
|
109
|
-
? theme.fg("warning",
|
|
144
|
+
? theme.fg("warning", oneLine(this.#confirmationMessage(pending)))
|
|
110
145
|
: theme.fg("muted", "↑/↓ select · ↵ spend a reset · Esc cancel");
|
|
111
|
-
this.#listContainer.addChild(new
|
|
146
|
+
this.#listContainer.addChild(new Text(hint, 0, 0));
|
|
112
147
|
|
|
113
148
|
if (this.#statusMessage) {
|
|
114
149
|
this.#listContainer.addChild(new Spacer(1));
|
|
115
|
-
this.#listContainer.addChild(new
|
|
150
|
+
this.#listContainer.addChild(new Text(theme.fg("warning", oneLine(this.#statusMessage)), 0, 0));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#confirmationMessage(account: ResetUsageAccount): string {
|
|
155
|
+
const subject = account.credit?.title ? `“${account.credit.title}”` : "1 saved reset";
|
|
156
|
+
const messages = [`Press Enter again to spend ${subject} for ${account.label} (${account.providerLabel}).`];
|
|
157
|
+
if (account.credit?.program === "juniper_tide") {
|
|
158
|
+
messages.push("This resets Claude's 5h session limit only; weekly limits stay unchanged.");
|
|
159
|
+
} else if (account.credit?.clears?.length) {
|
|
160
|
+
messages.push(`Covers ${account.credit.clears.map(formatUsageResetWindow).join(", ")}.`);
|
|
161
|
+
}
|
|
162
|
+
if (account.credit?.requiresLimit === false) {
|
|
163
|
+
messages.push("Optional early use: this can be spent before the covered limit is reached.");
|
|
116
164
|
}
|
|
165
|
+
messages.push("Esc cancels.");
|
|
166
|
+
return messages.join(" ");
|
|
117
167
|
}
|
|
118
168
|
|
|
119
169
|
handleInput(keyData: string): void {
|
|
@@ -150,8 +200,10 @@ export class ResetUsageSelectorComponent extends OverlayPanel {
|
|
|
150
200
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
151
201
|
const account = this.#menu.selectedItem;
|
|
152
202
|
if (!account) return;
|
|
153
|
-
if (account.
|
|
154
|
-
this.#statusMessage =
|
|
203
|
+
if (account.redeemableCount <= 0) {
|
|
204
|
+
this.#statusMessage = account.unavailableReason
|
|
205
|
+
? `That account's saved resets are unavailable: ${account.unavailableReason}`
|
|
206
|
+
: "That account has no saved resets usable right now.";
|
|
155
207
|
this.#updateList();
|
|
156
208
|
return;
|
|
157
209
|
}
|