@narumitw/pi-btw 0.55.2 → 0.55.3
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/dist/index.ts +143 -46
- package/dist/index.ts.map +3 -3
- package/package.json +2 -2
- package/src/fullscreen-ui.ts +136 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-btw",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.3",
|
|
4
4
|
"description": "Pi extension that adds a /btw side-question command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"directory": "packages/pi-btw"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@narumitw/pi-tui-kit": "^0.58.
|
|
50
|
+
"@narumitw/pi-tui-kit": "^0.58.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@earendil-works/pi-ai": "*",
|
package/src/fullscreen-ui.ts
CHANGED
|
@@ -7,9 +7,14 @@ import {
|
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import {
|
|
9
9
|
type Component,
|
|
10
|
+
isKeyRelease,
|
|
11
|
+
Key,
|
|
12
|
+
matchesKey,
|
|
10
13
|
type OverlayHandle,
|
|
11
14
|
type TUI,
|
|
12
15
|
TuiAltScreen,
|
|
16
|
+
type TuiInputListener,
|
|
17
|
+
type TuiInputListenerResult,
|
|
13
18
|
truncateToWidth,
|
|
14
19
|
} from "@earendil-works/pi-tui";
|
|
15
20
|
import { sanitizeSingleLine } from "./text.js";
|
|
@@ -25,6 +30,7 @@ type BtwCustomFactory<T> = (
|
|
|
25
30
|
type BtwFullscreenTui = TUI & {
|
|
26
31
|
flash?: (message: string, durationMs?: number) => void;
|
|
27
32
|
setLayoutRoot(component: Component | undefined): void;
|
|
33
|
+
addInputListenerBeforeViewport?(listener: TuiInputListener): () => void;
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
export interface BtwFullscreenLayoutComponent extends Component {
|
|
@@ -69,9 +75,10 @@ export async function runBtwFullscreen<T>(
|
|
|
69
75
|
));
|
|
70
76
|
let liveEditorText = ctx.ui.getEditorText();
|
|
71
77
|
let restoreEditor = false;
|
|
78
|
+
let host: BtwFullscreenHost<T> | undefined;
|
|
72
79
|
const outcome = await ctx.ui.custom<FullscreenOutcome<T>>(
|
|
73
|
-
(parent, theme, keybindings, done) =>
|
|
74
|
-
new BtwFullscreenHost(
|
|
80
|
+
(parent, theme, keybindings, done) => {
|
|
81
|
+
host = new BtwFullscreenHost(
|
|
75
82
|
parent,
|
|
76
83
|
theme,
|
|
77
84
|
keybindings,
|
|
@@ -87,7 +94,13 @@ export async function runBtwFullscreen<T>(
|
|
|
87
94
|
done(value);
|
|
88
95
|
},
|
|
89
96
|
createTui,
|
|
90
|
-
)
|
|
97
|
+
);
|
|
98
|
+
return host;
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
overlay: true,
|
|
102
|
+
onHandle: (handle) => host?.setParentOverlay(handle),
|
|
103
|
+
},
|
|
91
104
|
);
|
|
92
105
|
if (restoreEditor) {
|
|
93
106
|
try {
|
|
@@ -100,6 +113,59 @@ export async function runBtwFullscreen<T>(
|
|
|
100
113
|
return outcome.value;
|
|
101
114
|
}
|
|
102
115
|
|
|
116
|
+
type BtwInputListeners = {
|
|
117
|
+
beforeViewport: Set<TuiInputListener>;
|
|
118
|
+
regular: Set<TuiInputListener>;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const btwInputListeners = new WeakMap<BtwTuiAltScreen, BtwInputListeners>();
|
|
122
|
+
|
|
123
|
+
function dispatchBtwInput(listeners: BtwInputListeners, data: string): TuiInputListenerResult {
|
|
124
|
+
let current = data;
|
|
125
|
+
for (const group of [listeners.beforeViewport, listeners.regular]) {
|
|
126
|
+
for (const listener of group) {
|
|
127
|
+
const result = listener(current);
|
|
128
|
+
if (result?.consume) return result;
|
|
129
|
+
if (result?.data !== undefined) current = result.data;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return current === data ? undefined : { data: current };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class BtwTuiAltScreen extends TuiAltScreen {
|
|
136
|
+
override addInputListener(listener: TuiInputListener): () => void {
|
|
137
|
+
let listeners = btwInputListeners.get(this);
|
|
138
|
+
if (!listeners) {
|
|
139
|
+
const registeredListeners: BtwInputListeners = {
|
|
140
|
+
beforeViewport: new Set(),
|
|
141
|
+
regular: new Set(),
|
|
142
|
+
};
|
|
143
|
+
btwInputListeners.set(this, registeredListeners);
|
|
144
|
+
super.addInputListener((data) => dispatchBtwInput(registeredListeners, data));
|
|
145
|
+
listeners = registeredListeners;
|
|
146
|
+
}
|
|
147
|
+
listeners.regular.add(listener);
|
|
148
|
+
return () => listeners.regular.delete(listener);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
addInputListenerBeforeViewport(listener: TuiInputListener): () => void {
|
|
152
|
+
const listeners = btwInputListeners.get(this);
|
|
153
|
+
if (!listeners) return super.addInputListener(listener);
|
|
154
|
+
listeners.beforeViewport.add(listener);
|
|
155
|
+
return () => listeners.beforeViewport.delete(listener);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override removeInputListener(listener: TuiInputListener): void {
|
|
159
|
+
const listeners = btwInputListeners.get(this);
|
|
160
|
+
if (!listeners) {
|
|
161
|
+
super.removeInputListener(listener);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
listeners.beforeViewport.delete(listener);
|
|
165
|
+
listeners.regular.delete(listener);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
103
169
|
function createBtwFullscreenTui(
|
|
104
170
|
parent: TUI,
|
|
105
171
|
theme: Theme,
|
|
@@ -108,7 +174,7 @@ function createBtwFullscreenTui(
|
|
|
108
174
|
): BtwFullscreenTui {
|
|
109
175
|
const styleSearchMatch = (text: string) =>
|
|
110
176
|
theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
|
|
111
|
-
return new
|
|
177
|
+
return new BtwTuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), undefined, {
|
|
112
178
|
mouse: true,
|
|
113
179
|
searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
|
|
114
180
|
searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
|
|
@@ -139,10 +205,18 @@ function openUrlInBrowser(target: string): void {
|
|
|
139
205
|
|
|
140
206
|
class BtwFullscreenHost<T> implements Component {
|
|
141
207
|
private fullscreen: BtwFullscreenTui | undefined;
|
|
208
|
+
private parentOverlay: OverlayHandle | undefined;
|
|
142
209
|
private cancelActiveCustom: (() => void) | undefined;
|
|
210
|
+
private hardCancelActiveCustom: (() => void) | undefined;
|
|
211
|
+
private removeHardCancelListener: (() => void) | undefined;
|
|
143
212
|
private started = false;
|
|
144
213
|
private disposed = false;
|
|
145
214
|
private finished = false;
|
|
215
|
+
private parentStopped = false;
|
|
216
|
+
private parentRestarted = false;
|
|
217
|
+
private fullscreenCreated = false;
|
|
218
|
+
private fullscreenStopped = false;
|
|
219
|
+
private cleanupError: unknown;
|
|
146
220
|
|
|
147
221
|
constructor(
|
|
148
222
|
private readonly parent: TUI,
|
|
@@ -156,6 +230,10 @@ class BtwFullscreenHost<T> implements Component {
|
|
|
156
230
|
queueMicrotask(() => void this.start());
|
|
157
231
|
}
|
|
158
232
|
|
|
233
|
+
setParentOverlay(overlay: OverlayHandle): void {
|
|
234
|
+
this.parentOverlay = overlay;
|
|
235
|
+
}
|
|
236
|
+
|
|
159
237
|
render(width: number): string[] {
|
|
160
238
|
return [truncateToWidth(this.theme.fg("muted", "Opening btw side thread…"), width)];
|
|
161
239
|
}
|
|
@@ -172,45 +250,69 @@ class BtwFullscreenHost<T> implements Component {
|
|
|
172
250
|
if (this.started || this.finished) return;
|
|
173
251
|
this.started = true;
|
|
174
252
|
let outcome: FullscreenOutcome<T>;
|
|
175
|
-
let parentStopped = false;
|
|
176
|
-
let fullscreenCreated = false;
|
|
177
253
|
try {
|
|
178
254
|
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
179
255
|
this.parent.stop({ preserveScreen: true });
|
|
180
|
-
parentStopped = true;
|
|
256
|
+
this.parentStopped = true;
|
|
181
257
|
if (this.disposed) throw new FullscreenUiDisposedError();
|
|
182
258
|
this.fullscreen = this.createTui(this.parent, this.theme);
|
|
183
|
-
fullscreenCreated = true;
|
|
259
|
+
this.fullscreenCreated = true;
|
|
184
260
|
this.fullscreen.start();
|
|
261
|
+
// Waiting for the custom promise would leave follow-up keys bound to the side TUI.
|
|
262
|
+
const addHardCancelListener =
|
|
263
|
+
this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ??
|
|
264
|
+
this.fullscreen.addInputListener.bind(this.fullscreen);
|
|
265
|
+
this.removeHardCancelListener = addHardCancelListener((data) => {
|
|
266
|
+
if (isKeyRelease(data) || !matchesKey(data, Key.ctrl("c"))) return undefined;
|
|
267
|
+
try {
|
|
268
|
+
this.hardCancelActiveCustom?.();
|
|
269
|
+
} finally {
|
|
270
|
+
this.restoreParent();
|
|
271
|
+
}
|
|
272
|
+
return { consume: true };
|
|
273
|
+
});
|
|
185
274
|
outcome = { kind: "completed", value: await this.run(this.createContext()) };
|
|
186
275
|
} catch (error) {
|
|
187
276
|
outcome = { kind: "failed", error };
|
|
188
277
|
}
|
|
189
278
|
|
|
190
|
-
let cleanupError: unknown;
|
|
191
279
|
try {
|
|
192
280
|
this.cancelActiveCustom?.();
|
|
193
281
|
} catch (error) {
|
|
194
|
-
cleanupError
|
|
282
|
+
this.cleanupError ??= error;
|
|
195
283
|
}
|
|
196
|
-
|
|
284
|
+
this.restoreParent();
|
|
285
|
+
if (this.cleanupError !== undefined) outcome = { kind: "failed", error: this.cleanupError };
|
|
286
|
+
this.finished = true;
|
|
287
|
+
this.done(outcome);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private restoreParent(): void {
|
|
291
|
+
this.removeHardCancelListener?.();
|
|
292
|
+
this.removeHardCancelListener = undefined;
|
|
293
|
+
if (this.fullscreenCreated && !this.fullscreenStopped) {
|
|
294
|
+
this.fullscreenStopped = true;
|
|
197
295
|
try {
|
|
198
296
|
this.fullscreen?.stop({ preserveScreen: true });
|
|
199
297
|
} catch (error) {
|
|
200
|
-
cleanupError ??= error;
|
|
298
|
+
this.cleanupError ??= error;
|
|
201
299
|
}
|
|
202
300
|
}
|
|
203
|
-
if (parentStopped)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
301
|
+
if (!this.parentStopped || this.parentRestarted) return;
|
|
302
|
+
const parentOverlay = this.parentOverlay;
|
|
303
|
+
this.parentOverlay = undefined;
|
|
304
|
+
try {
|
|
305
|
+
parentOverlay?.setHidden(true);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
this.cleanupError ??= error;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
this.parent.start();
|
|
311
|
+
this.parentRestarted = true;
|
|
312
|
+
this.parent.renderNow(false);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.cleanupError ??= error;
|
|
210
315
|
}
|
|
211
|
-
if (cleanupError !== undefined) outcome = { kind: "failed", error: cleanupError };
|
|
212
|
-
this.finished = true;
|
|
213
|
-
this.done(outcome);
|
|
214
316
|
}
|
|
215
317
|
|
|
216
318
|
private createContext(): ExtensionCommandContext {
|
|
@@ -296,6 +398,7 @@ class BtwFullscreenHost<T> implements Component {
|
|
|
296
398
|
if (promiseSettled || !hasPendingValue) return;
|
|
297
399
|
promiseSettled = true;
|
|
298
400
|
this.cancelActiveCustom = undefined;
|
|
401
|
+
this.hardCancelActiveCustom = undefined;
|
|
299
402
|
if (!factorySettled) {
|
|
300
403
|
resolve(pendingValue as Value);
|
|
301
404
|
return;
|
|
@@ -319,6 +422,7 @@ class BtwFullscreenHost<T> implements Component {
|
|
|
319
422
|
closed = true;
|
|
320
423
|
promiseSettled = true;
|
|
321
424
|
this.cancelActiveCustom = undefined;
|
|
425
|
+
this.hardCancelActiveCustom = undefined;
|
|
322
426
|
try {
|
|
323
427
|
unmount();
|
|
324
428
|
reject(error);
|
|
@@ -331,6 +435,16 @@ class BtwFullscreenHost<T> implements Component {
|
|
|
331
435
|
disposeComponent();
|
|
332
436
|
if (!promiseSettled) fail(new FullscreenUiDisposedError());
|
|
333
437
|
};
|
|
438
|
+
this.hardCancelActiveCustom = () => {
|
|
439
|
+
if (promiseSettled) return;
|
|
440
|
+
try {
|
|
441
|
+
component?.handleInput?.("\u0003");
|
|
442
|
+
} catch (error) {
|
|
443
|
+
fail(error);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.cancelActiveCustom?.();
|
|
447
|
+
};
|
|
334
448
|
|
|
335
449
|
let created: ReturnType<BtwCustomFactory<Value>>;
|
|
336
450
|
try {
|