@oh-my-pi/pi-tui 18.11.0 → 18.11.2
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 +52 -146
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +37 -0
- package/dist/types/native/blobs.d.ts +11 -11
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +24 -11
- package/dist/types/prompt/composer.d.ts +20 -0
- package/dist/types/prompt/custom-editor.d.ts +2 -0
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/status-line/context-usage.d.ts +3 -0
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/tools/eval.d.ts +5 -0
- package/dist/types/tools/grouped-file-output.d.ts +2 -0
- package/dist/types/tools/read.d.ts +5 -4
- package/dist/types/tui.d.ts +10 -0
- package/package.json +9 -9
- package/src/apps/git/git-tui.ts +27 -10
- package/src/apps/git/sidebar.ts +5 -1
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/assistant-message.ts +37 -12
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/image-loading.ts +4 -6
- package/src/chat/read-tool-group.ts +1 -0
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.ts +11 -4
- package/src/chat/user-message.ts +22 -15
- package/src/chrome/transcript-container.ts +10 -0
- package/src/components/editor.ts +12 -12
- package/src/components/image.ts +23 -6
- package/src/components/markdown.ts +375 -6
- package/src/components/settings-list.ts +2 -4
- package/src/native/blobs.ts +22 -6
- package/src/native/reconcile.ts +19 -15
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/btw-history-panel.ts +30 -23
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +107 -49
- package/src/overlays/settings-selector.ts +3 -0
- package/src/prompt/composer.ts +180 -23
- package/src/prompt/custom-editor.ts +19 -1
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/render/code-cell.ts +8 -1
- package/src/render/hyperlink.ts +5 -0
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/status-line/component.ts +22 -10
- package/src/status-line/context-usage.ts +22 -16
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/startup.ts +6 -6
- package/src/stdin-buffer.ts +11 -1
- package/src/terminal.ts +136 -42
- package/src/tools/ast-grep.ts +2 -2
- package/src/tools/edit.ts +16 -1
- package/src/tools/eval.ts +51 -11
- package/src/tools/grouped-file-output.ts +6 -1
- package/src/tools/read.ts +6 -5
- package/src/tools/write.ts +1 -1
- package/src/tui.ts +61 -30
- package/src/utils.ts +5 -3
|
@@ -198,6 +198,8 @@ export declare class CustomEditor extends Editor {
|
|
|
198
198
|
* item markers use the accent color so separate follow-ups remain visible while composing.
|
|
199
199
|
* Called once per layout segment, so whole-buffer inputs are computed once per text revision. */
|
|
200
200
|
decorateText: (text: string, context: EditorTextDecorationContext) => string;
|
|
201
|
+
/** Catch the typed word up with the caret first: its leaving the word (or a blur) re-describes the typo marks. */
|
|
202
|
+
describe(cx: DescribeContext): NativeNode;
|
|
201
203
|
/**
|
|
202
204
|
* TSP decorations: magic keywords shimmer (terminal-clocked) in the accent
|
|
203
205
|
* color, chips and markers take their token colors, misspellings are marked,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
2
3
|
/** Materialized image destination returned by the host blob writer. */
|
|
3
4
|
interface ImageBlobResult {
|
|
4
5
|
displayPath: string;
|
|
@@ -23,4 +24,25 @@ type ImageBlobWriterSync = (data: Buffer, options?: {
|
|
|
23
24
|
export declare function imageReferenceHyperlink(label: string, index: number, imageLinks: readonly (string | undefined)[] | undefined, renderLabel: (text: string) => string): string;
|
|
24
25
|
export declare function materializeImageReferenceLinks(images: readonly ImageContent[] | undefined, putBlob: ImageBlobWriter): Promise<(string | undefined)[] | undefined>;
|
|
25
26
|
export declare function materializeImageReferenceLinksSync(images: readonly ImageContent[] | undefined, putBlob: ImageBlobWriterSync): (string | undefined)[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Register the host's blob writer that backs links for images without a file of their
|
|
29
|
+
* own. Returns a disposer that unregisters it unless another writer replaced it since.
|
|
30
|
+
*/
|
|
31
|
+
export declare function setImageBlobWriter(next: ImageBlobWriterSync): () => void;
|
|
32
|
+
/**
|
|
33
|
+
* Whether transcript images carry links under the current `tui.hyperlinks` policy: a
|
|
34
|
+
* native surface honours only an explicit `off`, text mode the resolved OSC 8 support.
|
|
35
|
+
* Renderers caching a linked image key their cache on it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function imageLinksEnabled(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The file a transcript image opens: `sourcePath` when it was read from one, else a
|
|
40
|
+
* blob copy of its bytes written through the host. Undefined when links are disabled
|
|
41
|
+
* or no file exists, so a disabled policy never writes blobs.
|
|
42
|
+
*/
|
|
43
|
+
export declare function transcriptImageLink(image: Pick<ImageContent, "data" | "mimeType">, sourcePath?: string): string | undefined;
|
|
44
|
+
/** `text` (an image's placeholder) as an OSC 8 link to the image's file; plain when hyperlinks are off or no file exists. */
|
|
45
|
+
export declare function linkedImageText(text: string, image: Pick<ImageContent, "data" | "mimeType">, sourcePath?: string): string;
|
|
46
|
+
/** Native counterpart of {@link linkedImageText}: a span linking to the image's file when one exists. */
|
|
47
|
+
export declare function linkedImageSpan(text: string, image: Pick<ImageContent, "data" | "mimeType">, sourcePath: string | undefined, s?: string): TspSpan;
|
|
26
48
|
export {};
|
|
@@ -30,7 +30,8 @@ export declare class MacOSSpellingProvider implements EditorTextAssistProvider {
|
|
|
30
30
|
decorateTypos(text: string, context: SpellingDecorationContext, decorate?: (span: string) => string): string;
|
|
31
31
|
/**
|
|
32
32
|
* Misspelled prose ranges in `text` (offsets into it), in order and
|
|
33
|
-
* non-overlapping
|
|
33
|
+
* non-overlapping, except ones touching `context.typing` (the word still
|
|
34
|
+
* being typed). Schedules the verification check for uncached text; its
|
|
34
35
|
* result arrives through {@link onUpdate}.
|
|
35
36
|
*/
|
|
36
37
|
typoRanges(text: string, context: SpellingDecorationContext): readonly native.SpellingRange[];
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { TypingWord } from "./typing-word.js";
|
|
1
2
|
/** Logical source location for one rendered editor segment. */
|
|
2
3
|
export interface SpellingDecorationContext {
|
|
3
4
|
editorText: string;
|
|
4
5
|
lines: readonly string[];
|
|
5
6
|
line: number;
|
|
6
7
|
startCol: number;
|
|
8
|
+
/** The word being typed: typo marks touching it wait until the caret leaves it. */
|
|
9
|
+
typing?: TypingWord | undefined;
|
|
7
10
|
}
|
|
8
11
|
/**
|
|
9
12
|
* Whether `text[start, end)` is a prose word: unmasked by {@link maskNonProse},
|
|
@@ -10,7 +10,7 @@ export interface QueuedMessageGroup {
|
|
|
10
10
|
* Messages queued while the agent works, above the editor. ANSI: a heading
|
|
11
11
|
* per group, numbered dim rows and the dequeue hint. Natively (§8.1): a
|
|
12
12
|
* stack of pills, each `corner-down-right` · the text · `⌥↑ Edit`, the total
|
|
13
|
-
*
|
|
13
|
+
* (`N queued`, not a position) in the first pill when more than one waits; the edit control sends
|
|
14
14
|
* `queue.edit`, answered like the dequeue key.
|
|
15
15
|
*/
|
|
16
16
|
export declare class QueuedMessagesBand extends Container {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The word being typed in the composer. Misspelling marks hold off on it until
|
|
3
|
+
* the caret leaves it (a space or punctuation, a caret move, losing focus), the
|
|
4
|
+
* way Slack and Chromium mark a typo only once its word is done; marks on other
|
|
5
|
+
* words stay while typing.
|
|
6
|
+
*/
|
|
7
|
+
/** A word of one logical line, as UTF-16 columns `[start, end)`. */
|
|
8
|
+
export interface TypingWord {
|
|
9
|
+
line: number;
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
/** Follows an editor's text revision and caret to know which word is being typed. */
|
|
14
|
+
export declare class TypingWordTracker {
|
|
15
|
+
#private;
|
|
16
|
+
/** The word being typed, if any. */
|
|
17
|
+
get word(): TypingWord | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Catch up with the editor: an edit that leaves the caret in (or right after)
|
|
20
|
+
* a word starts typing it; a caret move out of that word, or losing focus,
|
|
21
|
+
* ends it. The first call only records the revision, so a restored draft is
|
|
22
|
+
* not held. Returns whether the held word changed.
|
|
23
|
+
*/
|
|
24
|
+
update(revision: number, lines: readonly string[], line: number, col: number, focused: boolean): boolean;
|
|
25
|
+
}
|
|
@@ -70,6 +70,8 @@ export declare function urlHyperlinkAlways(url: string, displayText: string): st
|
|
|
70
70
|
* Non-HTTP(S) or unparsable URLs yield a plain span.
|
|
71
71
|
*/
|
|
72
72
|
export declare function urlLinkSpan(target: string, displayText: string, s?: string): TspSpan;
|
|
73
|
+
/** Whether native link spans and nodes may carry an `href`: everything but an explicit `off` policy. */
|
|
74
|
+
export declare function isNativeLinkEnabled(): boolean;
|
|
73
75
|
/**
|
|
74
76
|
* A native link span to a filesystem path as a plain `file://` URI (the
|
|
75
77
|
* terminal opens it through the system; the location stays in the visible
|
|
@@ -32,7 +32,7 @@ export interface RenderCache {
|
|
|
32
32
|
lines: string[];
|
|
33
33
|
}
|
|
34
34
|
/** Build indentation and continuing branches for ancestor levels. */
|
|
35
|
-
export declare function buildTreePrefix(ancestors: boolean[], theme: Theme): string;
|
|
35
|
+
export declare function buildTreePrefix(ancestors: readonly boolean[], theme: Theme): string;
|
|
36
36
|
/** Return the branch glyph for a final or continuing tree item. */
|
|
37
37
|
export declare function getTreeBranch(isLast: boolean, theme: Theme): string;
|
|
38
38
|
/** Return the continuation prefix for subsequent lines of a tree item. */
|
|
@@ -98,6 +98,7 @@ export interface CompactionBoundaries {
|
|
|
98
98
|
export declare function computeCompactionBoundaries(compactionSettings: CompactionSettings, contextWindow: number, speculationLeadTokens?: number): CompactionBoundaries | null;
|
|
99
99
|
/** Stable inputs used to cache non-message token estimates. */
|
|
100
100
|
export interface NonMessageTokenSource {
|
|
101
|
+
readonly model?: Model;
|
|
101
102
|
readonly systemPrompt?: readonly string[];
|
|
102
103
|
readonly agent?: {
|
|
103
104
|
readonly state?: {
|
|
@@ -137,6 +138,8 @@ export declare function invalidateToolSchemaMetadata(tools: ToolSchemaSource): v
|
|
|
137
138
|
* {@link invalidateToolSchemaMetadata} when those inputs change.
|
|
138
139
|
*/
|
|
139
140
|
export declare function estimateToolSchemaTokens(tools: ToolSchemaSource, tokenizer: Tokenizer, sourceRevision?: number): number;
|
|
141
|
+
/** Count schemas carried on every request, excluding server-side deferred catalogs. */
|
|
142
|
+
export declare function estimateContextToolSchemaTokens(tools: ToolSchemaSource, tokenizer: Tokenizer, model: Model | undefined, sourceRevision?: number): number;
|
|
140
143
|
export declare function computeNonMessageTokens(session: NonMessageTokenSource, tokenizer: Tokenizer, sourceRevision?: number): number;
|
|
141
144
|
/**
|
|
142
145
|
* Shared helper for the four non-message token totals used by
|
|
@@ -97,11 +97,13 @@ export interface FooterSession {
|
|
|
97
97
|
autoResolvedThinkingLevel(): string | undefined;
|
|
98
98
|
getContextUsage: StatusLineSession["getContextUsage"];
|
|
99
99
|
modelRegistry: Pick<StatusLineSession["modelRegistry"], "isUsingOAuth">;
|
|
100
|
+
/**
|
|
101
|
+
* Cumulative session rollup. Unlike the active-window stats it spans every
|
|
102
|
+
* entry in the session, so compaction does not reset the counters, and it
|
|
103
|
+
* folds in usage carried by completed `task` tool results.
|
|
104
|
+
*/
|
|
100
105
|
sessionManager: {
|
|
101
|
-
|
|
102
|
-
type: string;
|
|
103
|
-
message?: AgentMessage;
|
|
104
|
-
}[];
|
|
106
|
+
getUsageStatistics: StatusLineSession["sessionManager"]["getUsageStatistics"];
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Status line for the speculative first frame, before any session exists.
|
|
3
3
|
*
|
|
4
|
-
* InteractiveMode persists {@link StatusLineStartupData} (settings plus the
|
|
5
|
-
*
|
|
6
|
-
* real {@link StatusLineComponent} as a fresh, unnamed session at
|
|
7
|
-
* terminal width. Path and git branch are read live; context usage is
|
|
8
|
-
* unknown so the gauge shows the window without a percent.
|
|
4
|
+
* InteractiveMode persists {@link StatusLineStartupData} (settings plus the
|
|
5
|
+
* resolved default role model and status inputs); the next launch renders it
|
|
6
|
+
* through the real {@link StatusLineComponent} as a fresh, unnamed session at
|
|
7
|
+
* the live terminal width. Path and git branch are read live; context usage is
|
|
8
|
+
* reported unknown so the gauge shows the window without a percent.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```ts ignore
|
|
@@ -22,7 +22,7 @@ import type { StatusLineSettings } from "./types.js";
|
|
|
22
22
|
export interface StatusLineStartupData {
|
|
23
23
|
readonly settings: StatusLineSettings;
|
|
24
24
|
readonly gitEnabled: boolean;
|
|
25
|
-
/** Model
|
|
25
|
+
/** Model resolved for the next fresh session's default role. */
|
|
26
26
|
readonly model?: Model;
|
|
27
27
|
readonly thinkingLevel?: ThinkingLevel;
|
|
28
28
|
readonly autoThinking: boolean;
|
|
@@ -61,6 +61,13 @@ export type StdinBufferEventMap = {
|
|
|
61
61
|
export declare class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
62
62
|
#private;
|
|
63
63
|
constructor(options?: StdinBufferOptions);
|
|
64
|
+
/**
|
|
65
|
+
* Whether a bracketed paste has started and its end marker has not arrived.
|
|
66
|
+
* Terminal skips its per-read DEC 2004 reassertion meanwhile: the host is
|
|
67
|
+
* evidently in bracketed-paste mode, and a paste delivered one key record
|
|
68
|
+
* per read would otherwise cost one terminal write per pasted key (#15757).
|
|
69
|
+
*/
|
|
70
|
+
get pasting(): boolean;
|
|
64
71
|
process(data: string | Buffer): void;
|
|
65
72
|
/**
|
|
66
73
|
* Gate unbracketed raw-paste classification on event-loop responsiveness.
|
package/dist/types/terminal.d.ts
CHANGED
|
@@ -117,7 +117,7 @@ export type GlyphProtocolReportHandler = (supported: boolean) => void;
|
|
|
117
117
|
/**
|
|
118
118
|
* Outcome of the Tern Surface Protocol `hello` probe: the terminal's reply,
|
|
119
119
|
* or null when it answered the DA1 sentinel first (no TSP) or the probe was
|
|
120
|
-
* skipped (`PI_TUI_NATIVE=0`, multiplexers).
|
|
120
|
+
* skipped (`PI_TUI_NATIVE=0`, multiplexers, devenv's reloading shell).
|
|
121
121
|
*/
|
|
122
122
|
export type TspHelloHandler = (hello: TspHello | null) => void;
|
|
123
123
|
/**
|
|
@@ -181,6 +181,7 @@ export interface Terminal {
|
|
|
181
181
|
get kittyEnableSequence(): string | null;
|
|
182
182
|
readonly keyboardEnhancementEnterSequence?: string | null;
|
|
183
183
|
readonly keyboardEnhancementExitSequence?: string | null;
|
|
184
|
+
readonly keyboardEnhancementRestoreSequence?: string | null;
|
|
184
185
|
moveBy(lines: number): void;
|
|
185
186
|
hideCursor(force?: boolean): void;
|
|
186
187
|
showCursor(force?: boolean): void;
|
|
@@ -283,9 +284,11 @@ export interface ProcessTerminalOptions {
|
|
|
283
284
|
conpty?: boolean;
|
|
284
285
|
/**
|
|
285
286
|
* Whether stdin is a native Windows console handle (win32, not WSL), whose
|
|
286
|
-
* console host
|
|
287
|
-
*
|
|
288
|
-
*
|
|
287
|
+
* console host is certain to answer `CSI ? 9001 h` with win32-input-mode key
|
|
288
|
+
* records. When false (WSL), the keyboard fallback requests modifyOtherKeys
|
|
289
|
+
* alongside win32-input-mode, since a WSL pty may not be relayed by a console
|
|
290
|
+
* host. Defaults to `process.platform === "win32"`; only consulted when
|
|
291
|
+
* `conpty` is also true.
|
|
289
292
|
*/
|
|
290
293
|
nativeWindowsConsole?: boolean;
|
|
291
294
|
}
|
|
@@ -298,6 +301,7 @@ export declare class ProcessTerminal implements Terminal {
|
|
|
298
301
|
get kittyProtocolActive(): boolean;
|
|
299
302
|
get kittyEnableSequence(): string | null;
|
|
300
303
|
get keyboardEnhancementEnterSequence(): string | null;
|
|
304
|
+
get keyboardEnhancementRestoreSequence(): string | null;
|
|
301
305
|
get keyboardEnhancementExitSequence(): string | null;
|
|
302
306
|
get appearance(): TerminalAppearance | undefined;
|
|
303
307
|
onAppearanceChange(callback: (appearance: TerminalAppearance, requestToken?: TerminalAppearanceRequestToken) => void): void;
|
|
@@ -20,6 +20,11 @@ export interface EvalCellResult {
|
|
|
20
20
|
code: string;
|
|
21
21
|
language?: EvalLanguage;
|
|
22
22
|
output: string;
|
|
23
|
+
/**
|
|
24
|
+
* `output` without its `display[N]:` JSON sections, which renderers show
|
|
25
|
+
* from `jsonOutputs` instead. Absent when the cell displayed no JSON.
|
|
26
|
+
*/
|
|
27
|
+
textOutput?: string;
|
|
23
28
|
status: "pending" | "running" | "complete" | "error";
|
|
24
29
|
durationMs?: number;
|
|
25
30
|
exitCode?: number;
|
|
@@ -111,9 +111,11 @@ export interface NumberedLine {
|
|
|
111
111
|
* of consecutive line numbers (`start` = the run's first line, match lines
|
|
112
112
|
* `mark`ed), with a `…` row wherever the numbers jump or an elision marker
|
|
113
113
|
* (`n: null`) sits. No path header: the row or head above names the file.
|
|
114
|
+
* `wrap` soft-wraps long lines instead of scrolling the block sideways.
|
|
114
115
|
*/
|
|
115
116
|
export declare function numberedCode(lines: readonly NumberedLine[], opts?: {
|
|
116
117
|
lang?: string;
|
|
118
|
+
wrap?: boolean;
|
|
117
119
|
}): NativeNode[];
|
|
118
120
|
/**
|
|
119
121
|
* Describe grouped search output (grep / ast-grep / ast-edit display content)
|
|
@@ -122,10 +122,11 @@ export declare function readSourceFsPath(details: ReadToolDetails | undefined):
|
|
|
122
122
|
*/
|
|
123
123
|
export declare function readResultIsMarkdown(details: ReadToolDetails | undefined, args: ReadRenderArgs | undefined): boolean;
|
|
124
124
|
/**
|
|
125
|
-
* Read content as numbered `code` (no path header: the head names
|
|
126
|
-
*
|
|
127
|
-
* `null` for an elided
|
|
128
|
-
* every line, else count
|
|
125
|
+
* Read content as numbered, wrapped `code` (no path header: the head names
|
|
126
|
+
* the file), matching the ANSI preview's hanging wrap. Numbers come from
|
|
127
|
+
* `lineNumbers` (`displayContent`'s per-line numbers, `null` for an elided
|
|
128
|
+
* block; runs split with `…` between) when they cover every line, else count
|
|
129
|
+
* up from `start`.
|
|
129
130
|
*/
|
|
130
131
|
export declare function readContentCode(content: string, start: number, numbers: readonly (number | null)[] | undefined, lang: string | undefined): NativeNode[];
|
|
131
132
|
/** Render file, image, and URL reads in the transcript. */
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -348,6 +348,7 @@ export declare class TUI extends Container {
|
|
|
348
348
|
overlayStack: {
|
|
349
349
|
component: Component;
|
|
350
350
|
options?: OverlayOptions;
|
|
351
|
+
/** Focus restored after closing; tracks live focus changes beneath an input-less key holder. */
|
|
351
352
|
preFocus: Component | null;
|
|
352
353
|
hidden: boolean;
|
|
353
354
|
/**
|
|
@@ -496,6 +497,13 @@ export declare class TUI extends Container {
|
|
|
496
497
|
* overtaking them. No-op unless `previous` owns the held keys.
|
|
497
498
|
*/
|
|
498
499
|
replaceHeldFocus(previous: Component, next: Component): void;
|
|
500
|
+
/**
|
|
501
|
+
* Register a hook that re-enables terminal modes on every start(). A hook
|
|
502
|
+
* added while the TUI is already running (a prepaint composer starts it
|
|
503
|
+
* before the interactive mode wires its controllers) missed that start, so
|
|
504
|
+
* it runs immediately instead of waiting for a stop/start cycle that may
|
|
505
|
+
* never come.
|
|
506
|
+
*/
|
|
499
507
|
addStartListener(listener: StartListener): () => void;
|
|
500
508
|
addInputListener(listener: InputListener): () => void;
|
|
501
509
|
removeInputListener(listener: InputListener): void;
|
|
@@ -508,6 +516,8 @@ export declare class TUI extends Container {
|
|
|
508
516
|
*/
|
|
509
517
|
resetDisplay(): void;
|
|
510
518
|
requestRender(force?: boolean, options?: RenderRequestOptions): void;
|
|
519
|
+
/** Run `callback` after `delayMs` on the render scheduler's clock, unless the TUI has stopped. */
|
|
520
|
+
scheduleAfter(delayMs: number, callback: () => void): RenderTimer;
|
|
511
521
|
/**
|
|
512
522
|
* Paint a forced frame synchronously when startup must hand off an already
|
|
513
523
|
* visible component tree before further async initialization. Same as
|
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.11.
|
|
4
|
+
"version": "18.11.2",
|
|
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.11.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.11.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.11.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.11.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.11.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.11.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.11.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.11.
|
|
44
|
+
"@oh-my-pi/omptype": "18.11.2",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.11.2",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.11.2",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.11.2",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.11.2",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.11.2",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.11.2",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.11.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/apps/git/git-tui.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* scrollbar, right sidebar (file management + commit form while dirty, HEAD
|
|
8
8
|
* commit details with author avatar when clean). Submitting an empty commit
|
|
9
9
|
* form generates an llm-git-compatible message; submitting populated fields
|
|
10
|
-
* commits them. A footer shows key hints
|
|
10
|
+
* commits them. A footer shows key hints; the staging and discarding ones are
|
|
11
|
+
* omitted while the tree is clean, where they cannot fire.
|
|
11
12
|
*
|
|
12
13
|
* `tab` moves focus between the diff and the sidebar; both panes take
|
|
13
14
|
* arrows/PgUp/PgDn, vim motions (`j`/`k`/`h`/`l`/`g`/`G`), and mouse
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
* refreshes. In the sidebar tree `←`/`→` collapse/expand directories,
|
|
23
24
|
* `enter` opens the selected file in the diff pane, `space` stages or
|
|
24
25
|
* unstages the selected row, and `delete` discards it — on a directory,
|
|
25
|
-
* every file underneath it.
|
|
26
|
+
* every file underneath it. All of the staging and discarding keys are inert
|
|
27
|
+
* on a clean tree, which shows the HEAD commit instead of a change list.
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
@@ -89,6 +91,28 @@ const STATUS_TTL_MS = 6_000;
|
|
|
89
91
|
|
|
90
92
|
type Focus = "diff" | "sidebar";
|
|
91
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Footer key hints. A clean tree renders the HEAD commit view, where stage
|
|
96
|
+
* and discard cannot fire: `#patchTargetFor` returns null and
|
|
97
|
+
* `#discardCurrentFile` returns early for `area: "commit"` rows, and the
|
|
98
|
+
* sidebar only builds stage/discard actions for the unstaged/staged
|
|
99
|
+
* sections. `commit` needs the commit form (absent on a clean tree) and
|
|
100
|
+
* `select` feeds the line-level stage/discard actions (no-ops on commit
|
|
101
|
+
* rows), so the commit view drops those too and says what it is showing.
|
|
102
|
+
*/
|
|
103
|
+
export function gitFooterHints(clean: boolean, focus: Focus): string {
|
|
104
|
+
const edit = clean
|
|
105
|
+
? ""
|
|
106
|
+
: focus === "diff"
|
|
107
|
+
? ` · ${formatKeyHints(["s", "u"])} stage · ${formatKeyHints(["x", "delete"])} discard`
|
|
108
|
+
: ` · ${formatKeyHint("space")} stage · ${formatKeyHint("delete")} discard`;
|
|
109
|
+
const commit = clean ? "" : ` · ${formatKeyHint("c")} commit`;
|
|
110
|
+
const select = clean || focus !== "diff" ? "" : ` · ${formatKeyHints(["shift+up", "shift+down"])} select`;
|
|
111
|
+
return focus === "diff"
|
|
112
|
+
? `${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHints(["]", "["])} file${select}${edit} · ${formatKeyHint("v")} view${commit} · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`
|
|
113
|
+
: `${formatKeyHints(["up", "down"])} move · ${formatKeyHints(["left", "right"])} fold${edit} · ${formatKeyHint("enter")} open · ${formatKeyHints(["alt+down", "alt+up"])} hunk${commit} · ${formatKeyHint("t")} tree · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`;
|
|
114
|
+
}
|
|
115
|
+
|
|
92
116
|
/** Tone of a status message: its colour in the header and its span token natively. */
|
|
93
117
|
type StatusTone = "success" | "warning" | "error" | "accent" | "dim";
|
|
94
118
|
|
|
@@ -793,14 +817,7 @@ class GitTuiComponent implements Component {
|
|
|
793
817
|
|
|
794
818
|
// The empty middle carries the key hints (or a fresh status message).
|
|
795
819
|
const status = this.#visibleStatus() ? theme.fg(this.#statusTone, this.#status) : "";
|
|
796
|
-
const middle =
|
|
797
|
-
status ||
|
|
798
|
-
theme.fg(
|
|
799
|
-
"dim",
|
|
800
|
-
this.#focus === "diff"
|
|
801
|
-
? `${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHints(["]", "["])} file · ${formatKeyHints(["shift+up", "shift+down"])} select · ${formatKeyHints(["s", "u"])} stage · ${formatKeyHints(["x", "delete"])} discard · ${formatKeyHint("v")} view · ${formatKeyHint("c")} commit · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`
|
|
802
|
-
: `${formatKeyHints(["up", "down"])} move · ${formatKeyHints(["left", "right"])} fold · ${formatKeyHint("space")} stage · ${formatKeyHint("delete")} discard · ${formatKeyHint("enter")} open · ${formatKeyHints(["alt+down", "alt+up"])} hunk · ${formatKeyHint("c")} commit · ${formatKeyHint("t")} tree · ${formatKeyHint("?")} keys · ${formatKeyHint("q")} quit`,
|
|
803
|
-
);
|
|
820
|
+
const middle = status || theme.fg("dim", gitFooterHints(this.#model.clean, this.#focus));
|
|
804
821
|
const free = width - row.width - right.width - 1;
|
|
805
822
|
const middleText = free > visibleWidth(middle) + 4 ? middle : truncateToWidth(middle, Math.max(0, free - 4));
|
|
806
823
|
const leftPad = Math.max(1, Math.floor((free - visibleWidth(middleText)) / 2));
|
package/src/apps/git/sidebar.ts
CHANGED
|
@@ -1667,6 +1667,9 @@ export class Sidebar {
|
|
|
1667
1667
|
text: ` ${theme.fg("dim", "parent:")} ${theme.fg("accent", head.parents.map(sha => sha.slice(0, 8)).join(" "))}`,
|
|
1668
1668
|
});
|
|
1669
1669
|
}
|
|
1670
|
+
// This view only appears when there is nothing to stage or discard, so say so
|
|
1671
|
+
// rather than letting the file list read as pending work.
|
|
1672
|
+
rows.push({ text: theme.fg("success", ` Working tree clean — showing committed ${head.shortSha}`) });
|
|
1670
1673
|
rows.push({ text: theme.fg("borderMuted", "─".repeat(Math.max(0, width))) });
|
|
1671
1674
|
if (!head.filesLoaded) {
|
|
1672
1675
|
rows.push({ text: theme.fg("dim", " Loading changed files…") });
|
|
@@ -1675,8 +1678,9 @@ export class Sidebar {
|
|
|
1675
1678
|
|
|
1676
1679
|
const additions = head.files.reduce((sum, file) => sum + (file.additions ?? 0), 0);
|
|
1677
1680
|
const deletions = head.files.reduce((sum, file) => sum + (file.deletions ?? 0), 0);
|
|
1681
|
+
const count = `${head.files.length} file${head.files.length === 1 ? "" : "s"}`;
|
|
1678
1682
|
rows.push({
|
|
1679
|
-
text: ` ${theme.bold(
|
|
1683
|
+
text: ` ${theme.bold(count)} ${theme.fg("dim", `in ${head.shortSha}`)} ${theme.fg("success", `+${additions}`)} ${theme.fg("error", `−${deletions}`)}`,
|
|
1680
1684
|
});
|
|
1681
1685
|
rows.push(this.#viewToggleRow(width));
|
|
1682
1686
|
rows.push(...this.#entryRows(head.files, "commit"));
|
package/src/autocomplete.ts
CHANGED
|
@@ -14,6 +14,11 @@ const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
|
14
14
|
*/
|
|
15
15
|
const AT_PARTIAL_DELAY_MS = 150;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Without a deadline the native walk follows symlinks at unbounded depth and never returns.
|
|
19
|
+
*/
|
|
20
|
+
const AT_FUZZY_TIMEOUT_MS = 1_500;
|
|
21
|
+
|
|
17
22
|
function buildAutocompleteFuzzyDiscoveryProfile(
|
|
18
23
|
query: string,
|
|
19
24
|
basePath: string,
|
|
@@ -25,6 +30,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
|
|
|
25
30
|
hidden: boolean;
|
|
26
31
|
gitignore: boolean;
|
|
27
32
|
cache: boolean;
|
|
33
|
+
timeoutMs: number;
|
|
28
34
|
signal?: AbortSignal;
|
|
29
35
|
} {
|
|
30
36
|
return {
|
|
@@ -34,6 +40,7 @@ function buildAutocompleteFuzzyDiscoveryProfile(
|
|
|
34
40
|
hidden: true,
|
|
35
41
|
gitignore: true,
|
|
36
42
|
cache: true,
|
|
43
|
+
timeoutMs: AT_FUZZY_TIMEOUT_MS,
|
|
37
44
|
...(signal ? { signal } : {}),
|
|
38
45
|
};
|
|
39
46
|
}
|
|
@@ -106,6 +113,18 @@ function extractQuotedPrefix(text: string): string | null {
|
|
|
106
113
|
return text.slice(quoteStart);
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
/** Extract the live whitespace-delimited or double-quoted `@` file token ending at the cursor. */
|
|
117
|
+
export function extractAtFilePrefix(text: string): string | null {
|
|
118
|
+
const quotedPrefix = extractQuotedPrefix(text);
|
|
119
|
+
if (quotedPrefix?.startsWith('@"')) {
|
|
120
|
+
return quotedPrefix;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lastDelimiterIndex = findLastDelimiter(text);
|
|
124
|
+
const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
|
|
125
|
+
return text[tokenStart] === "@" ? text.slice(tokenStart) : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
109
128
|
function parsePathPrefix(prefix: string): { rawPrefix: string; isAtPrefix: boolean; isQuotedPrefix: boolean } {
|
|
110
129
|
if (prefix.startsWith('@"')) {
|
|
111
130
|
return { rawPrefix: prefix.slice(2), isAtPrefix: true, isQuotedPrefix: true };
|
|
@@ -719,7 +738,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
719
738
|
}
|
|
720
739
|
|
|
721
740
|
// Check for @ file reference (fuzzy search) - must be after a delimiter or at start
|
|
722
|
-
const atPrefix =
|
|
741
|
+
const atPrefix = extractAtFilePrefix(textBeforeCursor);
|
|
723
742
|
if (atPrefix) {
|
|
724
743
|
const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
|
|
725
744
|
// Recursive fuzzy walks rooted outside the project (e.g. `@../`,
|
|
@@ -862,7 +881,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
862
881
|
|
|
863
882
|
// Check if we're completing a file attachment (prefix starts with "@")
|
|
864
883
|
if (prefix.startsWith("@")) {
|
|
865
|
-
const liveAtPrefix =
|
|
884
|
+
const liveAtPrefix = extractAtFilePrefix(textBeforeCursor);
|
|
866
885
|
if (liveAtPrefix) {
|
|
867
886
|
beforePrefix = currentLine.slice(0, cursorCol - liveAtPrefix.length);
|
|
868
887
|
}
|
|
@@ -896,23 +915,6 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
896
915
|
};
|
|
897
916
|
}
|
|
898
917
|
|
|
899
|
-
// Extract @ prefix for fuzzy file suggestions
|
|
900
|
-
#extractAtPrefix(text: string): string | null {
|
|
901
|
-
const quotedPrefix = extractQuotedPrefix(text);
|
|
902
|
-
if (quotedPrefix?.startsWith('@"')) {
|
|
903
|
-
return quotedPrefix;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const lastDelimiterIndex = findLastDelimiter(text);
|
|
907
|
-
const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
|
|
908
|
-
|
|
909
|
-
if (text[tokenStart] === "@") {
|
|
910
|
-
return text.slice(tokenStart);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
918
|
// Extract a path-like prefix from the text before cursor
|
|
917
919
|
#extractPathPrefix(text: string, forceExtract: boolean = false): string | null {
|
|
918
920
|
const quotedPrefix = extractQuotedPrefix(text);
|
package/src/bracketed-paste.ts
CHANGED
|
@@ -68,6 +68,7 @@ const DEFAULT_BYTE_LIMIT = 64 * 1024 * 1024;
|
|
|
68
68
|
*/
|
|
69
69
|
export class BracketedPasteHandler {
|
|
70
70
|
#buffer = "";
|
|
71
|
+
#bufferBytes = 0;
|
|
71
72
|
#active = false;
|
|
72
73
|
readonly #byteLimit: number;
|
|
73
74
|
|
|
@@ -88,7 +89,10 @@ export class BracketedPasteHandler {
|
|
|
88
89
|
const start = data.indexOf(PASTE_START);
|
|
89
90
|
const end = data.indexOf(PASTE_END);
|
|
90
91
|
if (start !== -1 && (end === -1 || start < end)) {
|
|
91
|
-
if (!this.#active)
|
|
92
|
+
if (!this.#active) {
|
|
93
|
+
this.#buffer = "";
|
|
94
|
+
this.#bufferBytes = 0;
|
|
95
|
+
}
|
|
92
96
|
this.#active = true;
|
|
93
97
|
// Only strip nested starts before this paste's first end. Later starts
|
|
94
98
|
// belong to the next paste in `remaining`.
|
|
@@ -100,6 +104,7 @@ export class BracketedPasteHandler {
|
|
|
100
104
|
|
|
101
105
|
if (!this.#active) return { handled: false };
|
|
102
106
|
|
|
107
|
+
this.#bufferBytes += Buffer.byteLength(data, "utf8");
|
|
103
108
|
this.#buffer += data;
|
|
104
109
|
|
|
105
110
|
const endIndex = this.#buffer.indexOf(PASTE_END);
|
|
@@ -108,6 +113,7 @@ export class BracketedPasteHandler {
|
|
|
108
113
|
const remaining = this.#buffer.substring(endIndex + PASTE_END.length);
|
|
109
114
|
|
|
110
115
|
this.#buffer = "";
|
|
116
|
+
this.#bufferBytes = 0;
|
|
111
117
|
this.#active = false;
|
|
112
118
|
|
|
113
119
|
return { handled: true, pasteContent, remaining };
|
|
@@ -118,9 +124,10 @@ export class BracketedPasteHandler {
|
|
|
118
124
|
// neither lost nor held forever, and reset paste mode so subsequent
|
|
119
125
|
// input recovers. See `StdinBuffer#abortPaste` for the sibling recovery
|
|
120
126
|
// semantics inside `StdinBuffer`.
|
|
121
|
-
if (this.#
|
|
127
|
+
if (this.#bufferBytes > this.#byteLimit) {
|
|
122
128
|
const pasteContent = this.#buffer;
|
|
123
129
|
this.#buffer = "";
|
|
130
|
+
this.#bufferBytes = 0;
|
|
124
131
|
this.#active = false;
|
|
125
132
|
return { handled: true, pasteContent, remaining: "" };
|
|
126
133
|
}
|