@oh-my-pi/pi-coding-agent 14.7.3 → 14.7.5

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 (59) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +7 -7
  3. package/src/cli/read-cli.ts +1 -2
  4. package/src/cli.ts +7 -1
  5. package/src/commands/read.ts +2 -7
  6. package/src/config/settings-schema.ts +0 -5
  7. package/src/edit/modes/hashline.ts +40 -19
  8. package/src/edit/modes/patch.ts +7 -5
  9. package/src/edit/modes/replace.ts +6 -2
  10. package/src/edit/notebook.ts +222 -0
  11. package/src/edit/read-file.ts +7 -0
  12. package/src/edit/renderer.ts +4 -3
  13. package/src/edit/streaming.ts +49 -7
  14. package/src/modes/components/diff.ts +54 -7
  15. package/src/modes/interactive-mode.ts +32 -4
  16. package/src/modes/loop-limit.ts +140 -0
  17. package/src/modes/types.ts +3 -1
  18. package/src/prompts/agents/designer.md +1 -2
  19. package/src/prompts/agents/explore.md +2 -5
  20. package/src/prompts/agents/init.md +1 -4
  21. package/src/prompts/agents/librarian.md +1 -3
  22. package/src/prompts/agents/plan.md +7 -8
  23. package/src/prompts/agents/reviewer.md +1 -2
  24. package/src/prompts/ci-green-request.md +10 -10
  25. package/src/prompts/commands/orchestrate.md +48 -0
  26. package/src/prompts/memories/consolidation.md +10 -10
  27. package/src/prompts/memories/read-path.md +6 -6
  28. package/src/prompts/system/agent-creation-architect.md +54 -44
  29. package/src/prompts/system/custom-system-prompt.md +3 -5
  30. package/src/prompts/system/eager-todo.md +4 -4
  31. package/src/prompts/system/handoff-document.md +7 -4
  32. package/src/prompts/system/plan-mode-active.md +7 -3
  33. package/src/prompts/system/plan-mode-approved.md +5 -5
  34. package/src/prompts/system/summarization-system.md +2 -2
  35. package/src/prompts/system/system-prompt.md +53 -65
  36. package/src/prompts/system/title-system.md +2 -2
  37. package/src/prompts/system/web-search.md +16 -19
  38. package/src/prompts/tools/bash.md +8 -8
  39. package/src/prompts/tools/browser.md +4 -4
  40. package/src/prompts/tools/debug.md +3 -1
  41. package/src/prompts/tools/eval.md +13 -9
  42. package/src/prompts/tools/hashline.md +4 -2
  43. package/src/prompts/tools/image-gen.md +1 -1
  44. package/src/prompts/tools/read.md +1 -2
  45. package/src/prompts/tools/reflect.md +3 -3
  46. package/src/prompts/tools/render-mermaid.md +2 -2
  47. package/src/prompts/tools/resolve.md +2 -2
  48. package/src/prompts/tools/retain.md +3 -2
  49. package/src/prompts/tools/rewind.md +2 -2
  50. package/src/prompts/tools/search-tool-bm25.md +3 -4
  51. package/src/prompts/tools/task.md +1 -1
  52. package/src/slash-commands/builtin-registry.ts +4 -2
  53. package/src/task/commands.ts +5 -1
  54. package/src/tools/fetch.ts +6 -7
  55. package/src/tools/index.ts +0 -4
  56. package/src/tools/read.ts +18 -7
  57. package/src/tools/renderers.ts +0 -2
  58. package/src/tools/write.ts +41 -26
  59. package/src/tools/notebook.ts +0 -286
@@ -1,18 +1,18 @@
1
1
  Drives a real Chromium tab with full puppeteer access via JS execution.
2
2
 
3
3
  <instruction>
4
- - For fetching static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `read` tool with a URL — reader-mode text without spinning up a browser. Use this tool when you need JS execution, authentication, or interactive actions.
4
+ - For static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `read` tool with a URL — reader-mode text without spinning up a browser. Use this tool when you need JS execution, authentication, or interactive actions.
5
5
  - Three actions only:
6
6
  - `open` — acquire (or reuse) a named tab. `name` defaults to `"main"`. Optional `url` navigates after the tab is ready. Optional `viewport` sets dimensions. Optional `dialogs: "accept" | "dismiss"` auto-handles `alert`/`confirm`/`beforeunload` so navigation/clicks don't hang (default: leave dialogs unhandled — page hangs until caller wires `page.on('dialog', …)`).
7
7
  - `close` — release a tab by `name`, or every tab with `all: true`. For spawned-app browsers, set `kill: true` to terminate the process tree (default leaves it running).
