@narumitw/pi-jupyter 0.1.1 → 0.1.4

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 CHANGED
@@ -11,6 +11,7 @@ The preview is a static text rendering of notebook cells and selected text outpu
11
11
  - Watches the selected notebook on disk and refreshes after external saves.
12
12
  - Displays markdown cells, code cells, execution counts, common text outputs/errors, and inline image outputs when the terminal supports images.
13
13
  - Non-capturing by default, so you can keep typing in Pi while the panel stays visible.
14
+ - Mouse-resizable right-side panel: drag the preview's left border to change its width.
14
15
  - Focus mode for scrolling the preview.
15
16
 
16
17
  ## Install
@@ -24,7 +25,7 @@ pi install npm:@narumitw/pi-jupyter
24
25
  Or pin a version:
25
26
 
26
27
  ```bash
27
- pi install npm:@narumitw/pi-jupyter@0.1.1
28
+ pi install npm:@narumitw/pi-jupyter@0.1.3
28
29
  ```
29
30
 
30
31
  Install for the current project only:
@@ -33,10 +34,10 @@ Install for the current project only:
33
34
  pi install npm:@narumitw/pi-jupyter -l
34
35
  ```
35
36
 
36
- If npm returns `E404`, the scoped package has not been published yet. Until then, install from GitHub/tag:
37
+ Install from GitHub/tag instead of npm:
37
38
 
38
39
  ```bash
