@fyeeme/pi-ask-user 2.0.0 → 2.0.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 +33 -30
- package/README.md +99 -79
- package/index.ts +473 -814
- package/package.json +6 -3
- package/src/ask-dialog.ts +1115 -0
- package/src/ask-legacy.ts +642 -0
- package/src/compat.ts +104 -0
- package/src/countdown-timer.ts +83 -0
- package/src/overlay-box.ts +53 -0
- package/src/types.ts +78 -0
package/src/compat.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-tui compatibility layer for the migrated oh-my-pi ask UI.
|
|
3
|
+
*
|
|
4
|
+
* oh-my-pi forked the pi tui package and added helpers/symbols the ask dialog
|
|
5
|
+
* depends on. pi's published `@earendil-works/pi-tui` does not export them, so
|
|
6
|
+
* this module re-implements the small surface the migration needs, mirroring
|
|
7
|
+
* omp semantics:
|
|
8
|
+
*
|
|
9
|
+
* - Ellipsis/padding/replaceTabs: omp utils.ts helpers.
|
|
10
|
+
* - SYMBOLS: omp theme symbol defaults (modes/theme/symbols.ts) — pi's Theme
|
|
11
|
+
* has no symbol table, so the glyphs are fixed constants here and colored
|
|
12
|
+
* through pi's theme.fg.
|
|
13
|
+
* - renderInlineMarkdown: omp's version is backed by a full marked lexer
|
|
14
|
+
* pi-tui does not export; labels render as plain text with the base color.
|
|
15
|
+
* - windowLines: omp's ScrollView(lines, {height}).setScrollOffset(n) string
|
|
16
|
+
* window — pi's ScrollView is a layout container over a Component and cannot
|
|
17
|
+
* window a plain string[] in a custom component's render().
|
|
18
|
+
*
|
|
19
|
+
* Markdown rendering itself uses pi's own Markdown component + getMarkdownTheme
|
|
20
|
+
* (both exported by @earendil-works/pi-coding-agent), which match omp 1:1.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
24
|
+
|
|
25
|
+
export { truncateToWidth, visibleWidth, matchesKey, Key } from "@earendil-works/pi-tui";
|
|
26
|
+
|
|
27
|
+
/** omp Ellipsis.Unicode ("…"). pi's truncateToWidth takes the glyph directly. */
|
|
28
|
+
export const ELLIPSIS = "…";
|
|
29
|
+
|
|
30
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
31
|
+
return Math.max(min, Math.min(value, max));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** omp utils.ts replaceTabs: tabs render inconsistently — replace with spaces. */
|
|
35
|
+
export function replaceTabs(text: string): string {
|
|
36
|
+
return text.replaceAll("\t", " ");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** omp utils.ts padding: n spaces (n clamped at 0). */
|
|
40
|
+
export function padding(n: number): string {
|
|
41
|
+
return n > 0 ? " ".repeat(n) : "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** omp theme symbol defaults (modes/theme/symbols.ts). */
|
|
45
|
+
export const SYMBOLS = {
|
|
46
|
+
nav: { cursor: "❯", selected: "➤" },
|
|
47
|
+
radio: { selected: "◉", unselected: "○" },
|
|
48
|
+
checkbox: { checked: "☑", unchecked: "☐" },
|
|
49
|
+
boxRound: {
|
|
50
|
+
topLeft: "╭",
|
|
51
|
+
topRight: "╮",
|
|
52
|
+
bottomLeft: "╰",
|
|
53
|
+
bottomRight: "╯",
|
|
54
|
+
horizontal: "─",
|
|
55
|
+
vertical: "│",
|
|
56
|
+
teeRight: "├",
|
|
57
|
+
teeLeft: "┤",
|
|
58
|
+
},
|
|
59
|
+
status: { success: "+", warning: "~" },
|
|
60
|
+
format: { bullet: "•" },
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
/** omp renderInlineMarkdown is backed by a full marked lexer pi-tui does not
|
|
64
|
+
* export; labels render as plain (ANSI-passthrough) text with the base color. */
|
|
65
|
+
export function renderInlineMarkdown(text: string, baseColor?: (t: string) => string): string {
|
|
66
|
+
if (typeof text !== "string") return (baseColor ?? (t => t))(text != null ? String(text) : "");
|
|
67
|
+
return (baseColor ?? (t => t))(text);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Window `lines` to `rows` starting at `offset`, padding short content with
|
|
72
|
+
* blank rows (omp ScrollView semantics: the body area never shrinks). A
|
|
73
|
+
* trailing column is NOT reserved for a scrollbar; callers render the omp
|
|
74
|
+
* ↑/↓/↕ clip indicator in the footer instead.
|
|
75
|
+
*/
|
|
76
|
+
export function windowLines(lines: readonly string[], offset: number, rows: number): string[] {
|
|
77
|
+
const out: string[] = [];
|
|
78
|
+
for (let i = 0; i < rows; i++) {
|
|
79
|
+
out.push(lines[offset + i] ?? "");
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** omp ask-dialog #clipIndicator: which scroll directions have hidden content. */
|
|
85
|
+
export function clipIndicator(offset: number, rows: number, totalRows: number): string {
|
|
86
|
+
const above = offset > 0;
|
|
87
|
+
const below = offset + rows < totalRows;
|
|
88
|
+
if (above && below) return "↕";
|
|
89
|
+
if (above) return "↑";
|
|
90
|
+
if (below) return "↓";
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Format a KeybindingsManager key list the way omp's formatKeyHints does. */
|
|
95
|
+
export function formatKeyHints(keys: readonly string[]): string {
|
|
96
|
+
return keys.join("/");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Normalize a key label for footer hints (omp editorKey/cancelKeyLabel). */
|
|
100
|
+
export function keyLabel(keys: readonly string[], fallback: string): string {
|
|
101
|
+
const [first = ""] = formatKeyHints(keys).split("/");
|
|
102
|
+
if (!first) return fallback;
|
|
103
|
+
return first === "escape" ? "Esc" : first === "pageup" ? "PgUp" : first === "pagedown" ? "PgDn" : first;
|
|
104
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable countdown timer for dialog components — migrated verbatim from
|
|
3
|
+
* oh-my-pi (packages/coding-agent/src/modes/components/countdown-timer.ts).
|
|
4
|
+
*
|
|
5
|
+
* Adaptation: the `tui` parameter is narrowed to the only member omp uses
|
|
6
|
+
* (`requestRender`), so tests can pass a plain object.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface TuiRenderHandle {
|
|
10
|
+
requestRender(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class CountdownTimer {
|
|
14
|
+
#intervalId: ReturnType<typeof setInterval> | undefined;
|
|
15
|
+
#expireTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
16
|
+
#remainingSeconds: number;
|
|
17
|
+
#deadlineMs = 0;
|
|
18
|
+
readonly #initialMs: number;
|
|
19
|
+
readonly #tui: TuiRenderHandle | undefined;
|
|
20
|
+
readonly #onTick: (seconds: number) => void;
|
|
21
|
+
readonly #onExpire: () => void;
|
|
22
|
+
|
|
23
|
+
constructor(timeoutMs: number, tui: TuiRenderHandle | undefined, onTick: (seconds: number) => void, onExpire: () => void) {
|
|
24
|
+
this.#initialMs = timeoutMs;
|
|
25
|
+
this.#tui = tui;
|
|
26
|
+
this.#onTick = onTick;
|
|
27
|
+
this.#onExpire = onExpire;
|
|
28
|
+
this.#remainingSeconds = Math.ceil(timeoutMs / 1000);
|
|
29
|
+
this.#start();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#calculateRemainingSeconds(now = Date.now()): number {
|
|
33
|
+
const remainingMs = Math.max(0, this.#deadlineMs - now);
|
|
34
|
+
return Math.ceil(remainingMs / 1000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#start(): void {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
this.#deadlineMs = now + this.#initialMs;
|
|
40
|
+
this.#remainingSeconds = this.#calculateRemainingSeconds(now);
|
|
41
|
+
this.#onTick(this.#remainingSeconds);
|
|
42
|
+
this.#tui?.requestRender();
|
|
43
|
+
|
|
44
|
+
this.#expireTimeoutId = setTimeout(() => {
|
|
45
|
+
this.dispose();
|
|
46
|
+
this.#onExpire();
|
|
47
|
+
}, this.#initialMs);
|
|
48
|
+
|
|
49
|
+
this.#startInterval();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#startInterval(): void {
|
|
53
|
+
if (this.#intervalId) {
|
|
54
|
+
clearInterval(this.#intervalId);
|
|
55
|
+
this.#intervalId = undefined;
|
|
56
|
+
}
|
|
57
|
+
this.#intervalId = setInterval(() => {
|
|
58
|
+
const remainingSeconds = this.#calculateRemainingSeconds();
|
|
59
|
+
if (remainingSeconds !== this.#remainingSeconds) {
|
|
60
|
+
this.#remainingSeconds = remainingSeconds;
|
|
61
|
+
this.#onTick(this.#remainingSeconds);
|
|
62
|
+
}
|
|
63
|
+
this.#tui?.requestRender();
|
|
64
|
+
}, 1000);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reset the countdown to its initial value */
|
|
68
|
+
reset(): void {
|
|
69
|
+
this.dispose();
|
|
70
|
+
this.#start();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
dispose(): void {
|
|
74
|
+
if (this.#intervalId) {
|
|
75
|
+
clearInterval(this.#intervalId);
|
|
76
|
+
this.#intervalId = undefined;
|
|
77
|
+
}
|
|
78
|
+
if (this.#expireTimeoutId) {
|
|
79
|
+
clearTimeout(this.#expireTimeoutId);
|
|
80
|
+
this.#expireTimeoutId = undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box-drawing chrome — migrated from oh-my-pi
|
|
3
|
+
* (packages/coding-agent/src/modes/components/overlay-box.ts).
|
|
4
|
+
*
|
|
5
|
+
* Adaptation: `theme.boxRound` glyphs come from the local SYMBOLS constant
|
|
6
|
+
* (pi's Theme has no symbol table); colors still route through pi's
|
|
7
|
+
* theme.fg. Only the helpers the ask dialog uses were carried over.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { padding, SYMBOLS, truncateToWidth, visibleWidth } from "./compat.ts";
|
|
12
|
+
|
|
13
|
+
/** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
|
|
14
|
+
export function fit(text: string, width: number): string {
|
|
15
|
+
if (width <= 0) return "";
|
|
16
|
+
const w = visibleWidth(text);
|
|
17
|
+
if (w === width) return text;
|
|
18
|
+
if (w < width) return text + padding(width - w);
|
|
19
|
+
const cut = truncateToWidth(text, width, "");
|
|
20
|
+
const cw = visibleWidth(cut);
|
|
21
|
+
return cw < width ? cut + padding(width - cw) : cut;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Top border with an optional accent-colored title inset into the rule. */
|
|
25
|
+
export function topBorder(theme: Theme, width: number, title: string): string {
|
|
26
|
+
const box = SYMBOLS.boxRound;
|
|
27
|
+
const inner = Math.max(0, width - 2);
|
|
28
|
+
if (!title) return theme.fg("border", box.topLeft + box.horizontal.repeat(inner) + box.topRight);
|
|
29
|
+
const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2), "");
|
|
30
|
+
const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
|
|
31
|
+
return (
|
|
32
|
+
theme.fg("border", box.topLeft + box.horizontal) +
|
|
33
|
+
theme.bold(theme.fg("accent", shown)) +
|
|
34
|
+
theme.fg("border", box.horizontal.repeat(fillWidth) + box.topRight)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A horizontal rule with left/right tees, splitting overlay sections. */
|
|
39
|
+
export function divider(theme: Theme, width: number): string {
|
|
40
|
+
const box = SYMBOLS.boxRound;
|
|
41
|
+
return theme.fg("border", box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function bottomBorder(theme: Theme, width: number): string {
|
|
45
|
+
const box = SYMBOLS.boxRound;
|
|
46
|
+
return theme.fg("border", box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Wrap pre-styled content in vertical borders with single-column insets. */
|
|
50
|
+
export function row(theme: Theme, content: string, width: number): string {
|
|
51
|
+
const box = SYMBOLS.boxRound;
|
|
52
|
+
return `${theme.fg("border", box.vertical)} ${fit(content, Math.max(0, width - 4))} ${theme.fg("border", box.vertical)}`;
|
|
53
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask dialog types — migrated from oh-my-pi. The ExtensionAskDialog / ExtensionUI
|
|
3
|
+
* shapes come from packages/coding-agent/src/extensibility/extensions/types.ts;
|
|
4
|
+
* QuestionResult/AskToolDetails/AskOption come from packages/coding-agent/src/tools/ask.ts.
|
|
5
|
+
*
|
|
6
|
+
* Adaptation: none — these are pure data shapes, carried over verbatim.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ExtensionUISelectOption {
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ExtensionUISelectItem = string | ExtensionUISelectOption;
|
|
15
|
+
|
|
16
|
+
export interface ExtensionAskDialogOption {
|
|
17
|
+
label: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
preview?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ExtensionAskDialogQuestion {
|
|
23
|
+
id: string;
|
|
24
|
+
question: string;
|
|
25
|
+
header?: string;
|
|
26
|
+
options: ExtensionAskDialogOption[];
|
|
27
|
+
multi?: boolean;
|
|
28
|
+
recommended?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ExtensionAskDialogResultItem {
|
|
32
|
+
id: string;
|
|
33
|
+
question: string;
|
|
34
|
+
options: string[];
|
|
35
|
+
multi: boolean;
|
|
36
|
+
selectedOptions: string[];
|
|
37
|
+
customInput?: string;
|
|
38
|
+
timedOut?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ExtensionAskDialogSubmitResult {
|
|
42
|
+
kind: "submit";
|
|
43
|
+
results: ExtensionAskDialogResultItem[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ExtensionAskDialogResult = ExtensionAskDialogSubmitResult;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Ask dialog types — migrated from oh-my-pi. The ExtensionAskDialog / ExtensionUI
|
|
50
|
+
* shapes come from packages/coding-agent/src/extensibility/extensions/types.ts;
|
|
51
|
+
* QuestionResult/AskToolDetails/AskOption come from packages/coding-agent/src/tools/ask.ts.
|
|
52
|
+
*
|
|
53
|
+
* Adaptation: none — these are pure data shapes, carried over verbatim.
|
|
54
|
+
*//** Result for a single question (omp AskToolDetails.QuestionResult). */
|
|
55
|
+
export interface QuestionResult {
|
|
56
|
+
id: string;
|
|
57
|
+
question: string;
|
|
58
|
+
options: string[];
|
|
59
|
+
multi: boolean;
|
|
60
|
+
selectedOptions: string[];
|
|
61
|
+
customInput?: string;
|
|
62
|
+
timedOut?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AskToolDetails {
|
|
66
|
+
question?: string;
|
|
67
|
+
options?: string[];
|
|
68
|
+
multi?: boolean;
|
|
69
|
+
selectedOptions?: string[];
|
|
70
|
+
customInput?: string;
|
|
71
|
+
timedOut?: boolean;
|
|
72
|
+
results?: QuestionResult[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AskOption {
|
|
76
|
+
label: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
}
|