@narumitw/pi-jupyter 0.1.0 → 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
@@ -18,30 +19,37 @@ The preview is a static text rendering of notebook cells and selected text outpu
18
19
  Install from npm globally:
19
20
 
20
21
  ```bash
21
- pi install npm:pi-jupyter
22
+ pi install npm:@narumitw/pi-jupyter
22
23
  ```
23
24
 
24
25
  Or pin a version:
25
26
 
26
27
  ```bash
27
- pi install npm:pi-jupyter@0.1.0
28
+ pi install npm:@narumitw/pi-jupyter@0.1.3
28
29
  ```
29
30
 
30
31
  Install for the current project only:
31
32
 
32
33
  ```bash
33
- pi install npm:pi-jupyter -l
34
+ pi install npm:@narumitw/pi-jupyter -l
34
35
  ```
35
36
 
36
- 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.0
40
+ pi install git:github.com/narumiruna/pi-jupyter@v0.1.3
41
+ ```
42
+
43
+ If you previously installed the unscoped package, remove it before installing the scoped package:
44
+
45
+ ```bash
46
+ pi remove npm:pi-jupyter
47
+ pi install npm:@narumitw/pi-jupyter
40
48
  ```
41
49
 
42
50
  ## Local development install
43
51
 
44
- Use one source at a time. If `npm:pi-jupyter` is installed globally and this repo is also installed locally with `-l`, Pi will load both and report shortcut conflicts.
52
+ Use one source at a time. If `npm:@narumitw/pi-jupyter` is installed globally and this repo is also installed locally with `-l`, Pi will load both and report shortcut conflicts.
45
53
 
46
54
  For temporary local testing, prefer:
47
55
 
@@ -55,13 +63,16 @@ For a persistent project-local install:
55
63
  pi install . -l
56
64
  ```
57
65
 
58
- If you also have the npm package installed globally, remove one of them before starting Pi:
66
+ If you also have an npm package installed globally, remove one source before starting Pi:
59
67
 
60
68
  ```bash
61
69
  # Keep the npm package; remove the project-local package from this repo
62
70
  pi remove . -l
63
71
 
64
72
  # Or keep the local package; remove the global npm package
73
+ pi remove npm:@narumitw/pi-jupyter
74
+
75
+ # If you installed the older unscoped package, remove that too
65
76
  pi remove npm:pi-jupyter
66
77
  ```
67
78
 
@@ -74,12 +85,25 @@ just format
74
85
  pre-commit install
75
86
  ```
76
87
 
77
- Publish to npm. This runs `biome check .` first via the `justfile`, then `npm publish`:
88
+ Publish to npm. This runs `biome check .` first via the `justfile`, then `npm publish --access public`:
78
89
 
79
90
  ```bash
91
+ npm login
80
92
  just publish
81
93
  ```
82
94
 
95
+ If npm requires two-factor authentication, pass the one-time password:
96
+
97
+ ```bash
98
+ just publish 123456
99
+ ```
100
+
101
+ After publishing succeeds, this install command will work:
102
+
103
+ ```bash
104
+ pi install npm:@narumitw/pi-jupyter
105
+ ```
106
+
83
107
  Preview the npm package without publishing:
84
108
 
85
109
  ```bash
@@ -100,6 +124,7 @@ just publish-dry-run
100
124
  - `Shift+F8` — focus preview for scrolling.
101
125
  - `Ctrl+Alt+J` / `Ctrl+Alt+K` — scroll preview down/up without focusing it.
102
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.
103
128
  - In focused preview: `↑`, `↓`, `PgUp`, `PgDn`, `Home` or `j`, `k`, `u`, `d`, `g` scroll; `Esc` or `F8` returns focus to the editor.
104
129
 
105
130
  ## Notes
@@ -107,6 +132,7 @@ just publish-dry-run
107
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.
108
133
 
109
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.
110
136
 
111
137
  If shortcuts conflict with Pi/editor keybindings, use the slash commands:
112
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.0",
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": [