@herbertgao/pi-extensions 2026.9.12 → 2026.9.14

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.
Files changed (90) hide show
  1. package/README.md +4 -4
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  5. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
  6. package/node_modules/@herbertgao/pi-subagents/package.json +4 -4
  7. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
  8. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
  9. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  10. package/node_modules/@herbertgao/sol-pi/package.json +5 -5
  11. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +2 -0
  12. package/node_modules/@narumitw/pi-btw/README.md +21 -4
  13. package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
  14. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  15. package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
  16. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  17. package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
  18. package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
  19. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
  20. package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
  21. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  22. package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
  23. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
  24. package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
  25. package/node_modules/pi-mcp-adapter/CHANGELOG.md +26 -0
  26. package/node_modules/pi-mcp-adapter/README.md +39 -9
  27. package/node_modules/pi-mcp-adapter/cli.js +33 -22
  28. package/node_modules/pi-mcp-adapter/commands.ts +3 -3
  29. package/node_modules/pi-mcp-adapter/config.ts +9 -1
  30. package/node_modules/pi-mcp-adapter/dist/config.js +9 -1
  31. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  32. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +1 -2
  33. package/node_modules/pi-mcp-adapter/dist/jev-client.js +59 -32
  34. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -1
  35. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +1 -1
  36. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +33 -4
  37. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +148 -28
  38. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -1
  39. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1 -2
  40. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -1
  41. package/node_modules/pi-mcp-adapter/dist/types.d.ts +7 -1
  42. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  43. package/node_modules/pi-mcp-adapter/index.ts +51 -38
  44. package/node_modules/pi-mcp-adapter/jev-client.ts +58 -31
  45. package/node_modules/pi-mcp-adapter/jev-contracts.ts +2 -2
  46. package/node_modules/pi-mcp-adapter/jev-key-store.ts +144 -31
  47. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  48. package/node_modules/pi-mcp-adapter/mcp-code.ts +6 -6
  49. package/node_modules/pi-mcp-adapter/package.json +5 -5
  50. package/node_modules/pi-mcp-adapter/semantic-search.ts +3 -3
  51. package/node_modules/pi-mcp-adapter/types.ts +7 -1
  52. package/node_modules/pi-multi-account/CHANGELOG.md +29 -0
  53. package/node_modules/pi-multi-account/README.md +39 -11
  54. package/node_modules/pi-multi-account/index.ts +371 -96
  55. package/node_modules/pi-multi-account/package.json +5 -4
  56. package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
  57. package/node_modules/pi-multi-account/usage.ts +31 -2
  58. package/node_modules/pi-typesafe/README.md +3 -1
  59. package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
  60. package/node_modules/pi-typesafe/dist/auth.js +13 -7
  61. package/node_modules/pi-typesafe/dist/backends.d.ts +41 -0
  62. package/node_modules/pi-typesafe/dist/backends.js +59 -0
  63. package/node_modules/pi-typesafe/dist/client.d.ts +4 -10
  64. package/node_modules/pi-typesafe/dist/client.js +72 -44
  65. package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
  66. package/node_modules/pi-typesafe/dist/credentials.js +15 -8
  67. package/node_modules/pi-typesafe/dist/errors.d.ts +10 -2
  68. package/node_modules/pi-typesafe/dist/errors.js +28 -7
  69. package/node_modules/pi-typesafe/dist/extension.js +6 -2
  70. package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
  71. package/node_modules/pi-typesafe/dist/index.js +1 -1
  72. package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
  73. package/node_modules/pi-typesafe/dist/login.js +17 -8
  74. package/node_modules/pi-typesafe/dist/schema.js +19 -13
  75. package/node_modules/pi-typesafe/package.json +4 -4
  76. package/node_modules/pi-web-access/CHANGELOG.md +22 -0
  77. package/node_modules/pi-web-access/README.md +21 -8
  78. package/node_modules/pi-web-access/baizhi.ts +124 -0
  79. package/node_modules/pi-web-access/crawl4ai.ts +2 -10
  80. package/node_modules/pi-web-access/curator-page.ts +3 -1
  81. package/node_modules/pi-web-access/curator-server.ts +1 -0
  82. package/node_modules/pi-web-access/dist/index.js +540 -220
  83. package/node_modules/pi-web-access/firecrawl.ts +2 -10
  84. package/node_modules/pi-web-access/gemini-search.ts +5 -10
  85. package/node_modules/pi-web-access/index.ts +134 -23
  86. package/node_modules/pi-web-access/package.json +5 -1
  87. package/node_modules/pi-web-access/storage.ts +1 -0
  88. package/node_modules/pi-web-access/tool-activation.ts +134 -0
  89. package/node_modules/pi-web-access/utils.ts +9 -1
  90. package/package.json +14 -14
