@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.8

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 (54) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/package.json +7 -7
  3. package/scripts/generate-template.ts +4 -3
  4. package/src/cli/setup-cli.ts +14 -161
  5. package/src/cli/stats-cli.ts +56 -2
  6. package/src/cli.ts +0 -1
  7. package/src/config/settings-schema.ts +0 -10
  8. package/src/eval/eval.lark +30 -10
  9. package/src/eval/js/context-manager.ts +334 -564
  10. package/src/eval/js/shared/helpers.ts +237 -0
  11. package/src/eval/js/shared/indirect-eval.ts +30 -0
  12. package/src/eval/js/shared/rewrite-imports.ts +211 -0
  13. package/src/eval/js/shared/runtime.ts +168 -0
  14. package/src/eval/js/shared/types.ts +18 -0
  15. package/src/eval/js/tool-bridge.ts +2 -4
  16. package/src/eval/js/worker-core.ts +146 -0
  17. package/src/eval/js/worker-entry.ts +24 -0
  18. package/src/eval/js/worker-protocol.ts +41 -0
  19. package/src/eval/parse.ts +218 -49
  20. package/src/eval/py/display.ts +71 -0
  21. package/src/eval/py/executor.ts +74 -89
  22. package/src/eval/py/index.ts +1 -2
  23. package/src/eval/py/kernel.ts +472 -900
  24. package/src/eval/py/prelude.py +95 -7
  25. package/src/eval/py/runner.py +879 -0
  26. package/src/eval/py/runtime.ts +3 -16
  27. package/src/eval/py/tool-bridge.ts +137 -0
  28. package/src/export/html/index.ts +5 -2
  29. package/src/export/html/template.generated.ts +1 -1
  30. package/src/export/html/template.js +93 -5
  31. package/src/export/html/template.macro.ts +4 -3
  32. package/src/internal-urls/docs-index.generated.ts +3 -3
  33. package/src/modes/components/read-tool-group.ts +9 -0
  34. package/src/modes/controllers/command-controller.ts +0 -23
  35. package/src/prompts/tools/eval.md +14 -27
  36. package/src/prompts/tools/read.md +1 -0
  37. package/src/session/agent-session.ts +0 -1
  38. package/src/session/history-storage.ts +77 -19
  39. package/src/tools/browser/tab-protocol.ts +4 -0
  40. package/src/tools/browser/tab-supervisor.ts +86 -5
  41. package/src/tools/browser/tab-worker.ts +104 -58
  42. package/src/tools/conflict-detect.ts +661 -0
  43. package/src/tools/eval.ts +1 -1
  44. package/src/tools/index.ts +6 -0
  45. package/src/tools/path-utils.ts +1 -1
  46. package/src/tools/read.ts +130 -0
  47. package/src/tools/write.ts +204 -0
  48. package/src/web/search/index.ts +6 -4
  49. package/src/cli/jupyter-cli.ts +0 -106
  50. package/src/commands/jupyter.ts +0 -32
  51. package/src/eval/py/cancellation.ts +0 -28
  52. package/src/eval/py/gateway-coordinator.ts +0 -424
  53. /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
  54. /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,75 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.9.8] - 2026-05-12
