@oh-my-pi/pi-tui 18.0.9 → 18.0.11
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 +18 -0
- package/dist/types/components/composer/band.d.ts +2 -0
- package/dist/types/components/composer/index.d.ts +1 -0
- package/dist/types/components/composer/registry.d.ts +7 -0
- package/dist/types/components/composer/types.d.ts +9 -3
- package/dist/types/components/editor.d.ts +2 -0
- package/dist/types/components/loader.d.ts +6 -0
- package/dist/types/terminal-capabilities.d.ts +9 -0
- package/package.json +3 -3
- package/src/components/composer/band.ts +42 -0
- package/src/components/composer/field.ts +1 -0
- package/src/components/composer/index.ts +1 -0
- package/src/components/composer/rail.ts +1 -0
- package/src/components/composer/registry.ts +12 -0
- package/src/components/composer/types.ts +18 -3
- package/src/components/editor.ts +35 -11
- package/src/components/loader.ts +19 -2
- package/src/components/markdown.ts +5 -5
- package/src/terminal-capabilities.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.0.11] - 2026-08-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `setTerminalHyperlinks()` to let hosts control OSC 8 hyperlink behavior in rendered Markdown links.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed inline color swatches appearing for words with hex-like prefixes, such as `#each`; swatches now appear only when the entire word is a valid color.
|
|
14
|
+
|
|
15
|
+
## [18.0.10] - 2026-08-28
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Press the Right Arrow at the end of a line to accept autocomplete suggestions, inline ghost-text completions, and spelling corrections, just like Tab.
|
|
20
|
+
- Added a smooth brand-color transition and an elapsed turn timer to the status line while the agent is working.
|
|
21
|
+
- Added a full-width “band” composer style for flush status lines.
|
|
22
|
+
|
|
5
23
|
## [18.0.9] - 2026-08-28
|
|
6
24
|
|
|
7
25
|
### Added
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { ComposerStyle, EditorBorderStyle } from "./types.js";
|
|
2
2
|
/** Whether an id names a composer style shipped by pi-tui. */
|
|
3
3
|
export declare function isBuiltinComposerStyle(id: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Whether a style paints its own row foreground.
|
|
6
|
+
*
|
|
7
|
+
* Extensions registered before `filledSurface` existed received undecorated
|
|
8
|
+
* row text, so an omitted flag remains filled for extension-owned ids.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isFilledComposerStyle(style: ComposerStyle): boolean;
|
|
4
11
|
/**
|
|
5
12
|
* Register one extension-owned composer style for this process.
|
|
6
13
|
*
|
|
@@ -10,7 +10,7 @@ import type { SymbolTheme } from "../../symbols.js";
|
|
|
10
10
|
/** Box-drawing glyph set used for composer chrome (the theme's `boxRound`). */
|
|
11
11
|
export type ComposerBox = SymbolTheme["boxRound"];
|
|
12
12
|
/** Built-in composer shape identifiers shipped by pi-tui. */
|
|
13
|
-
export declare const BUILTIN_EDITOR_BORDER_STYLES: readonly ["box", "claude", "pi", "borderless", "rule", "field", "rail"];
|
|
13
|
+
export declare const BUILTIN_EDITOR_BORDER_STYLES: readonly ["box", "band", "claude", "pi", "borderless", "rule", "field", "rail"];
|
|
14
14
|
/** Identifier for a built-in composer shape. */
|
|
15
15
|
export type BuiltinEditorBorderStyle = (typeof BUILTIN_EDITOR_BORDER_STYLES)[number];
|
|
16
16
|
/** Composer shape identifier; extensions may register additional strings. */
|
|
@@ -60,14 +60,20 @@ export interface ComposerRowContext extends ComposerChromeContext {
|
|
|
60
60
|
}
|
|
61
61
|
export interface ComposerStyle {
|
|
62
62
|
readonly id: EditorBorderStyle;
|
|
63
|
+
/**
|
|
64
|
+
* True when rows paint their own foreground through `surfaceColor`.
|
|
65
|
+
* Built-ins default to transparent; registered extensions that omit this
|
|
66
|
+
* field retain the pre-field behavior and receive undecorated row text.
|
|
67
|
+
*/
|
|
68
|
+
readonly filledSurface?: boolean;
|
|
63
69
|
/** Content rows carry left/right border glyphs; drives the cursor-reserve
|
|
64
70
|
* column, IME-safe layout, and the right-border scrollbar. */
|
|
65
71
|
readonly sideBorders: boolean;
|
|
66
72
|
/** Rows consumed by top+bottom chrome (drives maxHeight budgeting). */
|
|
67
73
|
readonly verticalChrome: 0 | 1 | 2;
|
|
68
74
|
/** Where the host should attach the status bar: embedded in the top border,
|
|
69
|
-
* docked onto a top rule, or detached into a standalone bottom bar. */
|
|
70
|
-
readonly statusAttachment: "top-border" | "top-rule-chip" | "none";
|
|
75
|
+
* rendered as a flush soft-capped band above the input, docked onto a top rule, or detached into a standalone bottom bar. */
|
|
76
|
+
readonly statusAttachment: "top-border" | "top-band" | "top-rule-chip" | "none";
|
|
71
77
|
/** Which segment groups the standalone bottom status bar shows. */
|
|
72
78
|
readonly bottomBar: "none" | "left" | "full";
|
|
73
79
|
/** Insert a blank spacer row between the editor and the standalone bottom
|
|
@@ -10,6 +10,8 @@ export interface EditorTheme {
|
|
|
10
10
|
accentColor?: (str: string) => string;
|
|
11
11
|
/** Background fill used by filled composer styles. */
|
|
12
12
|
surfaceColor?: (str: string) => string;
|
|
13
|
+
/** Foreground used when the composer shape leaves its text surface transparent. */
|
|
14
|
+
textColor?: (str: string) => string;
|
|
13
15
|
selectList: SelectListTheme;
|
|
14
16
|
symbols: SymbolTheme;
|
|
15
17
|
editorPaddingX?: number;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { TUI } from "../tui.js";
|
|
2
2
|
import { Text } from "./text.js";
|
|
3
|
+
/** Milliseconds between spinner-frame advances; exported so time-derived spinners elsewhere tick at the Loader cadence. */
|
|
4
|
+
export declare const SPINNER_ADVANCE_MS = 80;
|
|
3
5
|
type ColorFn = (str: string) => string;
|
|
4
6
|
/**
|
|
5
7
|
* Styles Loader message fragments without changing their visible text or width.
|
|
@@ -22,6 +24,10 @@ export declare class Loader extends Text {
|
|
|
22
24
|
stop(): void;
|
|
23
25
|
/** Lifecycle teardown: stop the animation timer. Idempotent. */
|
|
24
26
|
dispose(): void;
|
|
27
|
+
/** Install a lazy right-docked suffix for the spinner row (e.g. a styled
|
|
28
|
+
* session title). Re-evaluated every paint; dropped when the row leaves
|
|
29
|
+
* less than a two-cell gap. */
|
|
30
|
+
setTrailer(trailer: (() => string | undefined) | undefined): void;
|
|
25
31
|
setMessage(message: string): void;
|
|
26
32
|
}
|
|
27
33
|
export {};
|
|
@@ -206,6 +206,15 @@ export declare function setTerminalScreenToScrollback(enabled: boolean): void;
|
|
|
206
206
|
* capability); tests flip it directly to exercise the scaled-heading path.
|
|
207
207
|
*/
|
|
208
208
|
export declare function setTerminalTextSizing(enabled: boolean): void;
|
|
209
|
+
/**
|
|
210
|
+
* Override OSC 8 hyperlink capability at runtime. The coding-agent calls this
|
|
211
|
+
* from the `tui.hyperlinks` setting so its resolved policy (`off`/`auto`/`always`)
|
|
212
|
+
* drives every renderer that gates on {@link TERMINAL}`.hyperlinks` — notably the
|
|
213
|
+
* Markdown component's `[text](url)`/bare-URL links — consistently with the
|
|
214
|
+
* path/resource links that already consult the setting directly. Tests flip it
|
|
215
|
+
* to exercise the OSC 8 and plain-text paths deterministically.
|
|
216
|
+
*/
|
|
217
|
+
export declare function setTerminalHyperlinks(enabled: boolean): void;
|
|
209
218
|
export declare function getTerminalInfo(terminalId: TerminalId, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): TerminalInfo;
|
|
210
219
|
export interface CellDimensions {
|
|
211
220
|
widthPx: number;
|
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.0.
|
|
4
|
+
"version": "18.0.11",
|
|
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,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.0.11",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.0.11"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft status band composer (the rust omp default): the full status line sits
|
|
3
|
+
* flush against the terminal's left edge as a filled powerline band with a
|
|
4
|
+
* soft opening cap — no frame, rules, or corners — above an unboxed prompt
|
|
5
|
+
* anchored by a single curved `╰─ ` cue.
|
|
6
|
+
*/
|
|
7
|
+
import { truncateToWidth } from "../../utils";
|
|
8
|
+
import type { ComposerChromeContext, ComposerRowContext, ComposerStyle } from "./types";
|
|
9
|
+
|
|
10
|
+
export const bandComposerStyle: ComposerStyle = {
|
|
11
|
+
id: "band",
|
|
12
|
+
sideBorders: false,
|
|
13
|
+
verticalChrome: 1,
|
|
14
|
+
statusAttachment: "top-band",
|
|
15
|
+
bottomBar: "none",
|
|
16
|
+
bottomBarGap: false,
|
|
17
|
+
defaultPromptGutter: "╰─ ",
|
|
18
|
+
|
|
19
|
+
defaultPaddingX(): number {
|
|
20
|
+
return 0;
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
sideChromeWidth(): number {
|
|
24
|
+
return 0;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
renderTop(ctx: ComposerChromeContext): string | undefined {
|
|
28
|
+
const { topBorder, width } = ctx;
|
|
29
|
+
if (!topBorder?.content) return undefined;
|
|
30
|
+
// The band builder already sizes its groups + gauge to the full width;
|
|
31
|
+
// truncation only guards against a stale provider during resize.
|
|
32
|
+
return topBorder.width > width ? truncateToWidth(topBorder.content, width) : topBorder.content;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
renderRow(ctx: ComposerRowContext): string[] {
|
|
36
|
+
return [(ctx.gutter ? ctx.borderColor(ctx.gutter) : "") + ctx.text + ctx.pad];
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
renderBottom(): undefined {
|
|
40
|
+
return undefined;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -10,6 +10,7 @@ const ACCENT_RAIL = "▎";
|
|
|
10
10
|
/** Filled composer surface anchored by a single left accent rail. */
|
|
11
11
|
export const railComposerStyle: ComposerStyle = {
|
|
12
12
|
id: "rail",
|
|
13
|
+
filledSurface: true,
|
|
13
14
|
sideBorders: true,
|
|
14
15
|
verticalChrome: 0,
|
|
15
16
|
statusAttachment: "none",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bandComposerStyle } from "./band";
|
|
1
2
|
import { borderlessComposerStyle } from "./borderless";
|
|
2
3
|
import { boxComposerStyle } from "./box";
|
|
3
4
|
import { claudeComposerStyle } from "./claude";
|
|
@@ -9,6 +10,7 @@ import type { ComposerStyle, EditorBorderStyle } from "./types";
|
|
|
9
10
|
|
|
10
11
|
const BUILTIN_COMPOSER_STYLES: Readonly<Record<string, ComposerStyle>> = {
|
|
11
12
|
box: boxComposerStyle,
|
|
13
|
+
band: bandComposerStyle,
|
|
12
14
|
claude: claudeComposerStyle,
|
|
13
15
|
pi: piComposerStyle,
|
|
14
16
|
borderless: borderlessComposerStyle,
|
|
@@ -23,6 +25,16 @@ export function isBuiltinComposerStyle(id: string): boolean {
|
|
|
23
25
|
return Object.hasOwn(BUILTIN_COMPOSER_STYLES, id);
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Whether a style paints its own row foreground.
|
|
30
|
+
*
|
|
31
|
+
* Extensions registered before `filledSurface` existed received undecorated
|
|
32
|
+
* row text, so an omitted flag remains filled for extension-owned ids.
|
|
33
|
+
*/
|
|
34
|
+
export function isFilledComposerStyle(style: ComposerStyle): boolean {
|
|
35
|
+
return style.filledSurface ?? !isBuiltinComposerStyle(style.id);
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
/**
|
|
27
39
|
* Register one extension-owned composer style for this process.
|
|
28
40
|
*
|
|
@@ -12,7 +12,16 @@ import type { SymbolTheme } from "../../symbols";
|
|
|
12
12
|
export type ComposerBox = SymbolTheme["boxRound"];
|
|
13
13
|
|
|
14
14
|
/** Built-in composer shape identifiers shipped by pi-tui. */
|
|
15
|
-
export const BUILTIN_EDITOR_BORDER_STYLES = [
|
|
15
|
+
export const BUILTIN_EDITOR_BORDER_STYLES = [
|
|
16
|
+
"box",
|
|
17
|
+
"band",
|
|
18
|
+
"claude",
|
|
19
|
+
"pi",
|
|
20
|
+
"borderless",
|
|
21
|
+
"rule",
|
|
22
|
+
"field",
|
|
23
|
+
"rail",
|
|
24
|
+
] as const;
|
|
16
25
|
|
|
17
26
|
/** Identifier for a built-in composer shape. */
|
|
18
27
|
export type BuiltinEditorBorderStyle = (typeof BUILTIN_EDITOR_BORDER_STYLES)[number];
|
|
@@ -68,14 +77,20 @@ export interface ComposerRowContext extends ComposerChromeContext {
|
|
|
68
77
|
|
|
69
78
|
export interface ComposerStyle {
|
|
70
79
|
readonly id: EditorBorderStyle;
|
|
80
|
+
/**
|
|
81
|
+
* True when rows paint their own foreground through `surfaceColor`.
|
|
82
|
+
* Built-ins default to transparent; registered extensions that omit this
|
|
83
|
+
* field retain the pre-field behavior and receive undecorated row text.
|
|
84
|
+
*/
|
|
85
|
+
readonly filledSurface?: boolean;
|
|
71
86
|
/** Content rows carry left/right border glyphs; drives the cursor-reserve
|
|
72
87
|
* column, IME-safe layout, and the right-border scrollbar. */
|
|
73
88
|
readonly sideBorders: boolean;
|
|
74
89
|
/** Rows consumed by top+bottom chrome (drives maxHeight budgeting). */
|
|
75
90
|
readonly verticalChrome: 0 | 1 | 2;
|
|
76
91
|
/** Where the host should attach the status bar: embedded in the top border,
|
|
77
|
-
* docked onto a top rule, or detached into a standalone bottom bar. */
|
|
78
|
-
readonly statusAttachment: "top-border" | "top-rule-chip" | "none";
|
|
92
|
+
* rendered as a flush soft-capped band above the input, docked onto a top rule, or detached into a standalone bottom bar. */
|
|
93
|
+
readonly statusAttachment: "top-border" | "top-band" | "top-rule-chip" | "none";
|
|
79
94
|
/** Which segment groups the standalone bottom status bar shows. */
|
|
80
95
|
readonly bottomBar: "none" | "left" | "full";
|
|
81
96
|
/** Insert a blank spacer row between the editor and the standalone bottom
|
package/src/components/editor.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type EditorBorderStyle,
|
|
33
33
|
type EditorTopBorder,
|
|
34
34
|
getComposerStyle,
|
|
35
|
+
isFilledComposerStyle,
|
|
35
36
|
} from "./composer";
|
|
36
37
|
|
|
37
38
|
export type { EditorBorderStyle, EditorTopBorder };
|
|
@@ -401,6 +402,8 @@ export interface EditorTheme {
|
|
|
401
402
|
accentColor?: (str: string) => string;
|
|
402
403
|
/** Background fill used by filled composer styles. */
|
|
403
404
|
surfaceColor?: (str: string) => string;
|
|
405
|
+
/** Foreground used when the composer shape leaves its text surface transparent. */
|
|
406
|
+
textColor?: (str: string) => string;
|
|
404
407
|
selectList: SelectListTheme;
|
|
405
408
|
symbols: SymbolTheme;
|
|
406
409
|
editorPaddingX?: number;
|
|
@@ -1271,13 +1274,16 @@ export class Editor implements Component, Focusable {
|
|
|
1271
1274
|
displayWidth = visibleWidth(displayText);
|
|
1272
1275
|
}
|
|
1273
1276
|
}
|
|
1277
|
+
const renderedText = isFilledComposerStyle(style)
|
|
1278
|
+
? displayText
|
|
1279
|
+
: (this.#theme.textColor ?? PASSTHROUGH_COLOR)(displayText);
|
|
1274
1280
|
|
|
1275
1281
|
const linePad = padding(Math.max(0, lineContentWidth - displayWidth));
|
|
1276
1282
|
|
|
1277
1283
|
result.push(
|
|
1278
1284
|
...style.renderRow({
|
|
1279
1285
|
...chromeCtx,
|
|
1280
|
-
text:
|
|
1286
|
+
text: renderedText,
|
|
1281
1287
|
pad: linePad,
|
|
1282
1288
|
gutter: gutterText,
|
|
1283
1289
|
isLastRow: visibleIndex === visibleLayoutLines.length - 1,
|
|
@@ -1403,11 +1409,17 @@ export class Editor implements Component, Focusable {
|
|
|
1403
1409
|
this.#cancelAutocomplete(true);
|
|
1404
1410
|
return;
|
|
1405
1411
|
}
|
|
1412
|
+
// Right arrow at end of line accepts the selection like Tab (fish-style).
|
|
1413
|
+
// Mid-line, right arrow keeps its cursor-movement role and falls through.
|
|
1414
|
+
const rightArrowAccepts =
|
|
1415
|
+
kb.matchesCanonical(canonical, "tui.editor.cursorRight") &&
|
|
1416
|
+
this.#state.cursorCol >= (this.#state.lines[this.#state.cursorLine] ?? "").length;
|
|
1406
1417
|
if (
|
|
1407
1418
|
this.#autocompleteState === "assist" &&
|
|
1408
1419
|
(kb.matchesCanonical(canonical, "tui.input.submit") ||
|
|
1409
1420
|
data === "\n" ||
|
|
1410
|
-
kb.matchesCanonical(canonical, "tui.input.tab")
|
|
1421
|
+
kb.matchesCanonical(canonical, "tui.input.tab") ||
|
|
1422
|
+
rightArrowAccepts)
|
|
1411
1423
|
) {
|
|
1412
1424
|
this.#applySpellingSuggestion();
|
|
1413
1425
|
return;
|
|
@@ -1420,7 +1432,8 @@ export class Editor implements Component, Focusable {
|
|
|
1420
1432
|
kb.matchesCanonical(canonical, "tui.select.pageDown") ||
|
|
1421
1433
|
kb.matchesCanonical(canonical, "tui.input.submit") ||
|
|
1422
1434
|
data === "\n" ||
|
|
1423
|
-
kb.matchesCanonical(canonical, "tui.input.tab")
|
|
1435
|
+
kb.matchesCanonical(canonical, "tui.input.tab") ||
|
|
1436
|
+
rightArrowAccepts
|
|
1424
1437
|
) {
|
|
1425
1438
|
// Only pass navigation keys to the list, not Enter/Tab (we handle those directly)
|
|
1426
1439
|
if (
|
|
@@ -1435,7 +1448,7 @@ export class Editor implements Component, Focusable {
|
|
|
1435
1448
|
}
|
|
1436
1449
|
|
|
1437
1450
|
// If Tab was pressed, always apply the selection
|
|
1438
|
-
if (kb.matchesCanonical(canonical, "tui.input.tab")) {
|
|
1451
|
+
if (kb.matchesCanonical(canonical, "tui.input.tab") || rightArrowAccepts) {
|
|
1439
1452
|
const selected = this.#autocompleteList.getSelectedItem();
|
|
1440
1453
|
// Check for stale autocomplete state due to buffer edits since last refresh
|
|
1441
1454
|
// (destructive keys or paste can outrun the debounced update).
|
|
@@ -1731,6 +1744,13 @@ export class Editor implements Component, Focusable {
|
|
|
1731
1744
|
}
|
|
1732
1745
|
} else if (kb.matchesCanonical(canonical, "tui.editor.cursorRight")) {
|
|
1733
1746
|
// Right
|
|
1747
|
+
// At end of line, accept the inline ghost word completion (IME-style) like Tab.
|
|
1748
|
+
if (
|
|
1749
|
+
this.#state.cursorCol >= (this.#state.lines[this.#state.cursorLine] ?? "").length &&
|
|
1750
|
+
this.#acceptWordCompletion()
|
|
1751
|
+
) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1734
1754
|
this.#moveCursor(0, 1);
|
|
1735
1755
|
} else if (kb.matchesCanonical(canonical, "tui.editor.cursorLeft")) {
|
|
1736
1756
|
// Left
|
|
@@ -3435,13 +3455,7 @@ export class Editor implements Component, Focusable {
|
|
|
3435
3455
|
}
|
|
3436
3456
|
|
|
3437
3457
|
async #handleTabCompletion(): Promise<void> {
|
|
3438
|
-
|
|
3439
|
-
if (wordCompletion) {
|
|
3440
|
-
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
3441
|
-
const after = currentLine.slice(this.#state.cursorCol);
|
|
3442
|
-
this.#insertTextAtCursor(wordCompletion + (/^[\s.,;:!?"\])}]/.test(after) ? "" : " "));
|
|
3443
|
-
return;
|
|
3444
|
-
}
|
|
3458
|
+
if (this.#acceptWordCompletion()) return;
|
|
3445
3459
|
if (!this.#autocompleteProvider) return;
|
|
3446
3460
|
|
|
3447
3461
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
@@ -3458,6 +3472,16 @@ export class Editor implements Component, Focusable {
|
|
|
3458
3472
|
await this.#forceFileAutocomplete();
|
|
3459
3473
|
}
|
|
3460
3474
|
}
|
|
3475
|
+
/** Insert the inline ghost word completion at the cursor, if any. Shared by Tab and right-arrow accept. */
|
|
3476
|
+
#acceptWordCompletion(): boolean {
|
|
3477
|
+
const wordCompletion = this.#getWordCompletion();
|
|
3478
|
+
if (!wordCompletion) return false;
|
|
3479
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
3480
|
+
const after = currentLine.slice(this.#state.cursorCol);
|
|
3481
|
+
this.#insertTextAtCursor(wordCompletion + (/^[\s.,;:!?"\])}]/.test(after) ? "" : " "));
|
|
3482
|
+
return true;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3461
3485
|
async #showSpellingSuggestions(): Promise<void> {
|
|
3462
3486
|
const cursorLine = this.#state.cursorLine;
|
|
3463
3487
|
const cursorCol = this.#state.cursorCol;
|
package/src/components/loader.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { TUI } from "../tui";
|
|
2
|
-
import { getPaddingX, sliceByColumn, visibleWidth } from "../utils";
|
|
2
|
+
import { getPaddingX, padding, sliceByColumn, visibleWidth } from "../utils";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
|
|
5
5
|
const RENDER_INTERVAL_MS = 1000 / 30;
|
|
6
|
-
|
|
6
|
+
/** Milliseconds between spinner-frame advances; exported so time-derived spinners elsewhere tick at the Loader cadence. */
|
|
7
|
+
export const SPINNER_ADVANCE_MS = 80;
|
|
7
8
|
const RENDER_BACKPRESSURE_MULTIPLIER = 9;
|
|
8
9
|
const MAX_BACKPRESSURE_FRAME_COST_MS = 200;
|
|
9
10
|
|
|
@@ -28,6 +29,7 @@ export class Loader extends Text {
|
|
|
28
29
|
#layout?: readonly { leading: string; content: string; trailing: string }[];
|
|
29
30
|
#layoutFrames: readonly string[];
|
|
30
31
|
#layoutFrame: string;
|
|
32
|
+
#trailer?: () => string | undefined;
|
|
31
33
|
|
|
32
34
|
constructor(
|
|
33
35
|
ui: TUI,
|
|
@@ -102,6 +104,15 @@ export class Loader extends Text {
|
|
|
102
104
|
lines.push(`${leading}${content ? this.messageColorFn(content) : ""}${trailing}`);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
if (this.#trailer && lines.length > 1) {
|
|
108
|
+
const trailer = this.#trailer();
|
|
109
|
+
if (trailer) {
|
|
110
|
+
// Text pads rows to full width; drop that pad before docking right.
|
|
111
|
+
const body = lines[1].trimEnd();
|
|
112
|
+
const gap = width - visibleWidth(body) - visibleWidth(trailer);
|
|
113
|
+
if (gap >= 2) lines[1] = body + padding(gap) + trailer;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
105
116
|
return lines;
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -124,6 +135,12 @@ export class Loader extends Text {
|
|
|
124
135
|
dispose() {
|
|
125
136
|
this.stop();
|
|
126
137
|
}
|
|
138
|
+
/** Install a lazy right-docked suffix for the spinner row (e.g. a styled
|
|
139
|
+
* session title). Re-evaluated every paint; dropped when the row leaves
|
|
140
|
+
* less than a two-cell gap. */
|
|
141
|
+
setTrailer(trailer: (() => string | undefined) | undefined): void {
|
|
142
|
+
this.#trailer = trailer;
|
|
143
|
+
}
|
|
127
144
|
|
|
128
145
|
setMessage(message: string) {
|
|
129
146
|
if (message === this.message) {
|
|
@@ -1515,11 +1515,11 @@ const DEFAULT_COLOR_SWATCH_GLYPH = "■";
|
|
|
1515
1515
|
|
|
1516
1516
|
// `#` + 3-8 hex digits, not glued to a surrounding word/`#`/`&` (avoids HTML
|
|
1517
1517
|
// entities like ☃ and paths like foo#fff), not the start of a canonical
|
|
1518
|
-
// UUID, and not trailed by
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
|
|
1522
|
-
|
|
1518
|
+
// UUID, and not trailed by another word char (over-long runs and word
|
|
1519
|
+
// fragments like the "#eac" of "#each" never produce a misleading swatch).
|
|
1520
|
+
// Length/letter rules are enforced in classifyHexColor since the alternation
|
|
1521
|
+
// can't express "exactly 3, 6, or 8".
|
|
1522
|
+
const HEX_COLOR_REGEX = /(?<![\w#&])#(?![0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})([0-9a-fA-F]{3,8})(?!\w)/g;
|
|
1523
1523
|
const HEX_COLOR_EXACT_REGEX = /^#([0-9a-fA-F]{3,8})$/;
|
|
1524
1524
|
|
|
1525
1525
|
/**
|
|
@@ -676,6 +676,18 @@ export function setTerminalTextSizing(enabled: boolean): void {
|
|
|
676
676
|
TERMINAL.textSizing = enabled;
|
|
677
677
|
}
|
|
678
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Override OSC 8 hyperlink capability at runtime. The coding-agent calls this
|
|
681
|
+
* from the `tui.hyperlinks` setting so its resolved policy (`off`/`auto`/`always`)
|
|
682
|
+
* drives every renderer that gates on {@link TERMINAL}`.hyperlinks` — notably the
|
|
683
|
+
* Markdown component's `[text](url)`/bare-URL links — consistently with the
|
|
684
|
+
* path/resource links that already consult the setting directly. Tests flip it
|
|
685
|
+
* to exercise the OSC 8 and plain-text paths deterministically.
|
|
686
|
+
*/
|
|
687
|
+
export function setTerminalHyperlinks(enabled: boolean): void {
|
|
688
|
+
TERMINAL.hyperlinks = enabled;
|
|
689
|
+
}
|
|
690
|
+
|
|
679
691
|
export function getTerminalInfo(
|
|
680
692
|
terminalId: TerminalId,
|
|
681
693
|
platform: NodeJS.Platform = process.platform,
|