8
- - `run` — execute JS against an existing tab. The `code` is the body of an async function with `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. The function's return value is JSON-stringified into the tool result; multiple `display(value)` calls accumulate text/images.
8
+ - `run` — execute JS against an existing tab. `code` is the body of an async function with `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. The function's return value is JSON-stringified into the tool result; multiple `display(value)` calls accumulate text/images.
9
9
  - Tabs survive across `run` calls and across in-process subagents. Open once, reuse many times.
10
10
  - Browser kinds, selected by the `app` field on `open`:
11
11
  - default (no `app`) → headless Chromium with stealth patches.
12
12
  - `app.path` → spawn an absolute binary (Electron/CDP). If a running instance already exposes a CDP port, it is reused; otherwise stale instances are killed and a fresh one is spawned. No stealth patches — never tamper with a real desktop app.
13
13
  - `app.cdp_url` → connect to an existing CDP endpoint (e.g. `http://127.0.0.1:9222`).
14
14
  - `app.target` (with `path`/`cdp_url`) — substring matched against url+title to pick a BrowserWindow when the app exposes several.
15
- - Inside `run`, `tab` exposes high-level helpers; reach for `page` (raw puppeteer Page) when you need anything they don't cover. Available helpers:
15
+ - Inside `run`, `tab` exposes high-level helpers; reach for `page` (raw puppeteer Page) when you need anything they don't cover.
16
16
  - `tab.goto(url, { waitUntil? })` — clears the element cache and navigates.
17
17
  - `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot. Returns `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Element ids are stable until the next observe/goto.
18
18
  - `tab.id(n)` — resolves an element id from the most recent observe to a real `ElementHandle` you can `.click()`, `.type()`, etc.
@@ -66,5 +66,5 @@ Drives a real Chromium tab with full puppeteer access via JS execution.
66
66
  </examples>
67
67
 
68
68
  <output>
69
- Per call: any `display(value)` outputs (text/images) followed by the JSON-stringified return value of the `code` function. `run` always produces at least a status line.
69
+ - Per call: any `display(value)` outputs (text/images) followed by the JSON-stringified return value of the `code` function. `run` always produces at least a status line.
70
70
  </output>
@@ -1,4 +1,5 @@
1
- Provides debugger access through the Debug Adapter Protocol (DAP). Use this to launch or attach debuggers, set breakpoints, step through execution, inspect threads/stack/variables, evaluate expressions, capture program output, and interrupt hung programs.
1
+ Provides debugger access through the Debug Adapter Protocol (DAP).
2
+ Use for launching or attaching debuggers, setting breakpoints, stepping through execution, inspecting threads/stack/variables, evaluating expressions, capturing output, and interrupting hung programs.
2
3
 
3
4
  <instruction>
4
5
  - Prefer over bash for program state, breakpoints, stepping, thread inspection, or interrupting a running process.
@@ -23,6 +24,7 @@ Provides debugger access through the Debug Adapter Protocol (DAP). Use this to l
23
24
  3. `debug(action: "continue")`
24
25
  4. If the program appears hung: `debug(action: "pause")`
25
26
  5. Inspect state with `threads`, `stack_trace`, `scopes`, and `variables`
27
+
26
28
  # Raw debugger command through repl
27
29
  `debug(action: "evaluate", expression: "info registers", context: "repl")`
28
30
  </examples>
@@ -1,27 +1,31 @@
1
- Run code in a persistent kernel, using a series of codeblocks acting as cells.
1
+ Run code in a persistent kernel using codeblock cells.
2
2
 
3
3
  <instruction>
4
- Each cell is introduced by a header line of the form:
4
+ Cell header format:
5
5
 
6
6
  ```
7
7
  ===== <info> =====
8
8
  ```
9
9
 