@@ -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
+ }
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.37.0] - 2026-09-23
11
+
12
+ ### Highlights
13
+
14
+ - Use Jev semantic search with other System One providers, such as OpenCode Zen, Command Code, or OpenRouter, by setting `SYSTEMONE_ENDPOINT`.
15
+ - Stop agents from installing new MCP servers with `settings.allowInstall: false`.
16
+ - Turn off resource tools for every server with one `settings.exposeResources: false` setting.
17
+ - Start Pi without waiting on MCP servers even when cached tool metadata is missing, with `settings.deferWithMissingMetadata`.
18
+
19
+ ### Added
20
+
21
+ - Jev can send System One requests to any HTTPS provider endpoint set in `SYSTEMONE_ENDPOINT`. TypeSafe stays the default. API keys are stored per endpoint, and an invalid endpoint turns Jev off instead of quietly falling back to TypeSafe. Thanks to [@jagaliano](https://github.com/jagaliano) for [PR #645](https://github.com/nicobailon/pi-mcp-adapter/pull/645).
22
+ - `settings.deferWithMissingMetadata: true` lets Pi start without connecting to MCP servers even when their cached metadata is missing or out of date. Those servers show no tools until the first MCP call loads them. Thanks to [@j62268781-alt](https://github.com/j62268781-alt) for [#641](https://github.com/nicobailon/pi-mcp-adapter/issues/641).
23
+ - `settings.allowInstall: false` blocks agents from installing remote MCP servers with `mcp({ action: "install" })`, for headless or locked-down setups. Thanks to [@gastmaier](https://github.com/gastmaier) for [#638](https://github.com/nicobailon/pi-mcp-adapter/issues/638).
24
+ - `settings.exposeResources: false` hides resource tools for every server. A server's own `exposeResources` setting still wins. Thanks to [@rakesh-vs](https://github.com/rakesh-vs) for [PR #636](https://github.com/nicobailon/pi-mcp-adapter/pull/636).
25
+
26
+ ### Changed
27
+
28
+ - The Jev key command is now `pi-mcp-adapter key set systemone`, and the environment variable is `SYSTEMONE_API_KEY`. The old `key set typesafe` command still works, and `TYPESAFE_API_KEY` still works with the default TypeSafe endpoint.
29
+ - Supports Pi 0.87.
30
+
31
+ ### Fixed
32
+
33
+ - `getMcpOAuthTokensForUrl` no longer returns an expired access token when no refresh token is stored, so other extensions see a signed-out server instead of sending a dead token. Thanks to [@benjaminsirb](https://github.com/benjaminsirb) for [#644](https://github.com/nicobailon/pi-mcp-adapter/issues/644).
34
+ - Tools in `directTools: "search"` mode now stay inactive until a search selects them, even if another extension turns them back on. Tools a search activates stay on until the session ends. Thanks to [@VoidInTheShell](https://github.com/VoidInTheShell) for [PR #640](https://github.com/nicobailon/pi-mcp-adapter/pull/640).
35
+
10
36
  ## [2.36.0] - 2026-09-21
11
37
 
12
38
  ### Highlights