39
- pi install git:github.com/narumiruna/pi-jupyter@v0.1.1
40
+ pi install git:github.com/narumiruna/pi-jupyter@v0.1.3
40
41
  ```
41
42
 
42
43
  If you previously installed the unscoped package, remove it before installing the scoped package:
@@ -123,6 +124,7 @@ just publish-dry-run
123
124
  - `Shift+F8` — focus preview for scrolling.
124
125
  - `Ctrl+Alt+J` / `Ctrl+Alt+K` — scroll preview down/up without focusing it.
125
126
  - `Ctrl+Alt+D` / `Ctrl+Alt+U` — page down/up without focusing it.
127
+ - Drag the preview panel's left border with the mouse to resize it.
126
128
  - In focused preview: `↑`, `↓`, `PgUp`, `PgDn`, `Home` or `j`, `k`, `u`, `d`, `g` scroll; `Esc` or `F8` returns focus to the editor.
127
129
 
128
130
  ## Notes
@@ -130,6 +132,7 @@ just publish-dry-run
130
132
  PNG outputs are rendered as truecolor ANSI thumbnails, so matplotlib-style `image/png` output is visible in Ghostty even inside the right-side overlay. Other image formats use `@mariozechner/pi-tui` terminal image support when available, otherwise they fall back to an image placeholder.
131
133
 
132
134
  The panel auto-hides on narrow terminals (`< 90` columns). Resize wider if it does not appear.
135
+ Mouse resizing uses standard terminal mouse reporting; if your terminal reserves mouse drag for selection, use Shift-drag (or your terminal's selection modifier) to select text instead.
133
136
 
134
137
  If shortcuts conflict with Pi/editor keybindings, use the slash commands:
135
138
 
@@ -3,11 +3,15 @@ import { readdir, readFile, stat } from "node:fs/promises";
3
3
  import { basename, relative, resolve } from "node:path";
4
4
  import { inflateSync } from "node:zlib";
5
5
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
6
- import type { Component, OverlayHandle, TUI } from "@mariozechner/pi-tui";
6
+ import type { Component, OverlayHandle, OverlayOptions, TUI } from "@mariozechner/pi-tui";
7
7
  import { Image, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
8
8
 
9
9
  const PANEL_ID = "jupyter-preview";
10
10
  const NOTEBOOK_EXT = ".ipynb";
11
+ const DEFAULT_PANEL_WIDTH_PERCENT = 42;
12
+ const MIN_PANEL_WIDTH = 42;
13
+ const MIN_EDITOR_WIDTH = 24;
14
+ const RIGHT_MARGIN = 1;
11
15
 
12
16
  type NotebookCell = {
13
17
  cell_type?: string;
@@ -39,6 +43,8 @@ type PreviewState = {
39
43
  lastMtime?: Date;
40
44
  lastError?: string;
41
45
  model?: Notebook;
46
+ panelWidth?: number;
47
+ resizing?: boolean;
42
48
  };
43
49
 
44
50
  export default function jupyterPreview(pi: ExtensionAPI) {
@@ -53,6 +59,7 @@ export default function jupyterPreview(pi: ExtensionAPI) {
53
59
  let overlayHandle: OverlayHandle | undefined;
54
60
  let closeOverlay: (() => void) | undefined;
55
61
  let requestRender: (() => void) | undefined;
62
+ let removeMouseResize: (() => void) | undefined;
56
63
 
57
64
  async function setNotebookPath(rawPath: string, ctx: ExtensionContext): Promise<void> {
58
65
  const path = resolveNotebookPath(rawPath, ctx.cwd);
@@ -90,6 +97,16 @@ export default function jupyterPreview(pi: ExtensionAPI) {
90
97
  return;
91
98
  }
92
99
 
100
+ const overlayOptions: OverlayOptions = {
101
+ anchor: "right-center",
102
+ width: state.panelWidth ?? `${DEFAULT_PANEL_WIDTH_PERCENT}%`,
103
+ minWidth: MIN_PANEL_WIDTH,
104
+ maxHeight: "96%",
105
+ margin: { right: RIGHT_MARGIN },
106
+ nonCapturing: true,
107
+ visible: (termWidth) => termWidth >= 90,
108
+ };
109
+
93
110
  void ctx.ui
94
111
  .custom<void>(
95
112
  (tui, theme, _keybindings, done) => {
@@ -99,36 +116,33 @@ export default function jupyterPreview(pi: ExtensionAPI) {
99
116
  tui.requestRender();
100
117
  });
101
118
  requestRender = () => tui.requestRender();
119
+ removeMouseResize = installMouseResize(tui, state, overlayOptions, () => tui.requestRender());
102
120
  closeOverlay = () => {
103
121
  state.visible = false;
104
122
  state.focused = false;
123
+ state.resizing = false;
105
124
  done(undefined);
106
125
  };
107
126
  return panel;
108
127
  },
109
128
  {
110
129
  overlay: true,
111
- overlayOptions: {
112
- anchor: "right-center",
113
- width: "42%",
114
- minWidth: 42,
115
- maxHeight: "96%",
116
- margin: { right: 1 },
117
- nonCapturing: true,
118
- visible: (termWidth) => termWidth >= 90,
119
- },
130
+ overlayOptions,
120
131
  onHandle: (handle) => {
121
132
  overlayHandle = handle;
122
133
  },
123
134
  },
124
135
  )
125
136
  .finally(() => {
137
+ removeMouseResize?.();
138
+ removeMouseResize = undefined;
126
139
  overlayHandle = undefined;
127
140
  panel = undefined;
128
141
  closeOverlay = undefined;
129
142
  requestRender = undefined;
130
143
  state.visible = false;
131
144
  state.focused = false;
145
+ state.resizing = false;
132
146
  ctx.ui.setStatus(PANEL_ID, undefined);
133
147
  });
134
148
 
@@ -138,6 +152,7 @@ export default function jupyterPreview(pi: ExtensionAPI) {
138
152
  function hidePanel(ctx?: ExtensionContext): void {
139
153
  state.visible = false;
140
154
  state.focused = false;
155
+ state.resizing = false;
141
156
  ctx?.ui.setStatus(PANEL_ID, undefined);
142
157
  closeOverlay?.();
143
158
  overlayHandle?.hide();
@@ -310,6 +325,85 @@ export default function jupyterPreview(pi: ExtensionAPI) {
310
325
  });
311
326
  }
312
327
 
328
+ type MouseEvent = {
329
+ button: number;
330
+ x: number;
331
+ y: number;
332
+ released: boolean;
333
+ };
334
+
335
+ function installMouseResize(
336
+ tui: TUI,
337
+ state: PreviewState,
338
+ overlayOptions: OverlayOptions,
339
+ requestRender: () => void,
340
+ ): () => void {
341
+ const terminal = tui.terminal;
342
+ const enableMouse = "\x1b[?1000h\x1b[?1002h\x1b[?1006h";
343
+ const disableMouse = "\x1b[?1006l\x1b[?1002l\x1b[?1000l";
344
+ terminal.write(enableMouse);
345
+
346
+ const removeListener = tui.addInputListener((data) => {
347
+ const event = parseSgrMouseEvent(data);
348
+ if (!event) return undefined;
349
+
350
+ const isPrimaryButton = (event.button & 3) === 0;
351
+ const isMotion = (event.button & 32) !== 0;
352
+ const handleX = getPanelLeftBorderX(terminal.columns, state);
353
+
354
+ if (event.released) {
355
+ state.resizing = false;
356
+ requestRender();
357
+ return { consume: true };
358
+ }
359
+
360
+ if (!state.resizing && isPrimaryButton && Math.abs(event.x - handleX) <= 1) {
361
+ state.resizing = true;
362
+ }
363
+
364
+ if (state.resizing && isPrimaryButton && (isMotion || event.x !== handleX)) {
365
+ const nextWidth = clampPanelWidth(terminal.columns - RIGHT_MARGIN - (event.x - 1), terminal.columns);
366
+ state.panelWidth = nextWidth;
367
+ overlayOptions.width = nextWidth;
368
+ requestRender();
369
+ }
370
+
371
+ return { consume: true };
372
+ });
373
+
374
+ return () => {
375
+ state.resizing = false;
376
+ removeListener();
377
+ terminal.write(disableMouse);
378
+ };
379
+ }
380
+
381
+ function parseSgrMouseEvent(data: string): MouseEvent | undefined {
382
+ const prefix = `${String.fromCharCode(27)}[<`;
383
+ if (!data.startsWith(prefix)) return undefined;
384
+ const suffix = data.at(-1);
385
+ if (suffix !== "M" && suffix !== "m") return undefined;
386
+ const parts = data.slice(prefix.length, -1).split(";");
387
+ if (parts.length !== 3) return undefined;
388
+ const [button, x, y] = parts.map((part) => Number.parseInt(part, 10));
389
+ if (![button, x, y].every(Number.isFinite)) return undefined;
390
+ return { button, x, y, released: suffix === "m" };
391
+ }
392
+
393
+ function getPanelLeftBorderX(termWidth: number, state: PreviewState): number {
394
+ const width = clampPanelWidth(
395
+ state.panelWidth ?? Math.floor((termWidth * DEFAULT_PANEL_WIDTH_PERCENT) / 100),
396
+ termWidth,
397
+ );
398
+ const zeroBasedCol = termWidth - RIGHT_MARGIN - width;
399
+ return zeroBasedCol + 1;
400
+ }
401
+
402
+ function clampPanelWidth(width: number, termWidth: number): number {
403
+ const maxWidth = Math.max(MIN_PANEL_WIDTH, termWidth - RIGHT_MARGIN - MIN_EDITOR_WIDTH);
404
+ return Math.max(MIN_PANEL_WIDTH, Math.min(maxWidth, Math.round(width)));
405
+ }
406
+
313
407
  class NotebookPreviewPanel implements Component {
314
408
  constructor(
315
409
  private tui: TUI,
@@ -350,7 +444,7 @@ class NotebookPreviewPanel implements Component {
350
444
  const pathLabel = this.state.path
351
445
  ? relative(this.state.cwd, this.state.path) || basename(this.state.path)
352
446
  : "no notebook";
353
- const title = `${this.state.focused ? "● " : ""}Jupyter Preview`;
447
+ const title = `${this.state.resizing ? "↔ " : this.state.focused ? "● " : ""}Jupyter Preview`;
354
448
  const lines: string[] = [border(`╭${"─".repeat(inner)}╮`), pad(` ${accent(title)} ${dim(pathLabel)}`)];
355
449
  lines.push(border("├") + border("─".repeat(inner)) + border("┤"));
356
450
 
@@ -368,9 +462,11 @@ class NotebookPreviewPanel implements Component {
368
462
  }
369
463
 
370
464
  lines.push(border("├") + border("─".repeat(inner)) + border("┤"));
371
- const footer = this.state.focused
372
- ? " ↑↓ PgUp/PgDn or j/k/u/d scroll • Esc/F8 return"
373
- : " Ctrl+Alt+j/k scroll • Ctrl+Alt+d/u page • Shift+F8 focus";
465
+ const footer = this.state.resizing
466
+ ? " Drag to resize width"
467
+ : this.state.focused
468
+ ? " ↑↓ PgUp/PgDn or j/k/u/d scroll • Esc/F8 return"
469
+ : " Drag left border resize • Ctrl+Alt+j/k scroll • Shift+F8 focus";
374
470
  lines.push(pad(dim(footer)));
375
471
  lines.push(border(`╰${"─".repeat(inner)}╯`));
376
472
  return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-jupyter",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Pi extension: right-side Jupyter notebook preview while editing .ipynb files.",
5
5
  "type": "module",
6
6
  "keywords": [