@herbertgao/pi-extensions 2026.9.12 → 2026.9.13
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 +4 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
- package/node_modules/@narumitw/pi-btw/README.md +21 -4
- package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
- package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
- package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
- package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
- package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
- package/node_modules/pi-multi-account/README.md +38 -11
- package/node_modules/pi-multi-account/index.ts +371 -96
- package/node_modules/pi-multi-account/package.json +5 -4
- package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
- package/node_modules/pi-multi-account/usage.ts +31 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
- package/node_modules/pi-typesafe/dist/auth.js +13 -7
- package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
- package/node_modules/pi-typesafe/dist/backends.js +33 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
- package/node_modules/pi-typesafe/dist/client.js +63 -39
- package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
- package/node_modules/pi-typesafe/dist/credentials.js +15 -8
- package/node_modules/pi-typesafe/dist/extension.js +4 -1
- package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
- package/node_modules/pi-typesafe/dist/login.js +17 -8
- package/node_modules/pi-typesafe/dist/schema.js +19 -13
- package/node_modules/pi-typesafe/package.json +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
type Focusable,
|
|
5
|
+
HStack,
|
|
6
|
+
isFocusable,
|
|
7
|
+
ScrollView,
|
|
8
|
+
type TuiMouseEvent,
|
|
9
|
+
type TuiMouseEventResult,
|
|
10
|
+
truncateToWidth,
|
|
11
|
+
visibleWidth,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
13
|
+
import { BtwPasteGuard } from "./keybindings.js";
|
|
14
|
+
import {
|
|
15
|
+
type BtwLayout,
|
|
16
|
+
DEFAULT_BTW_SIDE_PANE_RATIO,
|
|
17
|
+
MAX_BTW_SIDE_PANE_RATIO,
|
|
18
|
+
MIN_BTW_SIDE_PANE_RATIO,
|
|
19
|
+
} from "./settings.js";
|
|
20
|
+
|
|
21
|
+
export const MIN_BTW_SPLIT_COLUMNS = 80;
|
|
22
|
+
const PANE_DIVIDER_COLUMNS = 1;
|
|
23
|
+
// biome-ignore lint/complexity/useRegexLiterals: the constructor keeps a raw ESC control character out of source.
|
|
24
|
+
const SGR_MOUSE_PRESS_PATTERN = new RegExp("^\\u001b\\[<(\\d+);(\\d+);\\d+M$");
|
|
25
|
+
type BtwActivePane = "side" | "main";
|
|
26
|
+
|
|
27
|
+
export interface BtwFullscreenLayoutComponent extends Component {
|
|
28
|
+
getFullscreenLayout(): Component;
|
|
29
|
+
getPrimaryScrollView?(): ScrollView;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class BtwMainThreadInput implements Component, Focusable {
|
|
33
|
+
private target: Component | undefined;
|
|
34
|
+
private _focused = false;
|
|
35
|
+
private disposed = false;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
initialTarget: Component | null,
|
|
39
|
+
private readonly resolveTarget: () => Component | null,
|
|
40
|
+
private readonly requestRender: () => void,
|
|
41
|
+
) {
|
|
42
|
+
this.target = hasInput(initialTarget) ? initialTarget : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get focused(): boolean {
|
|
46
|
+
return this._focused;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set focused(value: boolean) {
|
|
50
|
+
this._focused = value;
|
|
51
|
+
if (this.target && isFocusable(this.target)) this.target.focused = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get wantsKeyRelease(): boolean {
|
|
55
|
+
return this.target?.wantsKeyRelease ?? false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render(): string[] {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handleInput(data: string): void {
|
|
63
|
+
if (this.disposed) return;
|
|
64
|
+
this.refreshTarget();
|
|
65
|
+
this.target?.handleInput?.(data);
|
|
66
|
+
this.refreshTarget();
|
|
67
|
+
this.requestRender();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
refreshTarget(): void {
|
|
71
|
+
if (this.disposed) return;
|
|
72
|
+
const next = this.resolveTarget();
|
|
73
|
+
if (!hasInput(next) || next === this.target) return;
|
|
74
|
+
if (this._focused && this.target && isFocusable(this.target)) this.target.focused = false;
|
|
75
|
+
this.target = next;
|
|
76
|
+
if (this._focused && isFocusable(next)) next.focused = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
invalidate(): void {}
|
|
80
|
+
|
|
81
|
+
dispose(): void {
|
|
82
|
+
if (this.disposed) return;
|
|
83
|
+
this.disposed = true;
|
|
84
|
+
if (this._focused && this.target && isFocusable(this.target)) this.target.focused = false;
|
|
85
|
+
this.target = undefined;
|
|
86
|
+
this._focused = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface BtwSplitPaneOptions {
|
|
91
|
+
sideComponent: Component;
|
|
92
|
+
sideLayout: Component;
|
|
93
|
+
mainThread: Component;
|
|
94
|
+
mainLayout?: Component;
|
|
95
|
+
mainInput: Component;
|
|
96
|
+
sideScrollView?: ScrollView;
|
|
97
|
+
layout: Exclude<BtwLayout, "fullscreen">;
|
|
98
|
+
theme: Theme;
|
|
99
|
+
terminalColumns(): number;
|
|
100
|
+
terminalRows(): number;
|
|
101
|
+
hasFocusedOverlay(): boolean;
|
|
102
|
+
setFocus(component: Component): void;
|
|
103
|
+
setViewportTarget(scrollView: ScrollView | undefined): void;
|
|
104
|
+
requestRender(): void;
|
|
105
|
+
sidePaneRatio?: number;
|
|
106
|
+
persistSidePaneRatio?(ratio: number): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class BtwSplitPane implements BtwFullscreenLayoutComponent {
|
|
110
|
+
private readonly pasteGuard = new BtwPasteGuard();
|
|
111
|
+
private readonly mainPane: MainThreadPane;
|
|
112
|
+
private readonly layoutRoot: HStack;
|
|
113
|
+
private readonly viewportRouter: PaneViewportRouter;
|
|
114
|
+
private activePane: BtwActivePane = "side";
|
|
115
|
+
private sidePaneRatio: number;
|
|
116
|
+
private persistedSidePaneRatio: number;
|
|
117
|
+
private dividerDragStartRatio: number | undefined;
|
|
118
|
+
private focusGeneration = 0;
|
|
119
|
+
private ratioSaveGeneration = 0;
|
|
120
|
+
private confirmedRatioSaveGeneration = 0;
|
|
121
|
+
private failedRatioSaveGenerationDuringDrag: number | undefined;
|
|
122
|
+
private disposed = false;
|
|
123
|
+
|
|
124
|
+
constructor(private readonly options: BtwSplitPaneOptions) {
|
|
125
|
+
this.sidePaneRatio = clampSidePaneRatio(options.sidePaneRatio ?? DEFAULT_BTW_SIDE_PANE_RATIO);
|
|
126
|
+
this.persistedSidePaneRatio = this.sidePaneRatio;
|
|
127
|
+
this.mainPane = new MainThreadPane(options.mainThread, options.mainLayout, options.theme, options.terminalRows);
|
|
128
|
+
this.viewportRouter = new PaneViewportRouter(
|
|
129
|
+
options.sideScrollView ?? findPrimaryScrollView(options.sideLayout),
|
|
130
|
+
this.mainPane.getPrimaryScrollView(),
|
|
131
|
+
options.setViewportTarget,
|
|
132
|
+
);
|
|
133
|
+
this.viewportRouter.activate("side");
|
|
134
|
+
const separator: Component = {
|
|
135
|
+
render: (width) =>
|
|
136
|
+
Array.from({ length: Math.max(1, options.terminalRows()) }, () => truncateToWidth(this.renderDivider(), width)),
|
|
137
|
+
handleMouse: (event) => this.handleDividerMouse(event),
|
|
138
|
+
invalidate() {},
|
|
139
|
+
};
|
|
140
|
+
const side = options.sideLayout;
|
|
141
|
+
const main = this.mainPane.getLayout();
|
|
142
|
+
this.layoutRoot =
|
|
143
|
+
options.layout === "left-pane"
|
|
144
|
+
? new ResponsivePaneRow(
|
|
145
|
+
side,
|
|
146
|
+
separator,
|
|
147
|
+
main,
|
|
148
|
+
0,
|
|
149
|
+
() => this.sidePaneRatio,
|
|
150
|
+
(width) => this.handleViewportWidth(width),
|
|
151
|
+
)
|
|
152
|
+
: new ResponsivePaneRow(
|
|
153
|
+
main,
|
|
154
|
+
separator,
|
|
155
|
+
side,
|
|
156
|
+
2,
|
|
157
|
+
() => this.sidePaneRatio,
|
|
158
|
+
(width) => this.handleViewportWidth(width),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getFullscreenLayout(): Component {
|
|
163
|
+
return this.layoutRoot;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
handleTerminalInput(data: string): boolean {
|
|
167
|
+
if (this.disposed) return false;
|
|
168
|
+
const pasted = this.pasteGuard.consume(data);
|
|
169
|
+
if (this.options.hasFocusedOverlay()) return false;
|
|
170
|
+
const width = this.terminalColumns();
|
|
171
|
+
if (width < MIN_BTW_SPLIT_COLUMNS && this.activePane !== "side") this.activatePane("side");
|
|
172
|
+
if (pasted) {
|
|
173
|
+
this.forwardPastedInput(data);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (width < MIN_BTW_SPLIT_COLUMNS) return false;
|
|
177
|
+
const pane = paneForMouseClick(data, width, this.options.layout, this.sidePaneRatio);
|
|
178
|
+
if (pane) this.queuePaneFocus(pane);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
render(width: number): string[] {
|
|
183
|
+
if (width <= 0) return [];
|
|
184
|
+
const safeWidth = Math.max(1, width);
|
|
185
|
+
this.handleViewportWidth(safeWidth);
|
|
186
|
+
if (safeWidth < MIN_BTW_SPLIT_COLUMNS) {
|
|
187
|
+
return this.options.sideComponent.render(safeWidth).map((line) => truncateToWidth(line, safeWidth));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { sideWidth, mainWidth } = paneWidths(safeWidth, this.options.layout, this.sidePaneRatio);
|
|
191
|
+
const sideLines = this.options.sideComponent.render(sideWidth);
|
|
192
|
+
const mainLines = this.mainPane.render(mainWidth);
|
|
193
|
+
const rows = Math.max(1, this.options.terminalRows());
|
|
194
|
+
const separator = this.renderDivider();
|
|
195
|
+
const lines: string[] = [];
|
|
196
|
+
for (let index = 0; index < rows; index += 1) {
|
|
197
|
+
const sideLine = padLine(sideLines[index] ?? "", sideWidth);
|
|
198
|
+
const mainLine = padLine(mainLines[index] ?? "", mainWidth);
|
|
199
|
+
lines.push(
|
|
200
|
+
this.options.layout === "left-pane"
|
|
201
|
+
? `${sideLine}${separator}${mainLine}`
|
|
202
|
+
: `${mainLine}${separator}${sideLine}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return lines.map((line) => truncateToWidth(line, safeWidth));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
invalidate(): void {
|
|
209
|
+
this.layoutRoot.invalidate();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
dispose(): void {
|
|
213
|
+
if (this.disposed) return;
|
|
214
|
+
this.disposed = true;
|
|
215
|
+
this.focusGeneration += 1;
|
|
216
|
+
this.dividerDragStartRatio = undefined;
|
|
217
|
+
this.failedRatioSaveGenerationDuringDrag = undefined;
|
|
218
|
+
this.viewportRouter.dispose();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private renderDivider(): string {
|
|
222
|
+
return this.options.theme.fg("borderMuted", "│");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private handleDividerMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
226
|
+
if (this.disposed) return undefined;
|
|
227
|
+
if (this.options.hasFocusedOverlay()) {
|
|
228
|
+
return { handled: true, render: this.cancelDividerDrag() };
|
|
229
|
+
}
|
|
230
|
+
if (event.type === "press" && event.button === "left") {
|
|
231
|
+
if (this.terminalColumns() < MIN_BTW_SPLIT_COLUMNS) return undefined;
|
|
232
|
+
this.focusGeneration += 1;
|
|
233
|
+
this.dividerDragStartRatio = this.sidePaneRatio;
|
|
234
|
+
return { handled: true, capture: true, render: false };
|
|
235
|
+
}
|
|
236
|
+
if (this.dividerDragStartRatio === undefined) return undefined;
|
|
237
|
+
if (event.type === "drag") {
|
|
238
|
+
const changed = this.updateSidePaneRatio(event.screenX);
|
|
239
|
+
return { handled: true, render: changed };
|
|
240
|
+
}
|
|
241
|
+
if (event.type === "release") {
|
|
242
|
+
const changed = this.updateSidePaneRatio(event.screenX);
|
|
243
|
+
const startRatio = this.dividerDragStartRatio;
|
|
244
|
+
const failedDuringDrag = this.failedRatioSaveGenerationDuringDrag === this.ratioSaveGeneration;
|
|
245
|
+
this.dividerDragStartRatio = undefined;
|
|
246
|
+
this.failedRatioSaveGenerationDuringDrag = undefined;
|
|
247
|
+
if (failedDuringDrag && this.sidePaneRatio === this.persistedSidePaneRatio) {
|
|
248
|
+
return { handled: true, render: changed };
|
|
249
|
+
}
|
|
250
|
+
if (this.sidePaneRatio !== startRatio) {
|
|
251
|
+
this.persistCurrentSidePaneRatio();
|
|
252
|
+
return { handled: true, render: changed };
|
|
253
|
+
}
|
|
254
|
+
const restored = failedDuringDrag && this.restorePersistedSidePaneRatio();
|
|
255
|
+
return { handled: true, render: changed || restored };
|
|
256
|
+
}
|
|
257
|
+
return { handled: true, render: false };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private updateSidePaneRatio(dividerColumn: number): boolean {
|
|
261
|
+
const width = this.terminalColumns();
|
|
262
|
+
if (width < MIN_BTW_SPLIT_COLUMNS) return false;
|
|
263
|
+
const contentWidth = width - PANE_DIVIDER_COLUMNS;
|
|
264
|
+
const leftWidth = clamp(Math.floor(dividerColumn), 1, contentWidth - 1);
|
|
265
|
+
const sideWidth = this.options.layout === "left-pane" ? leftWidth : contentWidth - leftWidth;
|
|
266
|
+
const ratio = clampSidePaneRatio(roundPaneRatio(sideWidth / contentWidth));
|
|
267
|
+
if (ratio === this.sidePaneRatio) return false;
|
|
268
|
+
this.sidePaneRatio = ratio;
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private cancelDividerDrag(): boolean {
|
|
273
|
+
const startRatio = this.dividerDragStartRatio;
|
|
274
|
+
if (startRatio === undefined) return false;
|
|
275
|
+
const failedDuringDrag = this.failedRatioSaveGenerationDuringDrag === this.ratioSaveGeneration;
|
|
276
|
+
this.dividerDragStartRatio = undefined;
|
|
277
|
+
this.failedRatioSaveGenerationDuringDrag = undefined;
|
|
278
|
+
return this.setSidePaneRatio(failedDuringDrag ? this.persistedSidePaneRatio : startRatio);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private persistCurrentSidePaneRatio(): void {
|
|
282
|
+
const persist = this.options.persistSidePaneRatio;
|
|
283
|
+
const ratio = this.sidePaneRatio;
|
|
284
|
+
if (!persist) {
|
|
285
|
+
this.persistedSidePaneRatio = ratio;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this.failedRatioSaveGenerationDuringDrag = undefined;
|
|
289
|
+
const saveGeneration = ++this.ratioSaveGeneration;
|
|
290
|
+
void Promise.resolve()
|
|
291
|
+
.then(() => persist(ratio))
|
|
292
|
+
.then(() => {
|
|
293
|
+
if (saveGeneration <= this.confirmedRatioSaveGeneration) return;
|
|
294
|
+
this.confirmedRatioSaveGeneration = saveGeneration;
|
|
295
|
+
this.persistedSidePaneRatio = ratio;
|
|
296
|
+
})
|
|
297
|
+
.catch(() => {
|
|
298
|
+
if (this.disposed || saveGeneration !== this.ratioSaveGeneration) return;
|
|
299
|
+
const dragActive = this.dividerDragStartRatio !== undefined;
|
|
300
|
+
const changed = this.restorePersistedSidePaneRatio();
|
|
301
|
+
if (dragActive) this.failedRatioSaveGenerationDuringDrag = saveGeneration;
|
|
302
|
+
if (changed) this.options.requestRender();
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private restorePersistedSidePaneRatio(): boolean {
|
|
307
|
+
return this.setSidePaneRatio(this.persistedSidePaneRatio);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private setSidePaneRatio(ratio: number): boolean {
|
|
311
|
+
if (ratio === this.sidePaneRatio) return false;
|
|
312
|
+
this.sidePaneRatio = ratio;
|
|
313
|
+
this.layoutRoot.invalidate();
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private queuePaneFocus(pane: BtwActivePane): void {
|
|
318
|
+
const generation = ++this.focusGeneration;
|
|
319
|
+
queueMicrotask(() => {
|
|
320
|
+
if (this.disposed || generation !== this.focusGeneration || this.options.hasFocusedOverlay()) return;
|
|
321
|
+
this.activatePane(this.terminalColumns() < MIN_BTW_SPLIT_COLUMNS ? "side" : pane);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private terminalColumns(): number {
|
|
326
|
+
return Math.max(1, Math.floor(this.options.terminalColumns()));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private handleViewportWidth(width: number): void {
|
|
330
|
+
if (
|
|
331
|
+
this.disposed ||
|
|
332
|
+
width >= MIN_BTW_SPLIT_COLUMNS ||
|
|
333
|
+
this.activePane === "side" ||
|
|
334
|
+
this.options.hasFocusedOverlay()
|
|
335
|
+
) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.activatePane("side");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private activatePane(pane: BtwActivePane): void {
|
|
342
|
+
if (this.disposed) return;
|
|
343
|
+
this.activePane = pane;
|
|
344
|
+
this.viewportRouter.activate(pane);
|
|
345
|
+
this.options.setFocus(pane === "side" ? this.options.sideComponent : this.options.mainInput);
|
|
346
|
+
this.options.requestRender();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private forwardPastedInput(data: string): void {
|
|
350
|
+
const target = this.activePane === "side" ? this.options.sideComponent : this.options.mainInput;
|
|
351
|
+
target.handleInput?.(data);
|
|
352
|
+
this.options.requestRender();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// HStack has no percentage basis, so refresh exact ratio-based widths from its
|
|
357
|
+
// viewport callback before each layout pass and let the side pane fill narrow views.
|
|
358
|
+
class ResponsivePaneRow extends HStack {
|
|
359
|
+
constructor(
|
|
360
|
+
left: Component,
|
|
361
|
+
separator: Component,
|
|
362
|
+
right: Component,
|
|
363
|
+
private readonly sideIndex: 0 | 2,
|
|
364
|
+
private readonly sidePaneRatio: () => number,
|
|
365
|
+
private readonly onViewportWidth: (width: number) => void,
|
|
366
|
+
) {
|
|
367
|
+
super([
|
|
368
|
+
{ component: left, basis: 1, grow: 0, shrink: 0, minSize: 1 },
|
|
369
|
+
{
|
|
370
|
+
component: separator,
|
|
371
|
+
basis: PANE_DIVIDER_COLUMNS,
|
|
372
|
+
grow: 0,
|
|
373
|
+
shrink: 0,
|
|
374
|
+
minSize: PANE_DIVIDER_COLUMNS,
|
|
375
|
+
},
|
|
376
|
+
{ component: right, basis: 1, grow: 0, shrink: 0, minSize: 1 },
|
|
377
|
+
]);
|
|
378
|
+
for (const [index, entry] of this.entries.entries()) {
|
|
379
|
+
entry.visible = (viewport) => {
|
|
380
|
+
if (index === 0) this.resize(viewport.width);
|
|
381
|
+
return index === this.sideIndex || viewport.width >= MIN_BTW_SPLIT_COLUMNS;
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private resize(width: number): void {
|
|
387
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
388
|
+
this.onViewportWidth(safeWidth);
|
|
389
|
+
if (safeWidth < MIN_BTW_SPLIT_COLUMNS) {
|
|
390
|
+
for (const [index, entry] of this.entries.entries()) {
|
|
391
|
+
entry.basis = index === this.sideIndex ? safeWidth : 1;
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const layout = this.sideIndex === 0 ? "left-pane" : "right-pane";
|
|
396
|
+
const { leftWidth, rightWidth } = paneWidths(safeWidth, layout, this.sidePaneRatio());
|
|
397
|
+
const left = this.entries[0];
|
|
398
|
+
const separator = this.entries[1];
|
|
399
|
+
const right = this.entries[2];
|
|
400
|
+
if (left) left.basis = leftWidth;
|
|
401
|
+
if (separator) separator.basis = PANE_DIVIDER_COLUMNS;
|
|
402
|
+
if (right) right.basis = rightWidth;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
class MainThreadPane {
|
|
407
|
+
private readonly body: Component;
|
|
408
|
+
private readonly layout: Component;
|
|
409
|
+
private readonly scroll: ScrollView | undefined;
|
|
410
|
+
|
|
411
|
+
constructor(
|
|
412
|
+
mainThread: Component,
|
|
413
|
+
mainLayout: Component | undefined,
|
|
414
|
+
theme: Theme,
|
|
415
|
+
private readonly terminalRows: () => number,
|
|
416
|
+
) {
|
|
417
|
+
this.body = {
|
|
418
|
+
render: (width) => mainThread.render(Math.max(1, width)).map((line) => truncateToWidth(line, Math.max(1, width))),
|
|
419
|
+
invalidate() {},
|
|
420
|
+
};
|
|
421
|
+
if (mainLayout) {
|
|
422
|
+
this.layout = mainLayout;
|
|
423
|
+
this.scroll = findPrimaryScrollView(mainLayout);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
this.scroll = new ScrollView(this.body, {
|
|
427
|
+
follow: "end",
|
|
428
|
+
scrollbar: "auto",
|
|
429
|
+
scrollbarTrackStyle: (text) => theme.fg("borderMuted", text),
|
|
430
|
+
scrollbarThumbStyle: (text) => theme.fg("muted", text),
|
|
431
|
+
});
|
|
432
|
+
this.layout = this.scroll;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
getLayout(): Component {
|
|
436
|
+
return this.layout;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
getPrimaryScrollView(): ScrollView | undefined {
|
|
440
|
+
return this.scroll;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
render(width: number): string[] {
|
|
444
|
+
if (width <= 0) return [];
|
|
445
|
+
const safeWidth = Math.max(1, width);
|
|
446
|
+
const rows = Math.max(1, this.terminalRows());
|
|
447
|
+
const visible = this.body.render(safeWidth).slice(-rows);
|
|
448
|
+
return [...Array.from({ length: Math.max(0, rows - visible.length) }, () => ""), ...visible];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
class PaneViewportRouter {
|
|
453
|
+
private readonly originalPrimary = new Map<ScrollView, boolean>();
|
|
454
|
+
private disposed = false;
|
|
455
|
+
|
|
456
|
+
constructor(
|
|
457
|
+
private readonly side: ScrollView | undefined,
|
|
458
|
+
private readonly main: ScrollView | undefined,
|
|
459
|
+
private readonly setViewportTarget: (scrollView: ScrollView | undefined) => void,
|
|
460
|
+
) {
|
|
461
|
+
for (const scrollView of [side, main]) {
|
|
462
|
+
if (scrollView) this.originalPrimary.set(scrollView, scrollView.primary);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
activate(pane: BtwActivePane): void {
|
|
467
|
+
if (this.disposed) return;
|
|
468
|
+
const target = pane === "side" ? this.side : this.main;
|
|
469
|
+
for (const scrollView of this.originalPrimary.keys()) setScrollViewPrimary(scrollView, scrollView === target);
|
|
470
|
+
this.setViewportTarget(target);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
dispose(): void {
|
|
474
|
+
if (this.disposed) return;
|
|
475
|
+
this.disposed = true;
|
|
476
|
+
for (const [scrollView, primary] of this.originalPrimary) setScrollViewPrimary(scrollView, primary);
|
|
477
|
+
this.setViewportTarget(undefined);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Pi exposes primary as a constructor-time ScrollView option but has no public
|
|
482
|
+
// active-pane viewport router. Preserve and restore this runtime field so Pi's
|
|
483
|
+
// native search, prompt navigation, and scrolling keep their standard behavior.
|
|
484
|
+
function setScrollViewPrimary(scrollView: ScrollView, primary: boolean): void {
|
|
485
|
+
Reflect.set(scrollView, "primary", primary);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function findPrimaryScrollView(root: Component): ScrollView | undefined {
|
|
489
|
+
const visited = new Set<Component>();
|
|
490
|
+
let fallback: ScrollView | undefined;
|
|
491
|
+
let primary: ScrollView | undefined;
|
|
492
|
+
const visit = (component: Component) => {
|
|
493
|
+
if (visited.has(component)) return;
|
|
494
|
+
visited.add(component);
|
|
495
|
+
if (component instanceof ScrollView) {
|
|
496
|
+
fallback ??= component;
|
|
497
|
+
if (component.primary) primary = component;
|
|
498
|
+
}
|
|
499
|
+
if (!("children" in component) || !Array.isArray(component.children)) return;
|
|
500
|
+
for (const child of component.children) visit(child);
|
|
501
|
+
};
|
|
502
|
+
visit(root);
|
|
503
|
+
return primary ?? fallback;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function paneForMouseClick(
|
|
507
|
+
data: string,
|
|
508
|
+
terminalColumns: number,
|
|
509
|
+
layout: Exclude<BtwLayout, "fullscreen">,
|
|
510
|
+
sidePaneRatio: number,
|
|
511
|
+
): BtwActivePane | undefined {
|
|
512
|
+
const match = SGR_MOUSE_PRESS_PATTERN.exec(data);
|
|
513
|
+
if (!match) return undefined;
|
|
514
|
+
const button = Number.parseInt(match[1] ?? "", 10);
|
|
515
|
+
if ((button & 32) !== 0 || (button & 64) !== 0 || (button & 3) !== 0) return undefined;
|
|
516
|
+
const column = Number.parseInt(match[2] ?? "", 10) - 1;
|
|
517
|
+
if (!Number.isFinite(column) || column < 0 || column >= terminalColumns) return undefined;
|
|
518
|
+
const { leftWidth } = paneWidths(terminalColumns, layout, sidePaneRatio);
|
|
519
|
+
if (column >= leftWidth && column < leftWidth + PANE_DIVIDER_COLUMNS) return undefined;
|
|
520
|
+
const clickedLeft = column < leftWidth;
|
|
521
|
+
if (layout === "left-pane") return clickedLeft ? "side" : "main";
|
|
522
|
+
return clickedLeft ? "main" : "side";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function paneWidths(width: number, layout: Exclude<BtwLayout, "fullscreen">, sidePaneRatio: number) {
|
|
526
|
+
const contentWidth = Math.max(2, width - PANE_DIVIDER_COLUMNS);
|
|
527
|
+
const sideWidth = clamp(
|
|
528
|
+
Math.round(contentWidth * clampSidePaneRatio(sidePaneRatio)),
|
|
529
|
+
1,
|
|
530
|
+
Math.max(1, contentWidth - 1),
|
|
531
|
+
);
|
|
532
|
+
const mainWidth = Math.max(1, contentWidth - sideWidth);
|
|
533
|
+
const leftWidth = layout === "left-pane" ? sideWidth : mainWidth;
|
|
534
|
+
const rightWidth = layout === "left-pane" ? mainWidth : sideWidth;
|
|
535
|
+
return { leftWidth, rightWidth, sideWidth, mainWidth };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function clampSidePaneRatio(ratio: number): number {
|
|
539
|
+
return Number.isFinite(ratio)
|
|
540
|
+
? clamp(ratio, MIN_BTW_SIDE_PANE_RATIO, MAX_BTW_SIDE_PANE_RATIO)
|
|
541
|
+
: DEFAULT_BTW_SIDE_PANE_RATIO;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function roundPaneRatio(ratio: number): number {
|
|
545
|
+
return Math.round(ratio * 10_000) / 10_000;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
549
|
+
return Math.max(minimum, Math.min(maximum, value));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function hasInput(component: Component | null): component is Component {
|
|
553
|
+
return component !== null && typeof component.handleInput === "function";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function padLine(line: string, width: number): string {
|
|
557
|
+
const truncated = truncateToWidth(line, width);
|
|
558
|
+
return `${truncated}${" ".repeat(Math.max(0, width - visibleWidth(truncated)))}`;
|
|
559
|
+
}
|
|
@@ -5,6 +5,29 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.23.0] — 2026-09-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `/multi-account pick` offers only the current account's models without filtering the shared registry or changing Pi core. `/multi-account save-default` saves the current model and effective thinking level together for new sessions, without making automatic failover overwrite startup preferences.
|
|
13
|
+
- Added `resumeAfterAllAccountsRecover` (default `true`) as a separate live-session liveness control. A real quota/rate-limit wall across every compatible account remains armed even when immediate `autoContinue` is disabled, polls recovery independently of footer visibility, and resumes on the first account that is genuinely usable.
|
|
14
|
+
|
|
15
|
+
### Compatibility
|
|
16
|
+
|
|
17
|
+
- Pi and pi-ai are peer dependencies in the tested range >=0.85.1, <0.88.0. Transport imports bind to the running host rather than an extension-local 0.85 copy. CI covers Pi 0.85.1, 0.86.1 and 0.87.1; standalone SDK callers must use their installed pi-ai context format.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Native provider wrappers preserve system instructions and tool definitions on Pi's transcript-based context, avoiding tool-free Anthropic requests and old token-estimator crashes (#65, #66).
|
|
22
|
+
- Partial Codex usage headers preserve account/plan display metadata for the same credential without refreshing old serviceability, credits or missing quota windows. Includes a routing regression for stale `serviceable: true` plus fresh 100% quota (adapted from #67, thanks @HerbertGao).
|
|
23
|
+
- Kimi OAuth and unauthenticated spare slots remain in the login picker but are no longer published as unusable static aliases. Only authenticated API-key aliases are published; existing user models.json entries remain untouched (#68).
|
|
24
|
+
- Updated the Anthropic billing-header Claude Code version to 2.1.280 (#64).
|
|
25
|
+
- A continuation that exhausted its newly selected fallback no longer stops after one hop: the next quota-driven switch receives its own continuation.
|
|
26
|
+
- Asynchronous rejection of Pi's injected follow-up no longer loses the interrupted task; the selected fallback remains armed for a bounded retry.
|
|
27
|
+
- `neverFailoverProviders` now also bypasses foreground startup/input preflights for unmanaged providers. Stale cooldowns, invalidations, or unknown auth no longer silently replace an opted-out route before its request. Automatic switch and pending-resume boundaries enforce the same exemption; explicit manual switches remain available.
|
|
28
|
+
- A bodyless `429 status code (no body)` from an unmanaged provider now retries the same route instead of being treated as provider-wide credit exhaustion. The retry honors `Retry-After` when available and otherwise uses the transient delay, avoiding both an unrelated-provider failover and a false six-hour bench for providers such as Cerebras.
|
|
29
|
+
- Numbered Codex OAuth slots no longer fall back to the public ChatGPT endpoint while the child proxy is enabled. A numbered alias is always registered against the loopback route that swaps in the real credential, from extension load onward, so the child-facing placeholder can never be sent to a provider that cannot read it (`Could not parse your authentication token`). Registration now requires an explicit route, so a future call site cannot silently reintroduce the public fallback.
|
|
30
|
+
|
|
8
31
|
## [1.22.0] — 2026-09-17
|
|
9
32
|
|
|
10
33
|
### Compatibility
|