pi-jar 0.2.6 → 0.2.7
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/README.md +2 -0
- package/package.json +1 -1
- package/src/ask-tool.ts +38 -14
- package/src/popup.ts +9 -1
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@ Pi only delivers mouse events in its **fullscreen** TUI mode. In regular mode th
|
|
|
93
93
|
|
|
94
94
|
## Popup navigation
|
|
95
95
|
|
|
96
|
+
Pi-jar questions (`jar_ask`) sit at the bottom above other pi-jar popups, which resume when the question finishes. Their compact option list supports ↑/↓ selection, PgUp/PgDn or wheel scrolling, and shows descriptions for the selected option only.
|
|
97
|
+
|
|
96
98
|
Pi-jar popups stay within the terminal viewport, including after resizing. Model, fallback, alias, theme and profile pickers filter as you type; use ↑/↓, PgUp/PgDn or the mouse wheel to move and Enter to choose. In Roles and Tasks, press `/` to search the full list, Enter to apply, and Esc while searching to clear it. Other pi-jar views support `/` to filter popup content (History and Changes retain their own search). Oversized popup chrome scrolls with the wheel or Ctrl+PgUp/PgDn.
|
|
97
99
|
|
|
98
100
|
The task widget starts collapsed to a one-line summary. In fullscreen, click its heading to expand/collapse and wheel-scroll the expanded list. It uses at most a quarter of the terminal height. In regular mode use `/jar tasks expand`, `/jar tasks collapse`, or `/jar tasks scroll N`; `/jar tasks` opens the full editor, where ←/→ folds/unfolds a parent's subtasks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jar",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "A warm, role-aware Pi terminal workspace with switchable profiles, animated UI, plan and goal workflows, tasks, change review, model roles, background shells and subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/ask-tool.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
2
|
+
import { acquirePopupPriority } from "./popup.ts";
|
|
3
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type OverlayHandle, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { cleanText } from "./status.ts";
|
|
6
6
|
|
|
@@ -34,7 +34,9 @@ async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index:
|
|
|
34
34
|
})).filter((option) => !!option.label);
|
|
35
35
|
if (!ctx.hasUI || ctx.mode !== "tui" || signal?.aborted) return { kind: "cancel" };
|
|
36
36
|
let abort: (() => void) | undefined;
|
|
37
|
-
|
|
37
|
+
const release = acquirePopupPriority();
|
|
38
|
+
let handle: OverlayHandle | undefined;
|
|
39
|
+
try { return await ctx.ui.custom<PickResult>((tui, theme, _keys, complete) => {
|
|
38
40
|
const done = (result: PickResult) => { if (abort) signal?.removeEventListener("abort", abort); complete(result); };
|
|
39
41
|
abort = () => done({ kind: "cancel" });
|
|
40
42
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -51,6 +53,8 @@ async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index:
|
|
|
51
53
|
let hitRows: number[] = [];
|
|
52
54
|
let scroll = 0;
|
|
53
55
|
let lastSelected = selected;
|
|
56
|
+
let page = 10;
|
|
57
|
+
let reveal = false;
|
|
54
58
|
const totalRows = () => options.length + actions.length;
|
|
55
59
|
const choose = () => {
|
|
56
60
|
if (selected < options.length) {
|
|
@@ -72,6 +76,10 @@ async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index:
|
|
|
72
76
|
if (matchesKey(data, Key.escape)) return done({ kind: "cancel" });
|
|
73
77
|
if (matchesKey(data, Key.up)) selected = (selected + totalRows() - 1) % totalRows();
|
|
74
78
|
else if (matchesKey(data, Key.down)) selected = (selected + 1) % totalRows();
|
|
79
|
+
else if (matchesKey(data, Key.pageDown)) { scroll += page; }
|
|
80
|
+
else if (matchesKey(data, Key.pageUp)) { scroll = Math.max(0, scroll - page); }
|
|
81
|
+
else if (matchesKey(data, Key.home)) { selected = 0; reveal = true; }
|
|
82
|
+
else if (matchesKey(data, Key.end)) { selected = totalRows() - 1; reveal = true; }
|
|
75
83
|
else if (multi && data === "a") {
|
|
76
84
|
// Select all, or clear when everything is already checked.
|
|
77
85
|
if (checked.size === options.length) checked.clear(); else options.forEach((_option, at) => checked.add(at));
|
|
@@ -95,14 +103,14 @@ async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index:
|
|
|
95
103
|
return { handled: true };
|
|
96
104
|
},
|
|
97
105
|
render(width: number): string[] {
|
|
106
|
+
// Pi exposes focus order rather than a numeric z-index; focus brings this overlay to the front.
|
|
107
|
+
if (handle && !handle.isFocused()) handle.focus();
|
|
98
108
|
hitRows = [];
|
|
109
|
+
const height = Math.max(1, tui.terminal?.rows ?? process.stdout.rows ?? 24);
|
|
99
110
|
const inner = Math.max(1, width - 4);
|
|
100
111
|
const badge = "◆ QUESTION " + String(index + 1) + "/" + String(total) + (multi ? " · MULTI SELECT" : "");
|
|
101
|
-
const title = question.header ? cleanText(question.header, 48) : "
|
|
102
|
-
const lines: string[] = [
|
|
103
|
-
fit(theme.fg("accent", "╭─ " + badge + " ─"), width),
|
|
104
|
-
fit(theme.fg("muted", "│ " + title), width)
|
|
105
|
-
];
|
|
112
|
+
const title = question.header ? " · " + cleanText(question.header, 48) : "";
|
|
113
|
+
const lines: string[] = [fit(theme.fg("accent", "╭─ " + badge + title), width)];
|
|
106
114
|
const content: { line: string; target?: number }[] = [];
|
|
107
115
|
for (const row of wrapTextWithAnsi(cleanText(question.question, 900), inner)) content.push({ line: fit("│ " + row, width) });
|
|
108
116
|
content.push({ line: fit(theme.fg("dim", "├" + "─".repeat(Math.max(0, width - 1))), width) });
|
|
@@ -111,7 +119,11 @@ async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index:
|
|
|
111
119
|
const marker = multi ? (checked.has(at) ? "☑" : "☐") : (active ? "●" : "○");
|
|
112
120
|
const number = String(at + 1).padStart(2, " ") + ".";
|
|
113
121
|
content.push({ target: at, line: fit(theme.fg(active ? "accent" : "muted", "│ " + (active ? "❯ " : " ") + marker + " " + number + " " + option.label), width) });
|
|
114
|
-
if (option.description
|
|
122
|
+
if (option.description && active) {
|
|
123
|
+
for (const description of wrapTextWithAnsi(option.description, Math.max(1, inner - 6)).slice(0, 2)) {
|
|
124
|
+
content.push({ target: at, line: fit(theme.fg("dim", "│ " + description), width) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
115
127
|
});
|
|
116
128
|
actions.forEach((action, at) => {
|
|
117
129
|
const row = options.length + at;
|
|
@@ -120,24 +132,36 @@ async function pickQuestion(ctx: ExtensionContext, question: AskQuestion, index:
|
|
|
120
132
|
const chip = "[ " + action.icon + " " + label + " ]";
|
|
121
133
|
content.push({ target: row, line: fit(theme.fg(active ? "accent" : "dim", "│ " + (active ? "❯ " : " ") + chip), width) });
|
|
122
134
|
});
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
// One viewport owns all question text/options/actions: no nested generic popup scrolling.
|
|
136
|
+
page = Math.max(1, Math.min(12, height - 2));
|
|
137
|
+
if (selected !== lastSelected || reveal) {
|
|
125
138
|
const target = content.findIndex((row) => row.target === selected);
|
|
126
139
|
if (target < scroll) scroll = target;
|
|
127
140
|
else if (target >= scroll + page) scroll = target - page + 1;
|
|
128
141
|
lastSelected = selected;
|
|
142
|
+
reveal = false;
|
|
129
143
|
}
|
|
130
144
|
scroll = Math.max(0, Math.min(scroll, Math.max(0, content.length - page)));
|
|
131
145
|
content.slice(scroll, scroll + page).forEach((row) => {
|
|
132
146
|
if (row.target !== undefined) hitRows[lines.length] = row.target;
|
|
133
147
|
lines.push(row.line);
|
|
134
148
|
});
|
|
135
|
-
const hint = multi ? `${checked.size}/${options.length} checked · ↑↓
|
|
149
|
+
const hint = multi ? `${checked.size}/${options.length} checked · ↑↓ · Space toggle · Enter · a all · Esc` : "↑↓ choose · Enter · Pg/wheel scroll · Esc";
|
|
136
150
|
lines.push(fit(theme.fg("dim", `╰─ ${scroll + 1}–${Math.min(scroll + page, content.length)}/${content.length} · ${hint}`), width));
|
|
137
|
-
|
|
151
|
+
if (height <= 2) {
|
|
152
|
+
const body = lines.slice(1, 1 + page);
|
|
153
|
+
const remapped = body.map((_line, row) => hitRows[row + 1]);
|
|
154
|
+
hitRows = remapped;
|
|
155
|
+
return body.slice(0, height).map((line) => fit(line, width));
|
|
156
|
+
}
|
|
157
|
+
return lines.slice(0, height).map((line) => visibleWidth(line) <= width ? line : fit(line, width));
|
|
138
158
|
}
|
|
139
159
|
};
|
|
140
|
-
}
|
|
160
|
+
}, {
|
|
161
|
+
overlay: true,
|
|
162
|
+
overlayOptions: { anchor: "bottom-center", width: "100%", maxHeight: "100%", margin: 0 },
|
|
163
|
+
onHandle(value) { handle = value; handle.focus(); }
|
|
164
|
+
}); } finally { release(); if (abort) signal?.removeEventListener("abort", abort); }
|
|
141
165
|
}
|
|
142
166
|
|
|
143
167
|
export async function askOne(ctx: ExtensionContext, question: AskQuestion, index = 0, total = 1, signal?: AbortSignal): Promise<AskAnswer> {
|
package/src/popup.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ExtensionContext, KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Input, Key, matchesKey, stripTerminalSequences, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
|
+
let priorityPopups = 0;
|
|
5
|
+
/** Hide ordinary jar overlays while a blocking question owns the screen. */
|
|
6
|
+
export function acquirePopupPriority(): () => void {
|
|
7
|
+
priorityPopups++;
|
|
8
|
+
let released = false;
|
|
9
|
+
return () => { if (!released) { released = true; priorityPopups--; } };
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
type Factory<T> = (tui: TUI, theme: Theme, keys: KeybindingsManager, done: (result: T) => void) => Component & { dispose?(): void };
|
|
5
13
|
export interface PopupOptions {
|
|
6
14
|
/** Views with their own search/editor keep ownership of printable keys. */
|
|
@@ -91,5 +99,5 @@ export function popup<T>(ctx: ExtensionContext, factory: Factory<T>, options: Po
|
|
|
91
99
|
return [header, ...body, footer].slice(0, height).map((line) => truncateToWidth(line, width));
|
|
92
100
|
}
|
|
93
101
|
};
|
|
94
|
-
}, { overlay: true, overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%", margin: 0 } });
|
|
102
|
+
}, { overlay: true, overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%", margin: 0, visible: () => priorityPopups === 0 } });
|
|
95
103
|
}
|