10
- where each side is at least 5 equal signs. Everything between one header and the next (or end of input) is the cell's code, verbatim. The info is space-separated tokens, all optional, in any order:
11
- - **Language**: {{#if py}}`py` for Python{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`js` / `ts` for JavaScript{{/if}}.{{#ifAll py js}} Omitted → inherit the previous cell's language (the first cell defaults to Python, falling back to JavaScript when Python is unavailable).{{else}} Omitted → inherit the previous cell's language.{{/ifAll}}
10
+ At least 5 equal signs on each side. Content between one header and the next (or end of input) is the cell's code, verbatim.
11
+ - **Language**: {{#if py}}`py` for Python{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`js` / `ts` for JavaScript{{/if}}.{{#ifAll py js}} Omitted → inherit previous cell's language (first cell defaults to Python, falls back to JavaScript).{{else}} Omitted → inherit previous cell's language.{{/ifAll}}
12
12
  - **Title shorthand**: `py:"…"`, `js:"…"`, `ts:"…"` set the language and the cell title together.
13
13
  - **Attributes**:
14
14
  - `id:"…"` — cell title (when language is unchanged or already set).
15
- - `t:<duration>` — per-cell timeout. Duration is digits with optional `ms` / `s` / `m` units (e.g. `t:500ms`, `t:15s`, `t:2m`). Default 30s.
16
- - `rst` — wipe **this cell's own language kernel** before running.{{#ifAll py js}} Other languages are untouched.{{/ifAll}}
15
+ - `t:<duration>` — per-cell timeout. Digits with optional `ms` / `s` / `m` units (e.g., `t:500ms`, `t:15s`, `t:2m`). Default 30s.
16
+ - `rst` — wipe this cell's own language kernel before running.{{#ifAll py js}} Other languages are untouched.{{/ifAll}}
17
17
 
18
- **Work incrementally:** one logical step per cell (imports, define, test, use). Pass multiple small cells in one call. Define small reusable functions you can debug individually. You **MUST** put workflow explanations in the assistant message or cell title — never inside cell code.
18
+ **Work incrementally:**
19
+ - One logical step per cell (imports, define, test, use).
20
+ - Pass multiple small cells in one call.
21
+ - Define small reusable functions for individual debugging.
22
+ - Put workflow explanations in the assistant message or cell title — never inside cell code.
19
23
 
20
24
  **On failure:** errors identify the failing cell (e.g., "Cell 3 failed"). Resubmit only the fixed cell (or fixed cell + remaining cells).
21
25
  </instruction>
22
26
 
23
27
  <prelude>
24
- {{#ifAll py js}}The same helpers are available in both runtimes with the same positional argument order. Python takes the trailing options as keyword args; JavaScript takes the same options as a trailing object literal. JavaScript helpers are async and `await`able; Python helpers run synchronously.{{else}}{{#if py}}Helpers run synchronously. Trailing options are passed as keyword arguments.{{/if}}{{#if js}}Helpers are async and `await`able. Trailing options are passed as a final object literal.{{/if}}{{/ifAll}}
28
+ {{#ifAll py js}}Same helpers in both runtimes with the same positional argument order. Python: trailing options as keyword args. JavaScript: trailing options as a trailing object literal. JavaScript helpers are async and `await`able; Python helpers run synchronously.{{else}}{{#if py}}Helpers run synchronously. Trailing options are keyword arguments.{{/if}}{{#if js}}Helpers are async and `await`able. Trailing options are a final object literal.{{/if}}{{/ifAll}}
25
29
  ```
26
30
  display(value) → None
27
31
  Render a value in the current cell output.
@@ -49,7 +53,7 @@ output(*ids, format?="raw", query?=None, offset?=None, limit?=None) → str | di
49
53
  {{/if}}</prelude>
50
54
 
51
55
  <output>
52
- Cells render like a Jupyter notebook. Pass any value to `display(value)`; non-presentable data is rendered as an interactive JSON tree, and presentable values (figures, images, dataframes, etc.) render with their native representation.
56
+ Cells render like a Jupyter notebook. `display(value)` renders non-presentable data as an interactive JSON tree. Presentable values (figures, images, dataframes, etc.) use their native representation.
53
57
  </output>
54
58
 
55
59
  <caution>
@@ -4,7 +4,7 @@ A patch contains one or more file sections. The first non-blank line of every ed
4
4
  Operations reference lines in the file by their line number and hash, called "Anchors", e.g. `5th`, `123ab`.
5
5
  You **MUST** copy them verbatim from the latest output for the file you're editing.
6
6
 
7
- This format is purely textual. The tool has NO awareness of language, indentation, brackets, fences, or table widths. You are responsible for emitting valid syntax in your replacements/insertions.
7
+ Purely textual format. The tool has NO awareness of language, indentation, brackets, fences, or table widths. Emit valid syntax in replacements/insertions.
8
8
 
9
9
  <ops>
10
10
  @PATH header: subsequent ops apply to PATH
@@ -89,7 +89,7 @@ This format is purely textual. The tool has NO awareness of language, indentatio
89
89
  + {{hrefr 1}}
90
90
  {{hsep}}const DEBUG = false;
91
91
 
92
- If your replacement payload would render with even one unchanged line in the diff, you have the wrong op or the wrong range. Stop and rewrite as `+`/`<`/`-` plus a narrower `=`.
92
+ If your replacement payload would render with even one unchanged line in the diff, you have the wrong op or range. Stop and rewrite as `+`/`<`/`-` plus a narrower `=`.
93
93
  </anti-pattern>
94
94
 
95
95
  <critical>
@@ -98,4 +98,6 @@ If your replacement payload would render with even one unchanged line in the dif
98
98
  - Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
99
99
  - `= A..B` deletes the range; payload is what's written. If a payload edge line already exists immediately outside `A..B`, widen the range to cover it — otherwise it duplicates.
100
100
  - Multiple ops in one patch are cheap. Prefer two narrow ops over one wide `=`.
101
+ - Before choosing a `= A..B` range, mentally delete lines A through B. If that would split an unclosed bracket, paren, brace, or string/template from a line above A, or orphan a closing delimiter that belongs to an opener inside the range, you are bisecting a syntactic construct. Widen the range to a self-contained boundary, or use `+`/`-` instead.
102
+ - `= A..B` removes the range as a unit; the lines immediately outside it remain. If those outside lines form a wrapper (`try {`, `catch`, `if`, `else`, loop delimiters) you do not intend to delete, your payload is inserted inside that wrapper. Make sure the payload remains valid and preserves required behavior like error handling. If you need to change the wrapper itself, include it in the range and reproduce it.
101
103
  </critical>
@@ -1,4 +1,4 @@
1
- Generates or edits images using the configured image provider.
1
+ Generates or edits images.
2
2
 
3
3
  <instructions>
4
4
  - You **MUST** provide a single detailed `subject` prompt for image generation or editing.
@@ -7,7 +7,6 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
7
7
 
8
8
  ## Parameters
9
9
  - `path` — file path or URL (required). Append `:<sel>` for line ranges or raw mode (for example `src/foo.ts:50-200` or `src/foo.ts:raw`).
10
- - `timeout` — seconds, for URLs only
11
10
 
12
11
  ## Selectors
13
12
 
@@ -33,7 +32,7 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
33
32
 
34
33
  # Inspection
35
34
 
36
- Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook files. Can inspect images.
35
+ Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook files. Notebooks are shown as editable `# %% [type] cell:N` text; edits to that text are applied back to the underlying `.ipynb` JSON while preserving notebook metadata where possible. Can inspect images.
37
36
 
38
37
  # Directories & Archives
39
38
 
@@ -1,5 +1,5 @@
1
- Generate a synthesised answer by reasoning over long-term memory. Unlike `recall` (which returns raw entries), `reflect` blends relevant memories into a single coherent response.
1
+ Generate a synthesised answer by reasoning over long-term memory. Unlike `recall`, `reflect` blends relevant memories into a coherent response.
2
2
 
3
- Use for open-ended questions that span many stored facts: "What do you know about this user?", "Summarize project decisions.", "What are my preferences for X?"
3
+ Use for open-ended questions spanning many stored facts: "What do you know about this user?", "Summarize project decisions.", "What are my preferences for X?"
4
4
 
5
- Provide an optional `context` to focus the synthesis on a specific angle or sub-topic.
5
+ Optional `context` parameter focuses the synthesis on a specific angle or sub-topic.
@@ -5,5 +5,5 @@ Parameters:
5
5
  - `config` (optional): JSON render configuration (spacing and layout options).
6
6
  Behavior:
7
7
  - Returns ASCII diagram text.
8
- - Saves full ASCII output to an artifact URL (`artifact://<id>`) when artifact storage is available.
9
- - Returns an error when the Mermaid input is invalid or rendering fails.
8
+ - Saves full output to `artifact://<id>` when storage is available.
9
+ - Returns error when Mermaid input is invalid or rendering fails.
@@ -4,5 +4,5 @@ Resolves a pending preview action by either applying or discarding it.
4
4
  - `"discard"` rejects the pending changes.
5
5
  - `reason` is required and must explain why you chose to apply or discard.
6
6
 
7
- This tool is only valid when a pending action exists (typically after a preview step).
8
- If no pending action exists, the call fails with an error.
7
+ Only valid when a pending action exists (typically after a preview step).
8
+ Call fails with an error when no pending action exists.
@@ -1,5 +1,6 @@
1
1
  Store one or more facts in long-term memory for future sessions.
2
2
 
3
- Use for durable, reusable knowledge: user preferences, project decisions, architectural choices, and anything that would improve future responses if recalled. Ephemeral task state does not belong here.
3
+ Use for durable, reusable knowledge: user preferences, project decisions, architectural choices, anything that improves future responses.
4
+ Ephemeral task state does not belong here.
4
5
 
5
- Each item must be specific and self-contained — include who, what, when, and why. Batch related facts in a single call; they are deduplicated and consolidated together.
6
+ Each item **MUST** be specific and self-contained — include who, what, when, and why. Batch related facts in a single call; they are deduplicated and consolidated.
@@ -1,6 +1,6 @@
1
- Ends an active checkpoint and rewinds context back to that checkpoint, replacing intermediate exploration with your report.
1
+ End an active checkpoint. Rewind context to it, replacing intermediate exploration with your report.
2
2
 
3
- Use this immediately after investigative work started with `checkpoint`.
3
+ Call immediately after `checkpoint`-started investigative work.
4
4
 
5
5
  Requirements:
6
6
  - `report` is **REQUIRED** and must be concise, factual, and actionable.
@@ -1,7 +1,6 @@
1
1
  Search hidden tool metadata to discover and activate tools.
2
2
 
3
- Use this tool when you need a capability that is not currently available in your active tool set. It searches all discoverable tools — including MCP tools and built-in tools that are hidden to save tokens.
4
-
3
+ Activate hidden tools (MCP and built-in) when you need a capability not in your active tool set.
5
4
  {{#if hasDiscoverableMCPServers}}Discoverable MCP servers in this session: {{#list discoverableMCPServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
6
5
  {{#if discoverableMCPToolCount}}Total discoverable tools available: {{discoverableMCPToolCount}}.{{/if}}
7
6
  Input:
@@ -16,7 +15,7 @@ Behavior:
16
15
  - Newly activated tools become available before the next model call in the same overall turn
17
16
 
18
17
  Notes:
19
- - If you are unsure, start with `limit` between 5 and 10 to see a broader set of tools.
18
+ Start with `limit` 510 if unsure.
20
19
  - `query` is matched against tool metadata fields:
21
20
  - `name`
22
21
  - `label`
@@ -25,7 +24,7 @@ Notes:
25
24
  - `description` / `summary`
26
25
  - input schema property keys (`schema_keys`)
27
26
 
28
- This is not repository search, file search, or code search. Use it only for tool discovery.
27
+ Not for repository/file/code search. Tool discovery only.
29
28
 
30
29
  Returns JSON with:
31
30
  - `query`
@@ -5,7 +5,7 @@ Launches subagents to parallelize workflows.
5
5
  - Use `job` (with `poll`) to wait. **MUST NOT** poll `read jobs://` in a loop.
6
6
  {{/if}}
7
7
 
8
- Subagents have no access to your conversation history. Every fact, file path, and decision they need **MUST** be explicit in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
8
+ Subagents have no conversation history. Every fact, file path, and decision they need **MUST** be explicit in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
9
9
 
10
10
  <parameters>
11
11
  - `agent`: agent type for all tasks
@@ -119,8 +119,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
119
119
  name: "loop",
120
120
  description:
121
121
  "Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
122
- handle: async (_command, runtime) => {
123
- await runtime.ctx.handleLoopCommand();
122
+ inlineHint: "[count|duration]",
123
+ allowArgs: true,
124
+ handle: async (command, runtime) => {
125
+ await runtime.ctx.handleLoopCommand(command.args);
124
126
  runtime.ctx.editor.setText("");
125
127
  },
126
128
  },
@@ -9,8 +9,12 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
9
9
  import { loadCapability } from "../discovery";
10
10
  // Embed command markdown files at build time
11
11
  import initMd from "../prompts/agents/init.md" with { type: "text" };
12
+ import orchestrateMd from "../prompts/commands/orchestrate.md" with { type: "text" };
12
13
 
13
- const EMBEDDED_COMMANDS: { name: string; content: string }[] = [{ name: "init.md", content: prompt.render(initMd) }];
14
+ const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
15
+ { name: "init.md", content: prompt.render(initMd) },
16
+ { name: "orchestrate.md", content: prompt.render(orchestrateMd) },
17
+ ];
14
18
 
15
19
  export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
16
20
 
@@ -1220,13 +1220,13 @@ function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: bool
1220
1220
 
1221
1221
  async function buildReadUrlCacheEntry(
1222
1222
  session: ToolSession,
1223
- params: { path: string; timeout?: number; raw?: boolean },
1223
+ params: { path: string; raw?: boolean },
1224
1224
  signal?: AbortSignal,
1225
1225
  options?: { ensureArtifact?: boolean },
1226
1226
  ): Promise<ReadUrlCacheEntry> {
1227
- const { path: url, timeout: rawTimeout = 20, raw = false } = params;
1227
+ const { path: url, raw = false } = params;
1228
1228
 
1229
- const effectiveTimeout = clampTimeout("fetch", rawTimeout);
1229
+ const effectiveTimeout = clampTimeout("fetch", 30);
1230
1230
 
1231
1231
  if (signal?.aborted) {
1232
1232
  throw new ToolAbortError();
@@ -1254,7 +1254,7 @@ async function buildReadUrlCacheEntry(
1254
1254
 
1255
1255
  export async function loadReadUrlCacheEntry(
1256
1256
  session: ToolSession,
1257
- params: { path: string; timeout?: number; raw?: boolean },
1257
+ params: { path: string; raw?: boolean },
1258
1258
  signal?: AbortSignal,
1259
1259
  options?: { ensureArtifact?: boolean; preferCached?: boolean },
1260
1260
  ): Promise<ReadUrlCacheEntry> {
@@ -1291,7 +1291,7 @@ function buildUrlReadOutput(result: FetchRenderResult, content: string): string
1291
1291
 
1292
1292
  export async function executeReadUrl(
1293
1293
  session: ToolSession,
1294
- params: { path: string; timeout?: number; raw?: boolean },
1294
+ params: { path: string; raw?: boolean },
1295
1295
  signal?: AbortSignal,
1296
1296
  ): Promise<AgentToolResult<ReadUrlToolDetails>> {
1297
1297
  let cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
@@ -1345,7 +1345,7 @@ function countNonEmptyLines(text: string): number {
1345
1345
 
1346
1346
  /** Render URL read call (URL preview) */
1347
1347
  export function renderReadUrlCall(
1348
- args: { path?: string; url?: string; timeout?: number; raw?: boolean },
1348
+ args: { path?: string; url?: string; raw?: boolean },
1349
1349
  _options: RenderResultOptions,
1350
1350
  uiTheme: Theme = theme,
1351
1351
  ): Component {
@@ -1355,7 +1355,6 @@ export function renderReadUrlCall(
1355
1355
  const description = `${domain}${path ? ` ${path}` : ""}`.trim();
1356
1356
  const meta: string[] = [];
1357
1357
  if (args.raw) meta.push("raw");
1358
- if (args.timeout !== undefined) meta.push(`timeout:${args.timeout}s`);
1359
1358
  const text = renderStatusLine({ icon: "pending", title: "Read", description, meta }, uiTheme);
1360
1359
  return new Text(text, 0, 0);
1361
1360
  }
@@ -37,7 +37,6 @@ import { HindsightRetainTool } from "./hindsight-retain";
37
37
  import { InspectImageTool } from "./inspect-image";
38
38
  import { IrcTool } from "./irc";
39
39
  import { JobTool } from "./job";
40
- import { NotebookTool } from "./notebook";
41
40
  import { wrapToolWithMetaNotice } from "./output-meta";
42
41
  import { ReadTool } from "./read";
43
42
  import { RecipeTool } from "./recipe";
@@ -80,7 +79,6 @@ export * from "./image-gen";
80
79
  export * from "./inspect-image";
81
80
  export * from "./irc";
82
81
  export * from "./job";
83
- export * from "./notebook";
84
82
  export * from "./read";
85
83
  export * from "./recipe";
86
84
  export * from "./render-mermaid";
@@ -271,7 +269,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
271
269
  find: s => new FindTool(s),
272
270
  search: s => new SearchTool(s),
273
271
  lsp: LspTool.createIf,
274
- notebook: s => new NotebookTool(s),
275
272
  inspect_image: s => new InspectImageTool(s),
276
273
  browser: s => new BrowserTool(s),
277
274
  checkpoint: CheckpointTool.createIf,
@@ -430,7 +427,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
430
427
  if (name === "ast_grep") return session.settings.get("astGrep.enabled");
431
428
  if (name === "ast_edit") return session.settings.get("astEdit.enabled");
432
429
  if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
433
- if (name === "notebook") return session.settings.get("notebook.enabled");
434
430
  if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
435
431
  if (name === "web_search") return session.settings.get("web_search.enabled");
436
432
  // search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
package/src/tools/read.ts CHANGED
@@ -9,6 +9,7 @@ import { Text } from "@oh-my-pi/pi-tui";
9
9
  import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import { type Static, Type } from "@sinclair/typebox";
11
11
  import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../edit/line-hash";
12
+ import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
12
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
14
  import { parseInternalUrl } from "../internal-urls/parse";
14
15
  import type { InternalUrl } from "../internal-urls/types";
@@ -418,7 +419,6 @@ const readSchema = Type.Object({
418
419
  description: 'path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
419
420
  examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com:L1-L40"],
420
421
  }),
421
- timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 20 })),
422
422
  });
423
423
 
424
424
  export type ReadToolInput = Static<typeof readSchema>;
@@ -1084,7 +1084,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1084
1084
  _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
1085
1085
  _toolContext?: AgentToolContext,
1086
1086
  ): Promise<AgentToolResult<ReadToolDetails>> {
1087
- let { path: readPath, timeout } = params;
1087
+ let { path: readPath } = params;
1088
1088
  if (readPath.startsWith("file://")) {
1089
1089
  readPath = expandPath(readPath);
1090
1090
  }
@@ -1098,7 +1098,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1098
1098
  if (parsedUrlTarget.offset !== undefined || parsedUrlTarget.limit !== undefined) {
1099
1099
  const cached = await loadReadUrlCacheEntry(
1100
1100
  this.session,
1101
- { path: parsedUrlTarget.path, timeout, raw: parsedUrlTarget.raw },
1101
+ { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw },
1102
1102
  signal,
1103
1103
  {
1104
1104
  ensureArtifact: true,
@@ -1111,7 +1111,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1111
1111
  entityLabel: "URL output",
1112
1112
  });
1113
1113
  }
1114
- return executeReadUrl(this.session, { path: parsedUrlTarget.path, timeout, raw: parsedUrlTarget.raw }, signal);
1114
+ return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw }, signal);
1115
1115
  }
1116
1116
 
1117
1117
  // Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://)
@@ -1196,7 +1196,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1196
1196
  const ext = path.extname(absolutePath).toLowerCase();
1197
1197
  const _hasEditTool = this.session.hasEditTool ?? true;
1198
1198
  const _language = getLanguageFromPath(absolutePath);
1199
- const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext) || (ext === ".ipynb" && parsed.kind === "raw");
1199
+ const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
1200
1200
  // Read the file based on type
1201
1201
  let content: Array<TextContent | ImageContent> | undefined;
1202
1202
  let details: ReadToolDetails = {};
@@ -1263,8 +1263,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1263
1263
  throw error;
1264
1264
  }
1265
1265
  }
1266
+ } else if (isNotebookPath(absolutePath) && parsed.kind !== "raw") {
1267
+ const { offset, limit } = selToOffsetLimit(parsed);
1268
+ return this.#buildInMemoryTextResult(
1269
+ await readEditableNotebookText(absolutePath, localReadPath),
1270
+ offset,
1271
+ limit,
1272
+ {
1273
+ details: { resolvedPath: absolutePath },
1274
+ sourcePath: absolutePath,
1275
+ entityLabel: "notebook",
1276
+ },
1277
+ );
1266
1278
  } else if (shouldConvertWithMarkit) {
1267
- // Convert document or notebook via markit.
1279
+ // Convert document via markit.
1268
1280
  const result = await convertFileWithMarkit(absolutePath, signal);
1269
1281
  if (result.ok) {
1270
1282
  // Apply truncation to converted content
@@ -1556,7 +1568,6 @@ interface ReadRenderArgs {
1556
1568
  path?: string;
1557
1569
  file_path?: string;
1558
1570
  sel?: string;
1559
- timeout?: number;
1560
1571
  // Legacy fields from old schema — tolerated for in-flight tool calls during transition
1561
1572
  offset?: number;
1562
1573
  limit?: number;
@@ -22,7 +22,6 @@ import { findToolRenderer } from "./find";
22
22
  import { githubToolRenderer } from "./gh-renderer";
23
23
  import { inspectImageToolRenderer } from "./inspect-image-renderer";
24
24
  import { jobToolRenderer } from "./job";
25
- import { notebookToolRenderer } from "./notebook";
26
25
  import { readToolRenderer } from "./read";
27
26
  import { recipeToolRenderer } from "./recipe/render";
28
27
  import { resolveToolRenderer } from "./resolve";
@@ -60,7 +59,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
60
59
  find: findToolRenderer as ToolRenderer,
61
60
  search: searchToolRenderer as ToolRenderer,
62
61
  lsp: lspToolRenderer as ToolRenderer,
63
- notebook: notebookToolRenderer as ToolRenderer,
64
62
  inspect_image: inspectImageToolRenderer as ToolRenderer,
65
63
  read: readToolRenderer as ToolRenderer,
66
64
  job: jobToolRenderer as ToolRenderer,
@@ -9,7 +9,7 @@ import { type Static, Type } from "@sinclair/typebox";
9
9
  import { stripHashlinePrefixes } from "../edit";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
11
  import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
12
- import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
12
+ import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
13
13
  import writeDescription from "../prompts/tools/write.md" with { type: "text" };
14
14
  import type { ToolSession } from "../sdk";
15
15
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
@@ -168,6 +168,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
168
168
  readonly concurrency = "exclusive";
169
169
  readonly loadMode = "discoverable";
170
170
  readonly summary = "Write content to a file (creates or overwrites)";
171
+ readonly intent = (args: Partial<WriteToolInput>) => (args.path ? `writing ${args.path}` : "writing");
171
172
 
172
173
  readonly #writethrough: WritethroughCallback;
173
174
 
@@ -523,52 +524,67 @@ function countLines(text: string): number {
523
524
  return text.split("\n").length;
524
525
  }
525
526
 
526
- function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
527
- const icon = uiTheme.getLangIcon(language);
528
- if (lineCount !== null) {
529
- return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
530
- }
531
- return uiTheme.fg("dim", `${icon}`);
527
+ function formatLineCountSuffix(lineCount: number, uiTheme: Theme): string {
528
+ if (lineCount <= 0) return "";
529
+ return uiTheme.fg("dim", ` · ${lineCount} line${lineCount === 1 ? "" : "s"}`);
532
530
  }
533
531
 
534
532
  function normalizeDisplayText(text: string): string {
535
533
  return text.replace(/\r/g, "");
536
534
  }
537
535
 
538
- function formatStreamingContent(content: string, uiTheme: Theme): string {
536
+ function formatStreamingContent(content: string, language: string | undefined, uiTheme: Theme): string {
539
537
  if (!content) return "";
540
538
  const lines = normalizeDisplayText(content).split("\n");
541
- const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
542
- const hidden = lines.length - displayLines.length;
539
+ const totalLines = lines.length;
540
+ const startIndex = Math.max(0, totalLines - WRITE_STREAMING_PREVIEW_LINES);
541
+ const visibleLines = lines.slice(startIndex);
542
+ const hidden = startIndex;
543
+ const highlighted = highlightCode(visibleLines.join("\n"), language);
544
+ const lineNumberWidth = String(totalLines).length;
543
545
 
544
546
  let text = "\n\n";
545
547
  if (hidden > 0) {
546
- text += uiTheme.fg("dim", `… (${hidden} earlier lines)\n`);
548
+ text += `${uiTheme.fg("dim", `… (${hidden} earlier line${hidden === 1 ? "" : "s"})`)}\n`;
547
549
  }
548
- for (const line of displayLines) {
549
- text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}\n`;
550
+ for (let i = 0; i < highlighted.length; i++) {
551
+ const lineNum = startIndex + i + 1;
552
+ const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}│`);
553
+ const body = replaceTabs(highlighted[i] ?? "");
554
+ text += ` ${gutter}${body}\n`;
550
555
  }
551
556
  text += uiTheme.fg("dim", `… (streaming)`);
552
557
  return text;
553
558
  }
554
559
 
555
- function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme): string {
560
+ function renderContentPreview(
561
+ content: string,
562
+ expanded: boolean,
563
+ language: string | undefined,
564
+ uiTheme: Theme,
565
+ ): string {
556
566
  if (!content) return "";
557
- const lines = normalizeDisplayText(content).split("\n");
558
- const maxLines = expanded ? lines.length : Math.min(lines.length, WRITE_PREVIEW_LINES);
559
- const displayLines = expanded ? lines : lines.slice(-maxLines);
560
- const hidden = lines.length - displayLines.length;
567
+ const rawLines = normalizeDisplayText(content).split("\n");
568
+ const totalLines = rawLines.length;
569
+ const maxLines = expanded ? totalLines : Math.min(totalLines, WRITE_PREVIEW_LINES);
570
+ const visibleLines = rawLines.slice(0, maxLines);
571
+ const highlighted = highlightCode(visibleLines.join("\n"), language);
572
+ const lineNumberWidth = String(maxLines).length;
573
+ const hidden = totalLines - maxLines;
561
574
 
562
575
  let text = "\n\n";
563
- for (const line of displayLines) {
564
- text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}\n`;
576
+ for (let i = 0; i < highlighted.length; i++) {
577
+ const lineNum = i + 1;
578
+ const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}│`);
579
+ const body = replaceTabs(highlighted[i] ?? "");
580
+ text += ` ${gutter}${body}\n`;
565
581
  }
566
582
  if (!expanded && hidden > 0) {
567
583
  const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
568
584
  const moreLine = `${formatMoreItems(hidden, "line")}${hint ? ` ${hint}` : ""}`;
569
585
  text += uiTheme.fg("dim", moreLine);
570
586
  }
571
- return text;
587
+ return text.trimEnd();
572
588
  }
573
589
 
574
590
  export const writeToolRenderer = {
@@ -588,7 +604,7 @@ export const writeToolRenderer = {
588
604
  }
589
605
 
590
606
  // Show streaming preview of content (tail)
591
- text += formatStreamingContent(args.content, uiTheme);
607
+ text += formatStreamingContent(args.content, lang, uiTheme);
592
608
 
593
609
  return new Text(text, 0, 0);
594
610
  },
@@ -606,17 +622,17 @@ export const writeToolRenderer = {
606
622
  const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
607
623
  const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
608
624
  const lineCount = countLines(fileContent);
625
+ const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
609
626
 
610
627
  // Build header with status icon
611
628
  const header = renderStatusLine(
612
629
  {
613
630
  icon: "success",
614
631
  title: "Write",
615
- description: `${langIcon} ${pathDisplay}`,
632
+ description: `${langIcon} ${pathDisplay}${lineSuffix}`,
616
633
  },
617
634
  uiTheme,
618
635
  );
619
- const metadataLine = formatMetadataLine(lineCount, lang ?? "text", uiTheme);
620
636
  const diagnostics = result.details?.diagnostics;
621
637
 
622
638
  let cached: RenderCache | undefined;
@@ -628,8 +644,7 @@ export const writeToolRenderer = {
628
644
  if (cached?.key === key) return cached.lines;
629
645
 
630
646
  let text = header;
631
- text += `\n${metadataLine}`;
632
- text += renderContentPreview(fileContent, expanded, uiTheme);
647
+ text += renderContentPreview(fileContent, expanded, lang, uiTheme);
633
648
 
634
649
  if (diagnostics) {
635
650
  const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>