6
+
7
+ ### Breaking Changes
8
+
9
+ - Changed the `eval` tool input format to a single-line `*** Cell <lang>:"<title>" [t:<duration>] [rst]` header per cell, replacing the `*** Begin <LANG>` / `*** End <LANG>` envelope and the standalone `*** Title:` / `*** Timeout:` / `*** Reset` directives. The lark grammar enforces a fixed attribute order; the runtime parser remains lenient (alias keys, bare positional tokens, single-quoted titles).
10
+
11
+ ### Added
12
+
13
+ - Added `:conflicts` read selector (`read <path>:conflicts`) to return a one-line index of all unresolved merge conflicts with stable `#N` IDs for quick inspection
14
+ - Added bulk conflict resolution with `write({ path: "conflict://*", content })` to resolve all currently registered conflicts across files in one call, expanding `@ours`/`@theirs`/`@base`/`@both` per conflict and returning per-file counts
15
+ - Added `read` support for `conflict://<N>` and `read conflict://<N>/<scope>` to inspect unresolved conflict regions captured by a prior read, including `ours`, `theirs`, and `base` side views with original file line alignment
16
+ - Added shorthand content tokens `@ours`, `@theirs`, `@both`, and `@base` to conflict-resolution writes using `path: "conflict://<N>"` so replacement content can be composed from recorded conflict sections
17
+ - Added conflict count metadata to read results so conflict files now show a warning badge (`⚠ N`) in the read tool UI
18
+ - Added support for explicit boolean `rst` values (`rst:true`, `rst:false`, `rst:1`, `rst:0`, `rst:yes`, `rst:no`, `rst:on`, `rst:off`) in `*** Cell` headers
19
+ - Added detection of unresolved git merge conflicts in `read` output: each marker block is registered with a session-stable id and surfaced in a footer with `ours`/`theirs` previews. Resolve a block by calling `write({ path: "conflict://<id>", content })` — the tool splices the recorded marker region (markers and all sides) with the supplied content and routes through the normal writethrough (LSP format/diagnostics, fs-cache invalidation).
20
+
21
+ ### Changed
22
+
23
+ - Changed `read` conflict warning footers to show `X of Y` unresolved conflicts when a range only captures part of a file and provide a `read <path>:conflicts` hint for the full list
24
+ - Changed conflict scanning in conflict read paths to inspect the whole file (with a 10 MB cap) so totals better reflect hidden conflicts and truncated scans are called out
25
+ - Changed conflict marker scanning during `read` to only register fully formed, column-0 merge-marker blocks, so indented or malformed marker-like lines are no longer treated as conflicts
26
+ - Changed `write` conflict resolution to validate `conflict://` IDs and report clear errors for malformed or unknown conflict URIs
27
+ - Changed the HTML transcript renderer to parse the new `*** Cell` headers while keeping the older `*** Begin <LANG>` and `===== ... =====` formats renderable for historical sessions.
28
+ - Changed the `eval` tool parser so a stray non-marker line between cells no longer crashes with `null is not an object (evaluating 'BEGIN_RE.exec(lines[i])[1]')`; stray content is consumed without aborting parsing.
29
+ - Changed `*** End` to be an optional, undocumented per-cell terminator (kept in the lark to satisfy GPT-trained models' natural terminator habit during constrained sampling).
30
+
31
+ ### Fixed
32
+
33
+ - Fixed single-conflict `write` retries to re-locate the recorded conflict block by exact marker content so shifted line numbers from out-of-band edits no longer prevent resolution
34
+ - Fixed `read conflict://*` handling by rejecting wildcard reads with a clear write-only guidance error
35
+ - Fixed conflict resolution to verify the live file still contains recorded `<<<<<<<` and `>>>>>>>` markers before splicing, preventing stale conflict IDs from silently corrupting out-of-band-edited files
36
+ - Fixed `@base` token handling so two-way conflicts without a base section now return a clear error
37
+ - Improved `*** Cell` header parsing to reject invalid `rst` values with a clear `invalid rst value` error
38
+
39
+ ## [14.9.7] - 2026-05-12
40
+
41
+ ### Breaking Changes
42
+
43
+ - Changed the `timeoutMs` execution option to no longer be enforced during worker-based JS runs, so callers must rely on external cancellation signals for time limits
44
+ - Replaced the Jupyter kernel gateway + WebSocket protocol behind the Python `eval` backend with a subprocess-backed runner that speaks NDJSON over stdin/stdout; removed the `jupyter_kernel_gateway`/`ipykernel` pip dependencies, the `python.sharedGateway` setting, the `omp jupyter` CLI command, and the `PI_PYTHON_GATEWAY_URL` / `PI_PYTHON_GATEWAY_TOKEN` environment variables
45
+
46
+ ### Added
47
+
48
+ - Added Python `tool.<name>(args)` support to `executePython` sessions so evaluated Python code can invoke session tools through the prelude `tool` proxy
49
+ - Added per-execution Python tool bridge session registration and loopback endpoint wiring so Python tool calls resolve to host tools and return tool results
50
+ - Added status-event forwarding for Python tool bridge calls so `tool` invocations can emit execution status updates
51
+ - Added browser-tab JavaScript execution through the shared runtime so tab runs now expose the standard helper globals (`read`, `write`, `sort`, `uniq`, `counter`, `diff`, `tree`, `env`, `output`, `display`, and `tool`)
52
+ - Added static ESM `import` support to browser-tab JavaScript by rewriting top-level imports and resolving them against the tab session context
53
+ - Added substring fallback matching to `HistoryStorage.search` so infix and short-token queries that FTS5 prefix matching misses are still returned
54
+ - Added a live single-line sync progress display to the stats command showing current/total sessions while syncing
55
+ - Added automatic inline JS evaluation fallback when worker creation failed so script execution still works in environments without worker support
56
+
57
+ ### Changed
58
+
59
+ - Changed `setup python` to only verify a reachable Python 3 interpreter instead of installing Jupyter dependencies
60
+ - Changed `info` output to remove the obsolete Python Gateway status block now that shared gateway management is no longer available
61
+ - Changed JavaScript execution in `executeJs` to expose the worker\u2019s real `process` object instead of a restricted, frozen subset
62
+ - Changed JavaScript evaluation to run per session in a worker-backed runner with explicit initialization and teardown handling
63
+ - Changed the Python backend to launch one `python -u runner.py` subprocess per kernel; cancellation now sends `SIGINT` which raises a real `KeyboardInterrupt` in user code, and the same subprocess is reused across cells in session mode
64
+ - Changed Python magic handling so `%pip`, `%cd`, `%env`, `%pwd`, `%ls`, `%time`, `%timeit`, `%who`, `%reset`, `%load`, `%run`, `%%bash`, `%%capture`, `%%timeit`, `%%writefile`, and `!shell` work without depending on IPython
65
+
66
+ ### Fixed
67
+
68
+ - Fixed Python output rendering so `text/markdown` takes precedence over `text/plain` and status bundles are emitted as status updates rather than plain text
69
+ - Fixed query tokenization in `HistoryStorage.search` so punctuation-delimited terms like `git-commit` are aligned with indexing and matched correctly
70
+ - Fixed history search result merging to de-duplicate matches and return full-text matches before substring-only matches while still respecting the requested limit
71
+ - Fixed JS run cancellation so aborting a run now also cancels in-flight tool calls and terminates the active worker session
72
+ - Fixed top-level `const`, `let`, and `class` declarations in evaluated JavaScript to persist across subsequent runs by rewriting top-level declarations
73
+
5
74
  ## [14.9.5] - 2026-05-12
6
75
  ### Breaking Changes
7
76
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.9.5",
4
+ "version": "14.9.8",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -47,12 +47,12 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/omp-stats": "14.9.5",
51
- "@oh-my-pi/pi-agent-core": "14.9.5",
52
- "@oh-my-pi/pi-ai": "14.9.5",
53
- "@oh-my-pi/pi-natives": "14.9.5",
54
- "@oh-my-pi/pi-tui": "14.9.5",
55
- "@oh-my-pi/pi-utils": "14.9.5",
50
+ "@oh-my-pi/omp-stats": "14.9.8",
51
+ "@oh-my-pi/pi-agent-core": "14.9.8",
52
+ "@oh-my-pi/pi-ai": "14.9.8",
53
+ "@oh-my-pi/pi-natives": "14.9.8",
54
+ "@oh-my-pi/pi-tui": "14.9.8",
55
+ "@oh-my-pi/pi-utils": "14.9.8",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@sinclair/typebox": "^0.34.49",
58
58
  "@types/turndown": "5.0.6",
@@ -18,10 +18,11 @@ const minifiedCss = css
18
18
  .replace(/\s*([{}:;,])\s*/g, "$1")
19
19
  .trim();
20
20
 
21
- // Inline everything
21
+ // Inline everything; use function replacements so `$'`, `$&`, `$$`, etc. inside
22
+ // the embedded CSS/JS are not interpreted as substitution patterns.
22
23
  const template = html
23
- .replace("<template-css/>", `<style>${minifiedCss}</style>`)
24
- .replace("<template-js/>", `<script>${js}</script>`);
24
+ .replace("<template-css/>", () => `<style>${minifiedCss}</style>`)
25
+ .replace("<template-js/>", () => `<script>${js}</script>`);
25
26
 
26
27
  // Write generated file
27
28
  const output = `// Auto-generated by scripts/generate-template.ts - DO NOT EDIT
@@ -21,7 +21,6 @@ export interface SetupCommandArgs {
21
21
 
22
22
  const VALID_COMPONENTS: SetupComponent[] = ["python", "stt"];
23
23
 
24
- const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
25
24
  const MANAGED_PYTHON_ENV = getPythonEnvDir();
26
25
 
27
26
  /**
@@ -65,10 +64,6 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
65
64
  interface PythonCheckResult {
66
65
  available: boolean;
67
66
  pythonPath?: string;
68
- uvPath?: string;
69
- pipPath?: string;
70
- missingPackages: string[];
71
- installedPackages: string[];
72
67
  usingManagedEnv?: boolean;
73
68
  managedEnvPath?: string;
74
69
  }
@@ -85,8 +80,6 @@ function managedPythonPath(): string {
85
80
  async function checkPythonSetup(): Promise<PythonCheckResult> {
86
81
  const result: PythonCheckResult = {
87
82
  available: false,
88
- missingPackages: [],
89
- installedPackages: [],
90
83
  managedEnvPath: MANAGED_PYTHON_ENV,
91
84
  };
92
85
 
@@ -94,109 +87,24 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
94
87
  const managedPath = managedPythonPath();
95
88
  const hasManagedEnv = await Bun.file(managedPath).exists();
96
89
 
97
- result.uvPath = $which("uv") ?? undefined;
98
- result.pipPath = $which("pip3") ?? $which("pip") ?? undefined;
99
-
100
- const candidates = [systemPythonPath, hasManagedEnv ? managedPath : undefined].filter(
101
- (candidate): candidate is string => !!candidate,
102
- );
103
- if (candidates.length === 0) {
90
+ const pythonPath = systemPythonPath ?? (hasManagedEnv ? managedPath : undefined);
91
+ if (!pythonPath) {
104
92
  return result;
105
93
  }
106
-
107
- result.pythonPath = systemPythonPath ?? managedPath;
108
- let bestMatch = {
109
- pythonPath: candidates[0],
110
- missingPackages: [...PYTHON_PACKAGES],
111
- installedPackages: [] as string[],
112
- usingManagedEnv: candidates[0] === managedPath,
113
- };
114
-
115
- for (const pythonPath of candidates) {
116
- const installedPackages: string[] = [];
117
- const missingPackages: string[] = [];
118
- for (const pkg of PYTHON_PACKAGES) {
119
- const moduleName = pkg === "jupyter_kernel_gateway" ? "kernel_gateway" : pkg;
120
- const script = `import importlib.util; raise SystemExit(0 if importlib.util.find_spec('${moduleName}') else 1)`;
121
- const check = await $`${pythonPath} -c ${script}`.quiet().nothrow();
122
- if (check.exitCode === 0) {
123
- installedPackages.push(pkg);
124
- } else {
125
- missingPackages.push(pkg);
126
- }
127
- }
128
-
129
- if (missingPackages.length < bestMatch.missingPackages.length) {
130
- bestMatch = {
131
- pythonPath,
132
- missingPackages,
133
- installedPackages,
134
- usingManagedEnv: pythonPath === managedPath,
135
- };
136
- }
137
-
138
- if (missingPackages.length === 0) {
139
- result.available = true;
140
- result.pythonPath = pythonPath;
141
- result.missingPackages = missingPackages;
142
- result.installedPackages = installedPackages;
143
- result.usingManagedEnv = pythonPath === managedPath;
144
- return result;
145
- }
146
- }
147
-
148
- result.pythonPath = bestMatch.pythonPath;
149
- result.missingPackages = bestMatch.missingPackages;
150
- result.installedPackages = bestMatch.installedPackages;
151
- result.usingManagedEnv = bestMatch.usingManagedEnv;
94
+ const probe = await $`${pythonPath} -c "import sys;sys.exit(0)"`.quiet().nothrow();
95
+ result.pythonPath = pythonPath;
96
+ result.available = probe.exitCode === 0;
97
+ result.usingManagedEnv = pythonPath === managedPath;
152
98
  return result;
153
99
  }
154
100
 
155
101
  /**
156
102
  * Install Python packages using uv (preferred) or pip.
157
103
  */
158
- async function installPythonPackages(
159
- packages: string[],
160
- pythonPath: string,
161
- uvPath?: string,
162
- pipPath?: string,
163
- ): Promise<{ success: boolean; usedManagedEnv: boolean }> {
164
- if (uvPath) {
165
- console.log(chalk.dim(`Installing via uv: ${packages.join(" ")}`));
166
- const result = await $`${uvPath} pip install ${packages}`.nothrow();
167
- if (result.exitCode === 0) {
168
- return { success: true, usedManagedEnv: false };
169
- }
170
- }
171
-
172
- if (pipPath) {
173
- console.log(chalk.dim(`Installing via pip: ${packages.join(" ")}`));
174
- const result = await $`${pipPath} install ${packages}`.nothrow();
175
- if (result.exitCode === 0) {
176
- return { success: true, usedManagedEnv: false };
177
- }
178
- }
179
-
180
- console.log(chalk.dim(`Falling back to managed virtual environment: ${MANAGED_PYTHON_ENV}`));
181
-
182
- if (uvPath) {
183
- const createEnv = await $`${uvPath} venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
184
- if (createEnv.exitCode !== 0) {
185
- return { success: false, usedManagedEnv: true };
186
- }
187
- const installInManagedEnv = await $`${uvPath} pip install --python ${MANAGED_PYTHON_ENV} ${packages}`.nothrow();
188
- return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
189
- }
190
-
191
- const createEnv = await $`${pythonPath} -m venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
192
- if (createEnv.exitCode !== 0) {
193
- return { success: false, usedManagedEnv: true };
194
- }
195
-
196
- const managedPython = managedPythonPath();
197
- const installInManagedEnv = await $`${managedPython} -m pip install ${packages}`.nothrow();
198
- return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
199
- }
104
+ // Python installation helper removed: the subprocess runner has no Python
105
+ // package dependencies beyond a working interpreter. `omp setup python --check`
106
+ // remains as a probe; users install optional libs (pandas, matplotlib, ...)
107
+ // directly via pip or the in-process `%pip` magic.
200
108
 
201
109
  /**
202
110
  * Run the setup command.
@@ -232,67 +140,13 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
232
140
  console.log(chalk.dim(`Using managed environment: ${check.managedEnvPath}`));
233
141
  }
234
142
 
235
- if (check.uvPath) {
236
- console.log(chalk.dim(`uv: ${check.uvPath}`));
237
- } else if (check.pipPath) {
238
- console.log(chalk.dim(`pip: ${check.pipPath}`));
239
- }
240
-
241
- if (check.installedPackages.length > 0) {
242
- console.log(chalk.green(`${theme.status.success} Installed: ${check.installedPackages.join(", ")}`));
243
- }
244
-
245
- if (check.missingPackages.length === 0) {
143
+ if (check.available) {
246
144
  console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
247
145
  return;
248
146
  }
249
147
 
250
- console.log(chalk.yellow(`${theme.status.warning} Missing: ${check.missingPackages.join(", ")}`));
251
-
252
- if (flags.check) {
253
- process.exit(1);
254
- }
255
-
256
- if (!check.uvPath && !check.pipPath) {
257
- console.error(chalk.red(`\n${theme.status.error} No package manager found`));
258
- console.error(chalk.dim("Install uv (recommended) or pip:"));
259
- console.error(chalk.dim(" curl -LsSf https://astral.sh/uv/install.sh | sh"));
260
- process.exit(1);
261
- }
262
-
263
- console.log("");
264
- const install = await installPythonPackages(check.missingPackages, check.pythonPath, check.uvPath, check.pipPath);
265
-
266
- if (!install.success) {
267
- console.error(chalk.red(`\n${theme.status.error} Installation failed`));
268
- console.error(chalk.dim("Try installing manually:"));
269
- if (install.usedManagedEnv) {
270
- if (check.uvPath) {
271
- console.error(chalk.dim(` uv venv ${MANAGED_PYTHON_ENV}`));
272
- console.error(
273
- chalk.dim(` uv pip install --python ${MANAGED_PYTHON_ENV} ${check.missingPackages.join(" ")}`),
274
- );
275
- } else {
276
- console.error(chalk.dim(` ${check.pythonPath} -m venv ${MANAGED_PYTHON_ENV}`));
277
- console.error(chalk.dim(` ${managedPythonPath()} -m pip install ${check.missingPackages.join(" ")}`));
278
- }
279
- } else {
280
- console.error(chalk.dim(` ${check.uvPath ? "uv pip" : "pip"} install ${check.missingPackages.join(" ")}`));
281
- }
282
- process.exit(1);
283
- }
284
-
285
- const recheck = await checkPythonSetup();
286
- if (recheck.available) {
287
- console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
288
- if (recheck.usingManagedEnv) {
289
- console.log(chalk.dim(`Managed Python environment: ${recheck.managedEnvPath}`));
290
- }
291
- } else {
292
- console.error(chalk.red(`\n${theme.status.error} Setup incomplete`));
293
- console.error(chalk.dim(`Still missing: ${recheck.missingPackages.join(", ")}`));
294
- process.exit(1);
295
- }
148
+ console.error(chalk.red(`\n${theme.status.error} Python interpreter reported failure`));
149
+ process.exit(1);
296
150
  }
297
151
 
298
152
  async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
@@ -359,9 +213,8 @@ ${chalk.bold("Usage:")}
359
213
  ${APP_NAME} setup <component> [options]
360
214
 
361
215
  ${chalk.bold("Components:")}
362
- python Install Jupyter kernel dependencies for Python code execution
216
+ python Verify a Python 3 interpreter is reachable for code execution
363
217
  stt Install speech-to-text dependencies (openai-whisper, recording tools)
364
- Packages: ${PYTHON_PACKAGES.join(", ")}
365
218
 
366
219
  ${chalk.bold("Options:")}
367
220
  -c, --check Check if dependencies are installed without installing
@@ -8,6 +8,58 @@ import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@oh-my-pi
8
8
  import chalk from "chalk";
9
9
  import { openPath } from "../utils/open";
10
10
 
11
+ /**
12
+ * Single-line TTY progress bar. On a non-TTY stream we just stay quiet -
13
+ * the final "Synced ..." summary still prints either way.
14
+ */
15
+ function createSyncProgressReporter(): {
16
+ onProgress: (event: { current: number; total: number; sessionFile: string }) => void;
17
+ finish: () => void;
18
+ } {
19
+ const stream = process.stderr;
20
+ const isTty = stream.isTTY === true;
21
+ let lastWidth = 0;
22
+ let lastRender = 0;
23
+ return {
24
+ onProgress(event) {
25
+ if (!isTty) return;
26
+ const now = Date.now();
27
+ // Throttle to ~30 fps and always force a render for the last file.
28
+ if (event.current < event.total && now - lastRender < 33) return;
29
+ lastRender = now;
30
+ const label = chalk.dim(shortenSessionFile(event.sessionFile));
31
+ const pct = ((event.current / event.total) * 100).toFixed(0).padStart(3, " ");
32
+ const counter = chalk.cyan(`[${event.current}/${event.total}]`);
33
+ const line = `${counter} ${pct}% ${label}`;
34
+ const columns = stream.columns ?? 120;
35
+ const trimmed = truncateToColumns(line, columns - 1);
36
+ stream.write(`\r${trimmed.padEnd(lastWidth)}`);
37
+ lastWidth = trimmed.length;
38
+ },
39
+ finish() {
40
+ if (!isTty || lastWidth === 0) return;
41
+ stream.write(`\r${" ".repeat(lastWidth)}\r`);
42
+ lastWidth = 0;
43
+ },
44
+ };
45
+ }
46
+
47
+ function shortenSessionFile(p: string): string {
48
+ const marker = "/sessions/";
49
+ const idx = p.indexOf(marker);
50
+ return idx >= 0 ? p.slice(idx + marker.length) : p;
51
+ }
52
+
53
+ function truncateToColumns(s: string, max: number): string {
54
+ if (max <= 0) return "";
55
+ const width = Bun.stringWidth(s, { countAnsiEscapeCodes: false });
56
+ if (width <= max) return s;
57
+ // Cheap right-trim with an ellipsis - we don't need ANSI-aware slicing
58
+ // because the colored prefix is short and the truncated tail is the
59
+ // dim filename, where dropping bytes is fine.
60
+ return `${s.slice(0, Math.max(0, max - 1))}\u2026`;
61
+ }
62
+
11
63
  // =============================================================================
12
64
  // Types
13
65
  // =============================================================================
@@ -74,8 +126,10 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
74
126
  );
75
127
 
76
128
  // Sync session files first
77
- console.log("Syncing session files...");
78
- const { processed, files } = await syncAllSessions();
129
+ const progress = createSyncProgressReporter();
130
+ process.stderr.write("Syncing session files...\n");
131
+ const { processed, files } = await syncAllSessions({ onProgress: progress.onProgress });
132
+ progress.finish();
79
133
  const total = await getTotalMessageCount();
80
134
  console.log(`Synced ${processed} new entries from ${files} files (${total} total)\n`);
81
135
 
package/src/cli.ts CHANGED
@@ -55,7 +55,6 @@ const commands: CommandEntry[] = [
55
55
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
56
56
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
57
57
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
58
- { name: "jupyter", load: () => import("./commands/jupyter").then(m => m.default) },
59
58
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
60
59
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
61
60
  { name: "shell", load: () => import("./commands/shell").then(m => m.default) },
@@ -1662,16 +1662,6 @@ export const SETTINGS_SCHEMA = {
1662
1662
  },
1663
1663
  },
1664
1664
 
1665
- "python.sharedGateway": {
1666
- type: "boolean",
1667
- default: true,
1668
- ui: {
1669
- tab: "editing",
1670
- label: "Shared Python Gateway",
1671
- description: "Share IPython kernel gateway across pi instances",
1672
- },
1673
- },
1674
-
1675
1665
  // ────────────────────────────────────────────────────────────────────────
1676
1666
  // Tools
1677
1667
  // ────────────────────────────────────────────────────────────────────────
@@ -1,16 +1,36 @@
1
- start: cell+
1
+ // Canonical Eval input. Each cell is introduced by a single header line:
2
+ //
3
+ // *** Cell <LANG>:"<title>" [t:<duration>] [rst]
4
+ //
5
+ // Attribute order is fixed: language+title, then optional timeout, then
6
+ // optional reset flag. Title may be empty (`py:""`).
7
+ //
8
+ // Tokens:
9
+ //
10
+ // py:"..." | js:"..." language plus title (required)
11
+ // t:<digits>(ms|s|m)? per-cell timeout (default 30s)
12
+ // rst reset this language's kernel before running
13
+ //
14
+ // Everything between one header line and the next (or the optional trailing
15
+ // `*** End`, or end of input) is the cell's code, verbatim. The runtime
16
+ // parser additionally accepts content before the first header as an implicit
17
+ // default-language cell, but that is lenient fallback and MUST NOT be relied
18
+ // on.
2
19
 
3
- cell: begin_cell attr* code_line* end_cell
4
- begin_cell: "*** Begin " LANG LF
5
- end_cell: "*** End Cell" LF?
20
+ start: cell+ end_marker
6
21
 
7
- attr: title | timeout | reset
8
- title: "*** Title: " /(.+)/ LF
9
- timeout: "*** Timeout: " /\d+(ms|s|m)?/ LF
10
- reset: "*** Reset" LF
22
+ cell: cell_header code_line*
11
23
 
12
- code_line: /[^\r\n]*/ LF
24
+ cell_header: "*** Cell" WS_INLINE LANG_TITLE (WS_INLINE T_ATTR)? (WS_INLINE RST_FLAG)? LF
13
25
 
14
- LANG: "JS" | "TS" | "PY"
26
+ end_marker: "*** End" LF?
27
+
28
+ code_line: CODE_TEXT LF | LF
29
+ CODE_TEXT: /([^*\r\n]|\*\*?[^*\r\n])+\*{0,2}|\*{1,2}/
30
+
31
+ LANG_TITLE: ("py" | "js") ":\"" /[^"\r\n]*/ "\""
32
+ T_ATTR: "t:" /\d+(ms|s|m)?/
33
+ RST_FLAG: "rst"
15
34
 
16
35
  %import common.LF
36
+ %import common.WS_INLINE