one-code-extension 0.1.0
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/LICENSE +32 -0
- package/README.md +202 -0
- package/agents/explore.md +24 -0
- package/agents/general-purpose.md +21 -0
- package/agents/plan.md +22 -0
- package/docs/decisions/auto-mode.md +526 -0
- package/docs/decisions/branding.md +127 -0
- package/docs/decisions/code-review-remediation.md +106 -0
- package/docs/decisions/compaction.md +140 -0
- package/docs/decisions/distribution.md +133 -0
- package/docs/decisions/hooks.md +64 -0
- package/docs/decisions/lsp.md +58 -0
- package/docs/decisions/mcp.md +104 -0
- package/docs/decisions/memory-state.md +170 -0
- package/docs/decisions/model-policy.md +126 -0
- package/docs/decisions/modes.md +178 -0
- package/docs/decisions/skills-plugins.md +56 -0
- package/docs/decisions/subagents-workflows.md +225 -0
- package/docs/decisions/system-prompt.md +98 -0
- package/docs/decisions/tools.md +407 -0
- package/docs/decisions/tui.md +202 -0
- package/docs/decisions.md +148 -0
- package/extensions/ask-user/index.ts +124 -0
- package/extensions/ask-user/questions.ts +60 -0
- package/extensions/auto-mode/actions.ts +78 -0
- package/extensions/auto-mode/classifier.ts +388 -0
- package/extensions/auto-mode/config.ts +237 -0
- package/extensions/auto-mode/decision-log.ts +60 -0
- package/extensions/auto-mode/defaults.ts +65 -0
- package/extensions/auto-mode/instructions.ts +74 -0
- package/extensions/auto-mode/model-picker.ts +225 -0
- package/extensions/auto-mode/model-select.ts +178 -0
- package/extensions/auto-mode/paths.ts +109 -0
- package/extensions/auto-mode/pause.ts +96 -0
- package/extensions/auto-mode/prompt.ts +307 -0
- package/extensions/auto-mode/rules.ts +96 -0
- package/extensions/auto-mode/safety-floor.ts +100 -0
- package/extensions/auto-mode/sensitive.ts +137 -0
- package/extensions/auto-mode/shell-analysis.ts +702 -0
- package/extensions/background/index.ts +405 -0
- package/extensions/background/registry.ts +81 -0
- package/extensions/background/wakeup.ts +41 -0
- package/extensions/bash/background.ts +136 -0
- package/extensions/bash/index.ts +149 -0
- package/extensions/branding/assistant-marker.ts +53 -0
- package/extensions/branding/index.ts +366 -0
- package/extensions/branding/prompt-editor.ts +42 -0
- package/extensions/branding/prompt-marker.ts +29 -0
- package/extensions/branding/startup.ts +152 -0
- package/extensions/claude-compat/index.ts +50 -0
- package/extensions/claude-context/index.ts +79 -0
- package/extensions/compaction/index.ts +231 -0
- package/extensions/compaction/prompt.ts +191 -0
- package/extensions/context-management/index.ts +147 -0
- package/extensions/effort/index.ts +216 -0
- package/extensions/effort/slider.ts +253 -0
- package/extensions/exit/index.ts +28 -0
- package/extensions/file-tracker/index.ts +103 -0
- package/extensions/file-tracker/tracker.ts +145 -0
- package/extensions/hooks/debug.ts +53 -0
- package/extensions/hooks/executor.ts +132 -0
- package/extensions/hooks/index.ts +310 -0
- package/extensions/hooks/matcher.ts +72 -0
- package/extensions/hooks/plugin-hooks.ts +79 -0
- package/extensions/hooks/protocol.ts +151 -0
- package/extensions/hooks/settings.ts +163 -0
- package/extensions/hooks/trust.ts +151 -0
- package/extensions/lib/branch-restore.ts +35 -0
- package/extensions/lib/claude-context.ts +148 -0
- package/extensions/lib/deferred.ts +119 -0
- package/extensions/lib/git.ts +14 -0
- package/extensions/lib/memory.ts +219 -0
- package/extensions/lib/model-policy.ts +436 -0
- package/extensions/lib/model-tier.ts +99 -0
- package/extensions/lib/notifications.ts +18 -0
- package/extensions/lib/paths.ts +26 -0
- package/extensions/lib/per-cwd.ts +21 -0
- package/extensions/lib/persisted-output.ts +70 -0
- package/extensions/lib/pi-version.ts +54 -0
- package/extensions/lib/plugins.ts +227 -0
- package/extensions/lib/reminders.ts +212 -0
- package/extensions/lib/scratchpad.ts +68 -0
- package/extensions/lib/tui-render.ts +522 -0
- package/extensions/lsp/client.ts +278 -0
- package/extensions/lsp/format.ts +46 -0
- package/extensions/lsp/index.ts +193 -0
- package/extensions/lsp/protocol.ts +64 -0
- package/extensions/lsp/servers.ts +79 -0
- package/extensions/mcp/client.ts +205 -0
- package/extensions/mcp/config.ts +199 -0
- package/extensions/mcp/index.ts +373 -0
- package/extensions/mcp/schema.ts +161 -0
- package/extensions/memory/index.ts +90 -0
- package/extensions/notebook/index.ts +77 -0
- package/extensions/notebook/notebook.ts +108 -0
- package/extensions/permissions/index.ts +876 -0
- package/extensions/permissions/matcher.ts +455 -0
- package/extensions/permissions/modes.ts +107 -0
- package/extensions/permissions/protected-paths.ts +125 -0
- package/extensions/permissions/settings.ts +98 -0
- package/extensions/plan-mode/index.ts +209 -0
- package/extensions/plan-mode/reminder.ts +33 -0
- package/extensions/plan-mode/slug.ts +28 -0
- package/extensions/plan-mode/viewer.ts +124 -0
- package/extensions/plugins/index.ts +115 -0
- package/extensions/plugins/template.ts +42 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +24 -0
- package/extensions/skill/index.ts +170 -0
- package/extensions/subagents/agents.ts +126 -0
- package/extensions/subagents/child.ts +414 -0
- package/extensions/subagents/default-model.ts +122 -0
- package/extensions/subagents/index.ts +1119 -0
- package/extensions/subagents/model-select.ts +346 -0
- package/extensions/subagents/rpc-turns.ts +123 -0
- package/extensions/subagents/runs.ts +93 -0
- package/extensions/subagents/usage.ts +53 -0
- package/extensions/subagents/worktree.ts +69 -0
- package/extensions/system-prompt/environment.ts +35 -0
- package/extensions/system-prompt/index.ts +54 -0
- package/extensions/system-prompt/template.ts +103 -0
- package/extensions/system-prompt/tiers/common.ts +63 -0
- package/extensions/system-prompt/tiers/frontier.ts +24 -0
- package/extensions/system-prompt/tiers/low.ts +71 -0
- package/extensions/system-prompt/tiers/mid.ts +56 -0
- package/extensions/system-reminder/index.ts +37 -0
- package/extensions/tasks/index.ts +140 -0
- package/extensions/tasks/store.ts +174 -0
- package/extensions/todo/index.ts +133 -0
- package/extensions/tool-search/index.ts +168 -0
- package/extensions/tool-style/index.ts +84 -0
- package/extensions/web/index.ts +43 -0
- package/extensions/web-fetch/extract.ts +139 -0
- package/extensions/web-fetch/index.ts +291 -0
- package/extensions/web-fetch/summarize.ts +78 -0
- package/extensions/workflow/agent-session.ts +368 -0
- package/extensions/workflow/globals.ts +251 -0
- package/extensions/workflow/index.ts +364 -0
- package/extensions/workflow/journal.ts +75 -0
- package/extensions/workflow/permission-gate.ts +71 -0
- package/extensions/workflow/run-manager.ts +306 -0
- package/extensions/workflow/saved-workflows.ts +45 -0
- package/extensions/workflow/script-source.ts +201 -0
- package/extensions/workflow/types.ts +99 -0
- package/extensions/workflow/vm-runtime.ts +31 -0
- package/extensions/workflow/widget.ts +56 -0
- package/extensions/worktree/index.ts +266 -0
- package/extensions/worktree/rewrite.ts +59 -0
- package/package.json +101 -0
- package/themes/one-code-light.json +76 -0
- package/themes/one-code.json +76 -0
- package/types/app.d.ts +5 -0
- package/types/community-packages.d.ts +12 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Tools
|
|
2
|
+
|
|
3
|
+
Part of [Decisions](../decisions.md).
|
|
4
|
+
|
|
5
|
+
## Tool names stay pi-idiomatic (snake_case)
|
|
6
|
+
|
|
7
|
+
pi keys its built-in overrides and typed `tool_call` events on `read`, `bash`,
|
|
8
|
+
`edit`, `write`, `grep`, `find`, `ls`. We register custom tools in the same
|
|
9
|
+
style (`todo_write`, `enter_plan_mode`, `subagent`). Users' Claude Code
|
|
10
|
+
permission rules still work: `matcher.ts` maps `Bash`, `Glob`, `WebFetch`,
|
|
11
|
+
`Task`, … onto our names. pi's Anthropic OAuth mode separately renames tools to
|
|
12
|
+
Claude Code's casing on the wire, so nothing is lost there either.
|
|
13
|
+
|
|
14
|
+
## Deferred tools (ToolSearch)
|
|
15
|
+
|
|
16
|
+
`extensions/lib/deferred.ts` holds a registry; any extension defers its own
|
|
17
|
+
tools by emitting `pi-claude-code:defer-tool` on the event bus. `tool-search`
|
|
18
|
+
deactivates them at session start, announces the names in an every-turn
|
|
19
|
+
`<system-reminder>` (as Claude Code does), and activates matches additively so
|
|
20
|
+
pi can use native deferred loading.
|
|
21
|
+
|
|
22
|
+
**Load order is load-bearing:** `tool-search` must appear before any extension
|
|
23
|
+
that defers a tool, because those emit during extension loading and pi's event
|
|
24
|
+
bus only delivers to already-registered listeners. Deferred tools also omit
|
|
25
|
+
`promptSnippet`/`promptGuidelines` — activating a tool that has them rebuilds
|
|
26
|
+
the system prompt and invalidates the cached prefix, defeating the purpose.
|
|
27
|
+
|
|
28
|
+
Verified on OpenAI (gpt-5.5): after `tool_search`, pi injected native
|
|
29
|
+
`tool_search_call` / `tool_search_output` items at the load point rather than
|
|
30
|
+
appending the schema to the request's tool array, and the model then called the
|
|
31
|
+
tool successfully.
|
|
32
|
+
|
|
33
|
+
Verified on Anthropic (2026-08-05, API key): both pi-owned paths, with
|
|
34
|
+
request-payload capture. Native (`claude-sonnet-5`, and any
|
|
35
|
+
`supportsToolReferences` model — first-party opus/sonnet/fable ≥5.4-ish, never
|
|
36
|
+
haiku): before activation the deactivated tools are simply absent from
|
|
37
|
+
`tools`; after `tool_search` activates `web_fetch`, the next request carries it
|
|
38
|
+
in `tools` with `defer_loading: true`. Fallback (`claude-haiku-4-5`): the
|
|
39
|
+
activated tool is appended as a full definition, complete `input_schema`, no
|
|
40
|
+
`defer_loading` flag. Both runs completed normally.
|
|
41
|
+
|
|
42
|
+
## Web tools
|
|
43
|
+
|
|
44
|
+
**Search — kept as a dependency.** `web_search` comes from **`pi-web-search`**:
|
|
45
|
+
it calls the *current model provider's own* search API (OpenAI/Codex, Anthropic,
|
|
46
|
+
Gemini), so no extra API key and no scraping. At 1,610 lines with **zero runtime
|
|
47
|
+
dependencies**, what it encodes is knowledge — three providers' search API shapes
|
|
48
|
+
— rather than machinery, and tracking those ourselves would be a worse trade.
|
|
49
|
+
It also registers Gemini-only `url_context`. Both tools are deferred.
|
|
50
|
+
|
|
51
|
+
One coupling to preserve: pi-web-search drives `setActiveTools` to hide
|
|
52
|
+
`url_context` on non-Gemini models. That composes with our deferral **only
|
|
53
|
+
because `tool-search` loads first**, so deferred tools are already deactivated
|
|
54
|
+
when pi-web-search snapshots the active set.
|
|
55
|
+
|
|
56
|
+
**Fetch — replaced with our own.** `extensions/web-fetch/` (~250 lines) replaces
|
|
57
|
+
`pi-web-access`, which was the heaviest thing in the tree: 21,719 lines, 7.5 MB,
|
|
58
|
+
seven runtime dependencies, and **114 of our then-208 installed packages** — to
|
|
59
|
+
supply one tool we used. Its other three tools were unused, its `web_search` was
|
|
60
|
+
deliberately overridden, and it spawned processes for GitHub cloning, video
|
|
61
|
+
extraction, and reading the browser's cookie store. That is a lot of unreviewed
|
|
62
|
+
surface and startup weight for an HTTP GET.
|
|
63
|
+
|
|
64
|
+
Ours keeps three focused dependencies (`@mozilla/readability`, `linkedom`,
|
|
65
|
+
`turndown`) because HTML-to-markdown quality genuinely affects what the model
|
|
66
|
+
reads; a regex stripper produces poor input on real documentation. Total install
|
|
67
|
+
went from 208 packages to 109.
|
|
68
|
+
|
|
69
|
+
Claude Code parity notes for `web_fetch`: http is upgraded to https,
|
|
70
|
+
non-web schemes are refused, responses are cached 15 minutes, and cross-host
|
|
71
|
+
redirects are reported rather than followed (so a redirect cannot quietly take
|
|
72
|
+
the agent elsewhere; same-host ones are followed).
|
|
73
|
+
|
|
74
|
+
**Deliberate deviation:** Claude Code answers a `prompt` against the page using a
|
|
75
|
+
small fast model. pi exposes no clean in-process completion helper, and a
|
|
76
|
+
summarisation call that fails silently would degrade quality invisibly — the
|
|
77
|
+
exact failure shape we have been removing. Instead the tool returns extracted
|
|
78
|
+
markdown windowed to 30k characters and reports `nextOffset`, so the model reads
|
|
79
|
+
the page itself and can page through a long one.
|
|
80
|
+
|
|
81
|
+
## AskUserQuestion — our own
|
|
82
|
+
|
|
83
|
+
`extensions/ask-user/` replaces the community `pi-ask-user`. That package worked
|
|
84
|
+
and had a nicer overlay UI, but it carried two problems:
|
|
85
|
+
|
|
86
|
+
1. It pulled **`@sinclair/typebox@0.34`** — a second, different schema library,
|
|
87
|
+
since pi itself uses `typebox@1.x` — and built its tool schema with it. It
|
|
88
|
+
worked, but two schema implementations in one process is a silent-divergence
|
|
89
|
+
risk, not a visible one.
|
|
90
|
+
2. It asked **one question per call**, while Claude Code's AskUserQuestion takes
|
|
91
|
+
up to four questions, each with `header`, optional `multiSelect`, and 2–4
|
|
92
|
+
labelled options with descriptions.
|
|
93
|
+
|
|
94
|
+
Ours matches Claude Code's schema and drops the duplicate dependency, at the cost
|
|
95
|
+
of a plainer presentation: pi has no native multi-question widget, so questions
|
|
96
|
+
are asked one dialog at a time via `ctx.ui.select`, with a `✓` marker
|
|
97
|
+
accumulating multi-select choices, a "Done selecting" entry, and an automatic
|
|
98
|
+
"Other (type your own answer)" escape hatch routed to `ctx.ui.input`. Cancelling
|
|
99
|
+
any question cancels the batch, since later answers are meaningless without the
|
|
100
|
+
earlier ones. Non-interactive sessions get an instruction to ask in the reply
|
|
101
|
+
instead.
|
|
102
|
+
|
|
103
|
+
Verified over RPC: a two-question call produced three dialogs — single-select,
|
|
104
|
+
then multi-select showing `✓ Caching` on the second pass — and returned both
|
|
105
|
+
answers in one structured result.
|
|
106
|
+
|
|
107
|
+
## web_fetch answers a `prompt` with a reader model
|
|
108
|
+
|
|
109
|
+
The original deviation ("no clean in-process completion helper; a summariser
|
|
110
|
+
that fails silently degrades quality invisibly") went stale on both halves: the
|
|
111
|
+
auto-mode classifier has been making exactly this call via pi-ai's
|
|
112
|
+
`completeSimple` for months, and the silent-failure objection argues for a
|
|
113
|
+
*loud fallback*, not for omitting the feature. So `web_fetch` now takes Claude
|
|
114
|
+
Code's optional `prompt`, and a reader model answers it against up to 120k
|
|
115
|
+
chars of the page — four times the window the main model would get — returning
|
|
116
|
+
just the answer with a header naming the reader.
|
|
117
|
+
|
|
118
|
+
The reader reuses the **classifier role profile** rather than adding a third
|
|
119
|
+
curated inventory: summarisation wants the same small-but-capable floor, and
|
|
120
|
+
two lists drifting apart helps nobody. Same containment (the page and the
|
|
121
|
+
query go to the reader, so it never leaves the session's provider/family) and
|
|
122
|
+
the same cost ceiling (never pricier than the session model); with no vetted
|
|
123
|
+
smaller model the session model reads the page itself, which still wins by
|
|
124
|
+
keeping the full page out of the conversation. `reasoning` and `temperature`
|
|
125
|
+
are not sent, for the same fail-closed reasons recorded for the classifier.
|
|
126
|
+
Any reader failure returns the raw windowed markdown with a note naming the
|
|
127
|
+
error — the fetch is never wasted and the degradation is never silent. Page
|
|
128
|
+
content rides in the user message tagged as untrusted data, and the system
|
|
129
|
+
prompt pins the reader to extraction.
|
|
130
|
+
|
|
131
|
+
Verified live on an OpenAI Codex Sol session (one-shot `--mode json` run with
|
|
132
|
+
`--dangerously-skip-permissions`; the tool's permission surface is unchanged by
|
|
133
|
+
this feature): the reader resolved to `openai-codex/gpt-5.6-luna` via the
|
|
134
|
+
classifier profile, and the tool result read "Answered by
|
|
135
|
+
openai-codex/gpt-5.6-luna from the full page" with a correct answer for the
|
|
136
|
+
fetched page. Typecheck and all 698 unit tests pass.
|
|
137
|
+
|
|
138
|
+
## Tool errors fail loud, never soft-fallback
|
|
139
|
+
|
|
140
|
+
A tool's `execute()` handler must, on a malformed / ambiguous / missing-required
|
|
141
|
+
call, return `isError: true` with the **corrective action named** — never a
|
|
142
|
+
plausible-but-wrong success. Reserve a non-error empty result for a genuine
|
|
143
|
+
no-op (an unsupported filetype, an intentionally cleared list), not for "the
|
|
144
|
+
operation failed" or "you called me wrong."
|
|
145
|
+
|
|
146
|
+
The archetype: `subagent` with run options but no `agent` silently returned the
|
|
147
|
+
agent catalog. A capable model self-corrects from that; a weaker one
|
|
148
|
+
(**observed on deepseek-v4-flash**, 2026-08-07) reads the catalog as a
|
|
149
|
+
non-sequitur, gets stuck, and — worse — invents a wrong cause (it blamed the
|
|
150
|
+
auto-mode gate). The failure mode is general: one response shape serving two
|
|
151
|
+
intents, a real failure reported as success (`isError` unset), or an error
|
|
152
|
+
message that states no next step. All three send a weak model chasing the wrong
|
|
153
|
+
fix.
|
|
154
|
+
|
|
155
|
+
A read-only audit of every `pi.registerTool` `execute()` (four parallel Sonnet
|
|
156
|
+
agents, one per tool group) found the same family across twelve tools; the
|
|
157
|
+
high/medium set was fixed in one pass:
|
|
158
|
+
|
|
159
|
+
- **Ambiguous overload** (like the archetype): `skill` (`args` without `skill`),
|
|
160
|
+
`list_mcp_resources` (bad `server` name read as "no resources"),
|
|
161
|
+
`enter_worktree` (empty `path` string fell through to *create*).
|
|
162
|
+
- **Silent failure / `isError` unset on a real failure**: `workflow`'s `agent()`
|
|
163
|
+
swallowed a `WorkflowScriptError` (script-config mistake) into a `null` the way
|
|
164
|
+
a genuine agent failure resolves — now it rethrows, matching `parallel()` /
|
|
165
|
+
`pipeline()`; `tool_search` dropped typo'd `select:` names and reported the rest
|
|
166
|
+
"Loaded", and its zero-match branch was a non-error — now unmatched names are
|
|
167
|
+
surfaced and a total miss is `isError: true`; `lsp_diagnostics` after a
|
|
168
|
+
mid-session server *crash* fell through to "install `<command>`" for an
|
|
169
|
+
already-installed server, and a start failure was reported as a clean "no
|
|
170
|
+
diagnostics" — now the crash reason is recorded and a recognised-but-unavailable
|
|
171
|
+
language is an error (only a truly unsupported filetype stays non-error).
|
|
172
|
+
- **Destructive-before-validate**: `schedule_wakeup` cancelled the pending wakeup
|
|
173
|
+
*before* validating params, so a malformed reschedule silently killed a running
|
|
174
|
+
`/loop` — validation now runs first and a rejected call leaves the loop intact.
|
|
175
|
+
- **Misleading cause**: `exit_plan_mode` called cold blamed "the plan file is
|
|
176
|
+
empty" instead of "you're not in plan mode"; `task_update` reported a
|
|
177
|
+
self-reference as "Unknown task id"; `send_message` to an unstarted run said
|
|
178
|
+
"No agent named …" in the same words as the catalog, hiding that the fix is to
|
|
179
|
+
spawn the run first.
|
|
180
|
+
|
|
181
|
+
Deliberately left for later (logged, not silently dropped): the **lows**
|
|
182
|
+
(`send_message`'s `""`-prefix matching the only run; `monitor`'s unwrapped bad
|
|
183
|
+
`ws.url`; `notebook_edit`'s raw `ENOENT`; `schedule_wakeup`'s silent clamp) and
|
|
184
|
+
two **vendor** gaps outside our code — `pi-web-search` never sets `isError`
|
|
185
|
+
(would need a wrapper in `web/index.ts`), and `mcp/client.ts` swallows
|
|
186
|
+
`listTools`/`listResources` failures into empty arrays.
|
|
187
|
+
|
|
188
|
+
## Harness discipline (divergences found by self-comparison)
|
|
189
|
+
|
|
190
|
+
Comparing our implementation against the real Claude Code harness *as observed
|
|
191
|
+
from inside a session* surfaced behaviours that reading a request payload would
|
|
192
|
+
never reveal, because they are enforcement, not prompting.
|
|
193
|
+
|
|
194
|
+
**File freshness — `extensions/file-tracker/`.** Claude Code refuses to edit a
|
|
195
|
+
file the model has not read, refuses to write over an unread existing file,
|
|
196
|
+
rejects an edit when the file changed after the read, and reports out-of-band
|
|
197
|
+
changes with a line-numbered excerpt. This exists to prevent lost updates, and it
|
|
198
|
+
is why Claude Code can tell the model "do not re-read a file you just edited — the
|
|
199
|
+
harness tracks file state for you". We now do the same.
|
|
200
|
+
|
|
201
|
+
Tracking is **by content, not by tool call**, which is the load-bearing detail:
|
|
202
|
+
it catches writes made through bash that no tool hook can see. (In the session
|
|
203
|
+
that prompted this, the reminders I received were triggered by my own `python3`
|
|
204
|
+
edits.)
|
|
205
|
+
|
|
206
|
+
One conflict worth recording, because the first implementation had it backwards:
|
|
207
|
+
announcing an external change must **not** mark the file as read. If the pre-turn
|
|
208
|
+
scan records the new content, the file becomes "fresh" and the stale-edit guard
|
|
209
|
+
stops firing — so the harness would helpfully report the change and then permit
|
|
210
|
+
the clobbering edit. Notifications are tracked separately from reads, purely to
|
|
211
|
+
suppress repeat warnings.
|
|
212
|
+
|
|
213
|
+
**Denial feedback carries the user's words.** The prompt option says "No, tell the
|
|
214
|
+
agent what to do differently", so it now actually asks, and the typed reason is
|
|
215
|
+
appended to the block reason (`The user said: …`). Previously the option promised
|
|
216
|
+
something the code did not deliver.
|
|
217
|
+
|
|
218
|
+
**State-driven nudges.** Claude Code injects a reminder when the task tools have
|
|
219
|
+
gone unused. `todo_write` now does the same after eight quiet turns, and also
|
|
220
|
+
flags a list with no `in_progress` item or several of them. A todo tool nobody
|
|
221
|
+
remembers to call is decoration.
|
|
222
|
+
|
|
223
|
+
**CLAUDE.md framing.** Context files are now introduced with Claude Code's own
|
|
224
|
+
wording — "IMPORTANT: These instructions OVERRIDE any default behavior and you
|
|
225
|
+
MUST follow them exactly as written" — rather than a neutral "project-specific
|
|
226
|
+
instructions" header. Placement still differs deliberately: ours sits in the
|
|
227
|
+
system prompt (via pi), Claude Code's arrives in a first-user-message reminder.
|
|
228
|
+
Ours caches better.
|
|
229
|
+
|
|
230
|
+
**`context_management` — on by default for first-party Anthropic only.**
|
|
231
|
+
Claude Code sends `clear_thinking_20251015` on every request so long sessions
|
|
232
|
+
stop carrying old reasoning blocks. `extensions/context-management/` does this
|
|
233
|
+
via `before_provider_request`. Default: enabled when the model's provider is
|
|
234
|
+
`anthropic` on `api.anthropic.com` (verified there, and it is what Claude Code
|
|
235
|
+
does); disabled for every other `anthropic-messages` endpoint.
|
|
236
|
+
`CC_CLEAR_THINKING=0` forces it off; `=1` forces it on for an endpoint you
|
|
237
|
+
have confirmed accepts it.
|
|
238
|
+
|
|
239
|
+
**Enabling for other providers later** — the default is scoped narrowly only
|
|
240
|
+
because these are untested, not because they can't work. What each needs
|
|
241
|
+
before flipping it on:
|
|
242
|
+
|
|
243
|
+
- **Bedrock**: takes beta flags as `anthropic_beta` (array) *in the request
|
|
244
|
+
body*, not the `anthropic-beta` header — needs its own request shaping plus
|
|
245
|
+
a live check that it accepts `context_management` at all.
|
|
246
|
+
- **Vertex**: `anthropic_version` in the body; same question about
|
|
247
|
+
`context_management` support.
|
|
248
|
+
- **Proxies/gateways (LiteLLM etc.)**: pass-through varies per deployment;
|
|
249
|
+
users can already opt in per-endpoint with `CC_CLEAR_THINKING=1` once they
|
|
250
|
+
have confirmed theirs forwards the header and body param.
|
|
251
|
+
|
|
252
|
+
To extend: broaden `clearThinkingEnabled()` per provider and add the
|
|
253
|
+
endpoint's beta representation next to `anthropicBetas()`; verify with a live
|
|
254
|
+
run using `debug-capture.ts` (remember it logs pre-mutation payloads — confirm
|
|
255
|
+
via absence of the 400, not via the capture).
|
|
256
|
+
|
|
257
|
+
Verified against the live API (2026-08-05, API key) — the body param alone is
|
|
258
|
+
not enough, found via a curl A/B:
|
|
259
|
+
|
|
260
|
+
- It requires `anthropic-beta: context-management-2025-06-27`, else 400
|
|
261
|
+
"context_management: Extra inputs are not permitted".
|
|
262
|
+
- The edit requires thinking enabled or an adaptive-thinking model, else 400
|
|
263
|
+
"`clear_thinking_20251015` strategy requires `thinking` to be enabled or
|
|
264
|
+
adaptive" — so the payload hook gates on `payload.thinking` /
|
|
265
|
+
`compat.forceAdaptiveThinking`.
|
|
266
|
+
- **Header clobber hazard**: an extension's `before_provider_headers` value
|
|
267
|
+
merges *after* pi's computed headers, replacing pi's own `anthropic-beta`
|
|
268
|
+
(OAuth identity betas, interleaved thinking for non-adaptive models,
|
|
269
|
+
fine-grained tool streaming). `anthropicBetas()` therefore rebuilds pi's
|
|
270
|
+
list (pinned v0.83 logic) and appends ours; OAuth is detected from
|
|
271
|
+
`~/.pi/agent/auth.json` entry `type`. Re-check on pi upgrades.
|
|
272
|
+
|
|
273
|
+
Confirmed working end-to-end on `claude-haiku-4-5` (non-adaptive, thinking
|
|
274
|
+
gated) and `claude-sonnet-5` (adaptive): no API errors, normal completions.
|
|
275
|
+
|
|
276
|
+
Verified end-to-end: an unread-file edit was blocked and the file left untouched;
|
|
277
|
+
after an external edit the stale guard blocked the edit and the change reminder
|
|
278
|
+
reached the model (both visible in the request payload); a declined write
|
|
279
|
+
delivered `The user said: Use append mode instead…`; and the todo nudge appeared
|
|
280
|
+
after ten quiet turns.
|
|
281
|
+
|
|
282
|
+
**Deliberately not copied:** OS sandboxing (pi's stance is to containerize);
|
|
283
|
+
harness-level command shaping such as blocking foreground `sleep`; background
|
|
284
|
+
agents with task notifications (still Phase 8); and mid-conversation
|
|
285
|
+
`role: "system"` messages, which pi's message types cannot express — our
|
|
286
|
+
user-message reminders are the closest equivalent. (The LLM safety classifier
|
|
287
|
+
that gates bash, once listed here as not copied because it needs a model call
|
|
288
|
+
per command, is now built — see [`auto-mode.md`](auto-mode.md).)
|
|
289
|
+
|
|
290
|
+
## Tier-aware tool surface: search built-ins for mid/low only (unreviewed)
|
|
291
|
+
|
|
292
|
+
**Decision.** pi registers `grep`/`find`/`ls` but never activates them (only
|
|
293
|
+
`read`/`bash`/`edit`/`write` are active by default — findings §2). One Code
|
|
294
|
+
activates the three search tools at session start (and on model change) for
|
|
295
|
+
the **mid/low** prompt tiers only; **frontier keeps pi's lean default**
|
|
296
|
+
(`extensions/search-tools/`).
|
|
297
|
+
|
|
298
|
+
**Why.** Every captured One Code payload carried no search tools while the
|
|
299
|
+
mid/low tier prompts explicitly steer to "the search tools" — instructions
|
|
300
|
+
pointing at tools that did not exist in the request, a weak-model trap.
|
|
301
|
+
Frontier is deliberately different: Claude Code v2.1.81 ships no
|
|
302
|
+
Grep/Glob/LS to frontier models (verified in `tools/eager-tools.json`) —
|
|
303
|
+
bash covers search there, and CC's own frontier prompt keeps only the generic
|
|
304
|
+
"prefer dedicated tools" line. The tier classifier from the prompt-tier
|
|
305
|
+
decision is reused so prompt text and tool surface always agree.
|
|
306
|
+
|
|
307
|
+
**Rejected.** Activating unconditionally (anti-parity for frontier, wasted
|
|
308
|
+
schema tokens); editing the tier prompts to say "use bash grep" (weaker
|
|
309
|
+
models measurably do better with structured tools, which is why the tiers
|
|
310
|
+
exist).
|
|
311
|
+
|
|
312
|
+
## Subagent steering: injected catalog, fork hardening (unreviewed)
|
|
313
|
+
|
|
314
|
+
**Decision.** (a) The agent catalog is injected as an every-turn keyed system
|
|
315
|
+
reminder (`subagent-agents`), and the `subagent` schema descriptions point at
|
|
316
|
+
it; (b) a fork child's task is wrapped in a framing preamble
|
|
317
|
+
(`forkTaskMessage` in `subagents/child.ts`): inherited context is reference
|
|
318
|
+
only, do ONLY the task, the parent's background task ids are not addressable;
|
|
319
|
+
(c) `model`/`thinking` overrides on a fork run are rejected with a corrective
|
|
320
|
+
error.
|
|
321
|
+
|
|
322
|
+
**Why.** A captured weak-model payload showed the model had no way to know
|
|
323
|
+
agent names without a discovery call (Claude Code injects the agent-type
|
|
324
|
+
list in a system reminder). The fork preamble and override rejection close
|
|
325
|
+
the fork-confabulation incident (docs/handoff-tool-ambiguity.md): a fork on
|
|
326
|
+
`thinking:"minimal"` abandoned its task and continued the inherited topic,
|
|
327
|
+
and the parent read the returned text as independent confirmation. Claude
|
|
328
|
+
Code silently *ignores* `model` for forks; rejection was chosen over silent
|
|
329
|
+
ignoring per the fail-loud convention.
|
|
330
|
+
|
|
331
|
+
**Rejected.** Renaming `agent` to CC's `subagent_type` (the tool is named
|
|
332
|
+
`subagent`, byte-parity is unreachable, and the pi-idiomatic naming decision
|
|
333
|
+
stands); silently coercing fork overrides (hides the caller's mistake).
|
|
334
|
+
|
|
335
|
+
## Background bash: override the built-in, gate before detaching (unreviewed)
|
|
336
|
+
|
|
337
|
+
**Decision.** `extensions/bash/` registers a tool named `bash`, overriding
|
|
338
|
+
pi's built-in (same-name registration — findings §2, the pattern pi's own
|
|
339
|
+
sandbox example uses). Foreground calls delegate to pi's real executor
|
|
340
|
+
(`createBashToolDefinition`, re-created per cwd). `run_in_background: true`
|
|
341
|
+
spawns detached in its own process group, returns a task id, spools to
|
|
342
|
+
`<sessionDir>/bash/<id>/output.log`, and registers a `kind:"bash"`
|
|
343
|
+
`BackgroundTask` — `task_output`/`task_stop` are kind-agnostic and work
|
|
344
|
+
unchanged. Completion notifications carry byte-identical text to
|
|
345
|
+
`task_output`, and legitimately-empty output is explicitly marked
|
|
346
|
+
`(no output — …)`. The permission gate / auto-mode classifier runs before
|
|
347
|
+
execute like any bash call — background bash is **not** auto-allowed, and the
|
|
348
|
+
gate fires before anything detaches (verified live).
|
|
349
|
+
|
|
350
|
+
**Why / rejected.** Full rationale and the rejected shapes (separate
|
|
351
|
+
`bash_background` tool; generalising `monitor`) in
|
|
352
|
+
[`../plan/background-bash.md`](../plan/background-bash.md), now implemented.
|
|
353
|
+
A separate tool name would diverge from Claude Code's single-Bash shape and
|
|
354
|
+
lose the existing `bash` permission rules for free.
|
|
355
|
+
|
|
356
|
+
## Deferral runs on all tiers; steering follows CC's channels (2026-08-10)
|
|
357
|
+
|
|
358
|
+
**Decision.** (a) Tool deferral (ToolSearch) applies on **every** tier, not just
|
|
359
|
+
frontier — the deferred-registry tools are deactivated and listed in the
|
|
360
|
+
deferred-tools reminder regardless of model; core tools (read/edit/write/bash/…)
|
|
361
|
+
never register as deferrable and stay eager. (b) Every harness-injected
|
|
362
|
+
notification shares `lib/notifications.ts`'s anti-confabulation preamble
|
|
363
|
+
(adapted from CC's). (c) MCP servers' `instructions` are injected as an
|
|
364
|
+
every-turn reminder in CC's format instead of being dropped.
|
|
365
|
+
|
|
366
|
+
**Why (a) reversed.** The original "frontier-only" was justified by a false
|
|
367
|
+
premise — findings §14 wrongly claimed CC ships Haiku all tools eagerly. The raw
|
|
368
|
+
captures (`haiku-4-6.json`, `latest-haiku.json`) show CC defers the long tail on
|
|
369
|
+
Haiku too. And it costs nothing to do everywhere: `pi.setActiveTools` edits
|
|
370
|
+
`agent.state.tools`, which *is* the request tool list, so deactivation omits a
|
|
371
|
+
tool from the wire on **any** provider (OpenRouter/deepseek included) — verified
|
|
372
|
+
live, 35→15 tools on `deepseek-v4-flash-free`. Provider-native deferral
|
|
373
|
+
(Anthropic `defer_loading`, OpenAI `tool_search_call`) is only a cache
|
|
374
|
+
optimization on top; without it, loading a tool mid-session invalidates the
|
|
375
|
+
prompt cache from the tools block down — an accepted tradeoff for cheap models.
|
|
376
|
+
|
|
377
|
+
**Rejected.** Keeping frontier-only as a deliberate divergence (weak models
|
|
378
|
+
fumble the indirection) — the user chose CC fidelity; the risk is documented and
|
|
379
|
+
revisitable. A One Code-specific notification format (CC's wording is
|
|
380
|
+
field-tested against exactly this hazard).
|
|
381
|
+
|
|
382
|
+
## CLAUDE.md, memory, and skills injected as the CC reminder stack (2026-08-10)
|
|
383
|
+
|
|
384
|
+
**Decision.** CLAUDE.md contents, the `MEMORY.md` index, and the skills listing
|
|
385
|
+
are no longer in the system prompt. They ride the first-user-message
|
|
386
|
+
`<system-reminder>` stack, prepended before the user's text in CC's fixed order
|
|
387
|
+
(deferred tools → subagent-models → agents → MCP → skills → `# claudeMd`), via
|
|
388
|
+
placement/order/suffix on the shared reminder queue (`lib/reminders.ts`,
|
|
389
|
+
`CONTEXT_ORDER`). CLAUDE.md + memory + `# userEmail` + `# currentDate` are bundled
|
|
390
|
+
into ONE byte-identical `# claudeMd` block (`extensions/claude-context` +
|
|
391
|
+
`lib/claude-context.ts`); discovery mirrors CC (global + project ancestors, not
|
|
392
|
+
pi's AGENTS.md-first loader). Tier-independent: the block is identical across
|
|
393
|
+
models — only the `# Memory` *spec* in the system prompt varies by tier (already
|
|
394
|
+
handled by `lib/memory.ts`).
|
|
395
|
+
|
|
396
|
+
**Why (reversed from "stay in the system prompt").** The goal is byte-identity
|
|
397
|
+
with CC's payload, which the user made explicit. CC puts this content in the user
|
|
398
|
+
turn (trust boundary: project/user-authored content isn't in the system role),
|
|
399
|
+
and the diff was visible and real. Injection is transient (pi's `context` event
|
|
400
|
+
runs on a `structuredClone`; the session keeps the clean user message), so
|
|
401
|
+
re-injecting every turn is safe on resume. The claudeMd block reproduces CC's
|
|
402
|
+
exact bytes including the trailing `\n\n` (findings §14).
|
|
403
|
+
|
|
404
|
+
**Rejected.** Keeping it in the system prompt (the earlier call — abandoned once
|
|
405
|
+
byte-identity became the requirement); a first-message idempotent-prepend without
|
|
406
|
+
the shared queue (the queue already models transient per-turn injection and
|
|
407
|
+
placement generalizes to all five reminders).
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# TUI
|
|
2
|
+
|
|
3
|
+
Part of [decisions](../decisions.md).
|
|
4
|
+
|
|
5
|
+
## Compact tool rendering — the Claude Code transcript look (unreviewed)
|
|
6
|
+
|
|
7
|
+
*2026-08-08.* The transcript was cluttered: every custom tool rendered through
|
|
8
|
+
pi's fallback (bold tool name + full JSON args + full output) inside a heavy
|
|
9
|
+
colored box, harness notifications printed their whole anti-confabulation
|
|
10
|
+
framing to the user, and thinking text was barely distinguishable from answer
|
|
11
|
+
text. Claude Code's transcript shows a one-line call (`● Tool(args)`), a
|
|
12
|
+
collapsed indented result (`⎿ …` + `+N lines (ctrl+o to expand)`), and hides
|
|
13
|
+
model-facing boilerplate.
|
|
14
|
+
|
|
15
|
+
**What was done:**
|
|
16
|
+
|
|
17
|
+
- **`extensions/lib/tui-render.ts`** (pure, unit-tested) provides
|
|
18
|
+
`ccToolRenderers(label, spec?)` — `renderShell: "self"` +
|
|
19
|
+
`renderCall`/`renderResult` producing the `●`/`⎿` layout with pi's native
|
|
20
|
+
ctrl+o expansion (`options.expanded`), status-colored bullet
|
|
21
|
+
(muted → success/error), a primary-arg heuristic for the call line
|
|
22
|
+
(per-tool `title`/`result` overrides where the heuristic is wrong), and
|
|
23
|
+
ANSI-aware width truncation on every line (pi-tui crashes on overwide
|
|
24
|
+
lines — findings §3). Every One Code-registered tool spreads it in; pi's
|
|
25
|
+
built-ins (read/bash/edit/…) keep their own upstream renderers.
|
|
26
|
+
- **Notifications render compact.** `task-notification`, `wakeup`,
|
|
27
|
+
`subagent-message`, `subagent-result`, `workflow-result` get
|
|
28
|
+
`registerMessageRenderer` components: one dim `✳ headline` line collapsed,
|
|
29
|
+
full body on ctrl+o. The `systemNotification` framing stays in the model
|
|
30
|
+
payload but is stripped from display (`notificationBody`) — it exists for
|
|
31
|
+
the model, not the user.
|
|
32
|
+
- **Bash background starts** render `⎿ Running in background (task <id> ·
|
|
33
|
+
log: …)` instead of the model-facing instruction paragraph; foreground
|
|
34
|
+
bash delegates to pi's own renderer untouched.
|
|
35
|
+
- **Themes:** `toolPendingBg`/`toolSuccessBg` set to `""` (terminal default —
|
|
36
|
+
pi maps the empty string to "no background"), which removes the box look
|
|
37
|
+
for *all* tools including pi built-ins; `toolErrorBg` kept as the one
|
|
38
|
+
remaining background signal. `thinkingText` moved from `gray` to `grayDim`
|
|
39
|
+
(240 dark / 245 light) so thinking reads clearly dimmer than answer text —
|
|
40
|
+
pi already italicizes it, and ctrl+t (`hideThinkingBlock`) remains the
|
|
41
|
+
user's collapse toggle.
|
|
42
|
+
|
|
43
|
+
**Rejected:** overriding pi's built-in tool renderers for a bullet-style call
|
|
44
|
+
line on read/grep/etc. — re-registering seven built-ins to restyle them is
|
|
45
|
+
high-risk surgery for marginal polish, and the theme change already de-boxes
|
|
46
|
+
them. Also rejected: hiding notifications entirely (CC-like) — a dim one-liner
|
|
47
|
+
keeps the transcript honest about turn triggers.
|
|
48
|
+
|
|
49
|
+
## Built-in tools joined the ● language; banner cut to four lines (unreviewed)
|
|
50
|
+
|
|
51
|
+
*2026-08-08, same session.* With custom tools on `●`/`⎿` and pi's built-ins on
|
|
52
|
+
their own style (`$ cmd`, `edit path`), the transcript spoke two languages —
|
|
53
|
+
the user asked for one (`● Bash(command)`, like Claude Code).
|
|
54
|
+
|
|
55
|
+
- **`extensions/tool-style/`** overrides read/write/edit/grep/find/ls under
|
|
56
|
+
their built-in names (the bash-extension mechanism, findings §2). Every
|
|
57
|
+
model-facing field is passed through byte-identical (system prompt
|
|
58
|
+
unchanged); execute delegates to a per-cwd instance of pi's real definition
|
|
59
|
+
(worktree moves). Rendering wraps rather than reimplements —
|
|
60
|
+
`ccWrapBuiltinRenderers` in the shared lib: `● Label(primary arg)` call
|
|
61
|
+
line; the **base result component indented under a `⎿` elbow**, so pi's
|
|
62
|
+
streaming, truncation, and ctrl+o expansion behavior stays upstream's.
|
|
63
|
+
Edit keeps its own call component (the diff preview a permission decision
|
|
64
|
+
depends on) with only the header line swapped (`● Update(path)`).
|
|
65
|
+
Inner components live on `context.state`, never `lastComponent` — base
|
|
66
|
+
renderers cast `lastComponent` to their concrete classes and would throw.
|
|
67
|
+
grep/find/ls stay tier-gated: search-tools reconciles the active list on
|
|
68
|
+
session_start/model_select either way. Bash reuses the same wrapper (in
|
|
69
|
+
`extensions/bash/`), which also dropped the "Took Ns" timer that counted
|
|
70
|
+
permission-prompt wait time as command time.
|
|
71
|
+
- **Trap that cost a debugging loop:** a padded Box's "blank" lines carry
|
|
72
|
+
ANSI paint (`\x1b[49m…`), so `line.trim() === ""` is false — blank-line
|
|
73
|
+
detection in renderers must strip ANSI first (`isBlankLine`). The stale
|
|
74
|
+
render looked exactly like a stale jiti cache and wasn't.
|
|
75
|
+
- **Banner** (`extensions/branding/`): three keymap-dump hint lines became
|
|
76
|
+
one curated line — only the controls a user can't discover (renamed dials,
|
|
77
|
+
collapsed-state toggles, input prefixes, the ultracode keyword), led by
|
|
78
|
+
`/hotkeys all keys` so the pointer to everything else survives narrow
|
|
79
|
+
terminals (`fitItems` drops whole trailing hints instead of cutting
|
|
80
|
+
mid-word). Sections collapse to one line: names when ≤3 items, counts
|
|
81
|
+
beyond (a truncated name dump carries no information). Startup warnings
|
|
82
|
+
point at their commands (`(/lsp for status)`, `(/mcp for status)`), and the
|
|
83
|
+
LSP one is capped at one line — the full server error stays in `/lsp`.
|
|
84
|
+
|
|
85
|
+
Verified live: `● Read/Grep/Update/Bash/LS` with `⎿` results, edit's diff
|
|
86
|
+
preview intact before approval, `seq 1 30` showing pi's "(25 earlier lines,
|
|
87
|
+
ctrl+o to expand)" under the elbow, banner at 170 and 80 columns.
|
|
88
|
+
|
|
89
|
+
## Thinking collapsed by default, expandable with ctrl+t (unreviewed)
|
|
90
|
+
|
|
91
|
+
*2026-08-08, follow-up to the same complaint.* Even dimmed and italic, visible
|
|
92
|
+
thinking still read as answer narration. pi already has the exact mechanism
|
|
93
|
+
Claude Code uses — `hideThinkingBlock` (persisted global setting, toggled by
|
|
94
|
+
ctrl+t and /settings) collapses each thinking run to a one-line label, and
|
|
95
|
+
`ctx.ui.setHiddenThinkingLabel()` is a public extension API for the label
|
|
96
|
+
text — so One Code adopts it instead of building rendering of its own (assistant
|
|
97
|
+
message rendering is not extension-replaceable anyway).
|
|
98
|
+
|
|
99
|
+
- **`branding` defaults `hideThinkingBlock: true` exactly once**: only when
|
|
100
|
+
the key is *absent* from `~/.pi/agent/settings.json` (pure decision helper
|
|
101
|
+
`shouldDefaultHideThinking` — a present key, either value, is the user's
|
|
102
|
+
choice and is never overridden; an unparseable file is left alone). Write
|
|
103
|
+
goes through pi's exported `SettingsManager` (locked merge-write).
|
|
104
|
+
- **Label**: `✻ Thinking… (ctrl+t to expand)`, set on every `session_start`
|
|
105
|
+
(pi resets it to "Thinking..." on /reload via `resetExtensionUI`).
|
|
106
|
+
- **Timing caveat (finding)**: pi creates its runtime `SettingsManager` — and
|
|
107
|
+
caches the file in memory — *before* extensions load, so the written default
|
|
108
|
+
lands from the **next** session; the install session still shows thinking
|
|
109
|
+
expanded until the user toggles.
|
|
110
|
+
|
|
111
|
+
Verified live across a restart: first launch writes the key, second session
|
|
112
|
+
renders `✻ Thinking… (ctrl+t to expand)`, ctrl+t expands to the dim italic
|
|
113
|
+
text and back, choice persists.
|
|
114
|
+
|
|
115
|
+
**Verified live** (tmux capture, deepseek-v4-flash-free): boxless transcript,
|
|
116
|
+
`● Todos(2 items)` / `● Task Output(<id>)` call lines, collapsed results,
|
|
117
|
+
`✳ Background bash … completed. (+2 lines, ctrl+o to expand)` notification,
|
|
118
|
+
ctrl+o expanding/collapsing both, thinking at `38;5;240` italic, no crash at
|
|
119
|
+
170 cols; 789 unit tests green.
|
|
120
|
+
|
|
121
|
+
## Fullscreen (alt-screen) for Claude Code's clean exit — a setting, not extension code (unreviewed)
|
|
122
|
+
|
|
123
|
+
Claude Code runs its whole session in the terminal's **alternate screen
|
|
124
|
+
buffer** (`ESC[?1049h` on start, `ESC[?1049l` on exit), so quitting restores
|
|
125
|
+
the terminal to whatever preceded it and CC prints only a one-line resume hint.
|
|
126
|
+
One Code's default looked different: pi's `TuiMainScreen` (inline, main buffer)
|
|
127
|
+
leaves the banner and whole conversation in scrollback after exit.
|
|
128
|
+
|
|
129
|
+
pi already has the machinery — `TuiAltScreen` (mode `"fullscreen"`), whose
|
|
130
|
+
`.stop()` writes `ESC[?1049l` and then pi prints `To resume this session:
|
|
131
|
+
pi --resume …` (`interactive-mode.ts`). It is gated behind the `tuiMode`
|
|
132
|
+
setting (`"regular"` default vs `"fullscreen"`), marked *experimental* by pi.
|
|
133
|
+
|
|
134
|
+
**Decision: recommend `tuiMode: "fullscreen"` as the One Code experience, but
|
|
135
|
+
deliver it as a documented user/global setting — not extension code.** The
|
|
136
|
+
renderer is a pi-core startup choice: it is built once from
|
|
137
|
+
`--tui-mode <mode>` (CLI) or `settingsManager.getUiMode()` (settings.json), and
|
|
138
|
+
the live switch (`switchUiMode`) is **private** to pi's `InteractiveMode` with
|
|
139
|
+
no extension-facing hook. A One Code extension therefore cannot set or flip it.
|
|
140
|
+
|
|
141
|
+
- **Rejected — flip it from an extension:** impossible; no API reaches the
|
|
142
|
+
renderer, and it is read before/independent of which package loads.
|
|
143
|
+
- **Rejected — One Code writes the user's global settings on load:** invasive
|
|
144
|
+
(mutating user config the package doesn't own) and it would change plain `pi`
|
|
145
|
+
everywhere, not just One Code sessions — the setting cannot be scoped to
|
|
146
|
+
"this package is active."
|
|
147
|
+
- **Chosen — document the toggle.** Set `"tuiMode": "fullscreen"` in
|
|
148
|
+
`~/.pi/agent/settings.json` (global, per-user; right for a user who always
|
|
149
|
+
runs One Code), or a project's `.pi/settings.json` (project settings override
|
|
150
|
+
global — `deepMergeSettings`), or `pi --tui-mode fullscreen` ad-hoc. `/settings
|
|
151
|
+
→ Interface layout → regular` reverts.
|
|
152
|
+
|
|
153
|
+
**Trade-off (accepted):** alt-screen means the conversation does not persist in
|
|
154
|
+
terminal scrollback after quit — that is the whole point of the clean exit, and
|
|
155
|
+
`pi --resume` covers re-entry.
|
|
156
|
+
|
|
157
|
+
**Trap this surfaced (finding §10.16):** the setting key was `uiMode` in the
|
|
158
|
+
pinned reference clone (v0.83.0) but was renamed `tuiMode` in installed pi
|
|
159
|
+
(v0.84.1); a `uiMode` key is silently ignored on 0.84.1. Clone re-pinned to
|
|
160
|
+
`v0.84.1` to match.
|
|
161
|
+
|
|
162
|
+
Set in `~/.pi/agent/settings.json` on 2026-08-10; live clean-exit behavior to be
|
|
163
|
+
confirmed by the user.
|
|
164
|
+
|
|
165
|
+
## Render memoization: components cache by width (2026-08-10)
|
|
166
|
+
|
|
167
|
+
pi-tui calls `render(width)` on **every mounted component on every frame**
|
|
168
|
+
(~60fps during streaming, every keystroke; fullscreen mode additionally
|
|
169
|
+
re-measures the full unclipped scrollback to compute scroll height) — findings
|
|
170
|
+
§15. Our `linesComponent` recomputed `build(width)` plus `truncateLine`'s
|
|
171
|
+
per-character ANSI scan on every call, so every historical `●`/`⎿` row taxed
|
|
172
|
+
every frame for the life of the session. Profiled on a 750-entry thread:
|
|
173
|
+
`lib/tui-render.ts` was the largest app-level CPU consumer during typing
|
|
174
|
+
(~0.5s self time per 10s, plus ~1s induced in pi-tui width/wrap utils).
|
|
175
|
+
|
|
176
|
+
**Decision:** memoize by width inside the component, and rely on the
|
|
177
|
+
factory-per-state-change contract for correctness:
|
|
178
|
+
|
|
179
|
+
- Any state change (spinner tick, args streaming in, result arriving, expand
|
|
180
|
+
toggle) makes pi re-invoke `renderCall`/`renderResult`, minting a **fresh
|
|
181
|
+
component with a fresh cache** — so within one component's lifetime `build`
|
|
182
|
+
is pure and a `Map<width, lines>` (capped at 4 entries) cannot go stale.
|
|
183
|
+
- `invalidate()` clears the cache — that is the path pi-tui uses for theme
|
|
184
|
+
swaps and resizes.
|
|
185
|
+
- The `ccWrapBuiltinRenderers` wrappers cache only when the call/result is
|
|
186
|
+
settled (`!isPartial`): a live base component may stream internally without
|
|
187
|
+
a factory re-run, so in-flight rows stay uncached.
|
|
188
|
+
- `truncateLine` gained the structural fast path: raw length bounds visible
|
|
189
|
+
width (escapes only add), so `line.length <= width` returns immediately —
|
|
190
|
+
the hot path for nearly every line; the scan itself now uses a sticky
|
|
191
|
+
module-level regex instead of per-character `slice().match()` allocations.
|
|
192
|
+
|
|
193
|
+
Verified: same typing/scrolling profile after the change — `truncateLine` and
|
|
194
|
+
`tui-render.ts` no longer appear in the top-30 functions at all; remaining
|
|
195
|
+
per-frame cost is pi-tui's own (upstream candidates: `docs/upstream_prs.md`
|
|
196
|
+
#2-#4). Unit tests pin the contract (build-once-per-width, invalidate clears).
|
|
197
|
+
|
|
198
|
+
**Rule for future renderers:** a component returned from
|
|
199
|
+
`renderCall`/`renderResult`/`registerMessageRenderer` must memoize its
|
|
200
|
+
`render(width)` (use `linesComponent`, or replicate its cache) — an uncached
|
|
201
|
+
component re-renders 60×/s for as long as it is mounted, and the cost
|
|
202
|
+
compounds linearly with thread length.
|