one-code-extension 0.3.0 → 0.4.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.
Files changed (119) hide show
  1. package/README.md +189 -94
  2. package/docs/guide/README.md +3 -2
  3. package/docs/guide/bring-your-claude-code-setup.md +7 -5
  4. package/docs/guide/configuration.md +7 -5
  5. package/docs/guide/differences-from-claude-code.md +25 -14
  6. package/docs/guide/doctor.md +5 -3
  7. package/docs/guide/hooks.md +20 -6
  8. package/docs/guide/installation.md +62 -12
  9. package/docs/guide/permissions-modes-and-auto-mode.md +46 -28
  10. package/docs/guide/providers-and-models.md +12 -10
  11. package/docs/guide/reference.md +9 -4
  12. package/docs/guide/sessions-and-context.md +23 -3
  13. package/docs/guide/skills-plugins-and-mcp.md +7 -5
  14. package/docs/guide/subagents-and-workflows.md +13 -11
  15. package/docs/guide/tasks-and-background-work.md +10 -8
  16. package/docs/guide/terminal-interface.md +7 -5
  17. package/docs/guide/tools.md +29 -5
  18. package/docs/guide/troubleshooting.md +26 -14
  19. package/docs/guide/windows.md +184 -0
  20. package/extensions/auto-mode/actions.ts +3 -1
  21. package/extensions/auto-mode/paths.ts +21 -23
  22. package/extensions/auto-mode/safety-floor.ts +44 -6
  23. package/extensions/auto-mode/shell-analysis.ts +25 -10
  24. package/extensions/auto-mode/transcript.ts +15 -5
  25. package/extensions/background/index.ts +83 -34
  26. package/extensions/background/monitor-batch.ts +7 -5
  27. package/extensions/background/wakeup.ts +18 -24
  28. package/extensions/bash/background.ts +19 -10
  29. package/extensions/bash/guards.ts +36 -16
  30. package/extensions/bash/index.ts +36 -166
  31. package/extensions/branding/index.ts +35 -4
  32. package/extensions/btw/index.ts +207 -0
  33. package/extensions/btw/panel.ts +179 -0
  34. package/extensions/btw/prompt.ts +45 -0
  35. package/extensions/clear/index.ts +15 -0
  36. package/extensions/compaction/fit.ts +9 -6
  37. package/extensions/doctor/build.ts +22 -2
  38. package/extensions/doctor/dependencies.ts +63 -31
  39. package/extensions/doctor/fix-prompt.ts +12 -4
  40. package/extensions/doctor/index.ts +1 -0
  41. package/extensions/doctor/models.ts +2 -1
  42. package/extensions/doctor/report.ts +2 -3
  43. package/extensions/doctor/update-lookup.ts +19 -11
  44. package/extensions/effort/index.ts +2 -1
  45. package/extensions/footer/footer-line.ts +19 -0
  46. package/extensions/footer/index.ts +25 -5
  47. package/extensions/hooks/executor.ts +84 -18
  48. package/extensions/hooks/index.ts +1 -0
  49. package/extensions/hooks/matcher.ts +23 -9
  50. package/extensions/hooks/settings.ts +15 -0
  51. package/extensions/lib/branch-restore.ts +12 -0
  52. package/extensions/lib/claude-context.ts +2 -3
  53. package/extensions/lib/claude-settings.ts +19 -0
  54. package/extensions/lib/command-launch.ts +71 -0
  55. package/extensions/lib/deferred.ts +173 -33
  56. package/extensions/lib/git.ts +16 -0
  57. package/extensions/lib/keys.ts +38 -0
  58. package/extensions/lib/local-command.ts +112 -0
  59. package/extensions/lib/model-facts.generated.json +263 -68
  60. package/extensions/lib/model-policy.ts +13 -0
  61. package/extensions/lib/model-tier.ts +16 -5
  62. package/extensions/lib/notifications.ts +547 -53
  63. package/extensions/lib/paths.ts +130 -2
  64. package/extensions/lib/pi-ai-estimate.ts +57 -26
  65. package/extensions/lib/pi-version.ts +1 -1
  66. package/extensions/lib/process-tree.ts +131 -5
  67. package/extensions/lib/reminders.ts +80 -27
  68. package/extensions/lib/scratchpad.ts +11 -10
  69. package/extensions/lib/shell-spawn.ts +298 -0
  70. package/extensions/lib/shell-tool.ts +252 -0
  71. package/extensions/lib/shell-tools.ts +13 -0
  72. package/extensions/lib/side-call.ts +76 -0
  73. package/extensions/lib/tui-render.ts +62 -11
  74. package/extensions/lib/update-check.mjs +155 -0
  75. package/extensions/lib/usage-bus.ts +1 -1
  76. package/extensions/lib/which.ts +62 -0
  77. package/extensions/lib/worktree-channel.ts +18 -0
  78. package/extensions/lib/worktree-isolation.ts +2 -1
  79. package/extensions/lsp/client.ts +14 -3
  80. package/extensions/lsp/index.ts +12 -5
  81. package/extensions/lsp/install-hints.ts +33 -3
  82. package/extensions/lsp/plugin-servers.ts +2 -2
  83. package/extensions/lsp/servers.ts +29 -7
  84. package/extensions/mcp/index.ts +3 -2
  85. package/extensions/memory/entries.ts +9 -6
  86. package/extensions/memory/index.ts +3 -2
  87. package/extensions/memory/open-external.ts +4 -1
  88. package/extensions/model-default/index.ts +21 -2
  89. package/extensions/permissions/index.ts +57 -28
  90. package/extensions/permissions/matcher.ts +120 -33
  91. package/extensions/permissions/modes.ts +14 -6
  92. package/extensions/permissions/powershell-rules.ts +549 -0
  93. package/extensions/permissions/session-grant.ts +9 -9
  94. package/extensions/plugins/index.ts +3 -2
  95. package/extensions/plugins/install/install.ts +1 -1
  96. package/extensions/powershell/description.ts +139 -0
  97. package/extensions/powershell/guards.ts +91 -0
  98. package/extensions/powershell/index.ts +89 -0
  99. package/extensions/powershell/policy.ts +91 -0
  100. package/extensions/recap/index.ts +10 -24
  101. package/extensions/recap/prompt.ts +3 -3
  102. package/extensions/session-title/index.ts +120 -0
  103. package/extensions/session-title/title.ts +111 -0
  104. package/extensions/skill/index.ts +40 -18
  105. package/extensions/skill/invoke.ts +1 -0
  106. package/extensions/subagents/hand-back-review.ts +10 -12
  107. package/extensions/subagents/index.ts +221 -78
  108. package/extensions/subagents/model-select.ts +75 -7
  109. package/extensions/system-prompt/environment.ts +14 -1
  110. package/extensions/system-reminder/index.ts +9 -0
  111. package/extensions/tasks/index.ts +2 -1
  112. package/extensions/tool-search/index.ts +41 -20
  113. package/extensions/workflow/agent-session.ts +6 -4
  114. package/extensions/workflow/index.ts +28 -5
  115. package/extensions/worktree/guards.ts +5 -4
  116. package/extensions/worktree/index.ts +15 -2
  117. package/extensions/worktree/rewrite.ts +17 -0
  118. package/package.json +7 -4
  119. package/types/app.d.ts +18 -2
@@ -0,0 +1,207 @@
1
+ /**
2
+ * btw extension — Claude Code's `/btw` ("by the way") side question.
3
+ *
4
+ * `/btw <question>` asks a one-off question that is answered from the current
5
+ * conversation's context while the main agent keeps working. CC frames the
6
+ * answering model as a separate, lightweight, tool-less instance (the verbatim
7
+ * reminder in prompt.ts, captured from CC 2.1.278) and shows the answer in a
8
+ * focused overlay panel, not in the transcript — so the exchange never enters
9
+ * the main agent's context and cannot steer its work.
10
+ *
11
+ * The call reuses recap's machinery: the exact messages the session last put on
12
+ * the wire (captured from the `context` event), name-only tool stubs so a
13
+ * history carrying tool_use blocks stays valid on strict providers, and
14
+ * withReasoningFallback for models that cannot disable thinking. Unlike recap it
15
+ * uses the SESSION model (not an economical one) — a side question deserves the
16
+ * same quality as the main conversation — and sends the full captured history,
17
+ * not just the recent window.
18
+ *
19
+ * Deviations from CC, logged in docs/decisions/btw.md: the exchange is stateless
20
+ * (each `/btw` sees only the shared main context, never a prior `/btw` — CC
21
+ * keeps a side-session; the "one-off, no follow-up" framing holds either way);
22
+ * `f to fork` is omitted (pi's `ctx.fork` branches from a session entry, not
23
+ * from an injected side exchange); and the call runs with an empty system prompt
24
+ * (the `context` event carries no system prompt to reuse), relying on the shared
25
+ * history plus the reminder.
26
+ */
27
+
28
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
29
+ import type { Api, Model } from "@earendil-works/pi-ai";
30
+ import { completeSimple } from "@earendil-works/pi-ai/compat";
31
+ import { convertToLlm, copyToClipboard, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
32
+ import { withReasoningFallback } from "../lib/model-policy.ts";
33
+ import { answerText, stripImageBlocks, toolStubs, trimToTurnBoundary } from "../lib/side-call.ts";
34
+ import { boundedDockHeight, truncateLine } from "../lib/tui-render.ts";
35
+ import { recordUsage } from "../lib/usage-bus.ts";
36
+ import { applyBtwKey, type BtwBody, BTW_MAX_HEIGHT, decodeBtwKey, initialBtwState, renderBtwPanel } from "./panel.ts";
37
+ import { sideQuestionMessage } from "./prompt.ts";
38
+
39
+ const BTW_MAX_TOKENS = 8192;
40
+ const BTW_TIMEOUT_MS = 120_000;
41
+ /** Description on the name-only tool stubs sent with the side question. */
42
+ const STUB_REASON = "Unavailable during this side question; answer in text.";
43
+
44
+ export default function btwExtension(pi: ExtensionAPI) {
45
+ // The exact request messages the session last sent (after every extension's
46
+ // mutations), captured like recap and the compaction extension do.
47
+ let capturedMessages: AgentMessage[] | undefined;
48
+ pi.on("context", (event) => {
49
+ capturedMessages = event.messages;
50
+ });
51
+
52
+ // The controller for an open panel's in-flight call. Aborted when the session
53
+ // is replaced or torn down, so a late resolve cannot repaint a disposed `tui`
54
+ // or record usage on a stale `pi` (the crash the subagents panel once hit —
55
+ // docs/decisions/tools.md lifecycle review).
56
+ let inFlight: AbortController | undefined;
57
+
58
+ // A new session (including after /clear) shares no context with the old one,
59
+ // and any in-flight side question is abandoned.
60
+ pi.on("session_start", () => {
61
+ capturedMessages = undefined;
62
+ inFlight?.abort();
63
+ inFlight = undefined;
64
+ });
65
+ pi.on("session_shutdown", () => {
66
+ inFlight?.abort();
67
+ inFlight = undefined;
68
+ });
69
+
70
+ /** Run the side question against the session model; returns the answer text. */
71
+ async function ask(ctx: ExtensionCommandContext, question: string, signal: AbortSignal): Promise<string> {
72
+ const model = ctx.model;
73
+ if (!model) throw new Error("No model is configured for this session.");
74
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
75
+ if (!auth.ok) throw new Error("No API key is available for the current model.");
76
+ const baseUrl = (auth as { baseUrl?: string }).baseUrl;
77
+
78
+ const tools = toolStubs(pi.getActiveTools(), STUB_REASON);
79
+ const context = trimToTurnBoundary(capturedMessages ?? []);
80
+ // btw runs on a cheap, possibly text-only reader and answers a text question,
81
+ // so images the conversation carried are stripped before the call
82
+ // (docs/decisions/model-policy.md).
83
+ const messages = [...stripImageBlocks(convertToLlm(context)), { role: "user" as const, content: sideQuestionMessage(question), timestamp: Date.now() }];
84
+
85
+ const result = await withReasoningFallback(
86
+ model as Model<Api>,
87
+ (reasoning) => {
88
+ const timeout = AbortSignal.timeout(BTW_TIMEOUT_MS);
89
+ return completeSimple(
90
+ baseUrl ? ({ ...model, baseUrl } as Model<Api>) : (model as Model<Api>),
91
+ { systemPrompt: "", messages, tools },
92
+ {
93
+ apiKey: auth.apiKey,
94
+ headers: auth.headers,
95
+ env: auth.env,
96
+ signal: AbortSignal.any([signal, timeout]),
97
+ maxTokens: BTW_MAX_TOKENS,
98
+ ...(reasoning ? { reasoning } : {}),
99
+ },
100
+ );
101
+ },
102
+ undefined,
103
+ (usage) => recordUsage(pi, "btw", usage),
104
+ );
105
+ return answerText(result.content);
106
+ }
107
+
108
+ pi.registerCommand("btw", {
109
+ description: "Ask a one-off side question, answered from the current context while the main agent keeps working",
110
+ handler: async (args, ctx) => {
111
+ const question = args.trim();
112
+ if (!question) {
113
+ ctx.ui.notify("Ask a side question: /btw <question>", "info");
114
+ return;
115
+ }
116
+
117
+ // Headless (-p / --mode json): no panel — answer and print it. The
118
+ // handler blocks to completion, so the process does not exit early.
119
+ if (!ctx.hasUI) {
120
+ try {
121
+ const answer = await ask(ctx, question, new AbortController().signal);
122
+ ctx.ui.notify(answer || "(no answer)", "info");
123
+ } catch (error) {
124
+ ctx.ui.notify(`Side question failed: ${error instanceof Error ? error.message : String(error)}`, "error");
125
+ }
126
+ return;
127
+ }
128
+
129
+ // Show the panel at once with a loading line; run the call concurrently
130
+ // and repaint when it lands. Closing the panel — or a session
131
+ // replacement/shutdown — aborts an in-flight call.
132
+ inFlight?.abort();
133
+ const controller = new AbortController();
134
+ inFlight = controller;
135
+ const view: { answer?: string; error?: string } = {};
136
+ const bodyState = (): BtwBody =>
137
+ view.error !== undefined ? { kind: "error", message: view.error } : view.answer !== undefined ? { kind: "answer", text: view.answer } : { kind: "loading" };
138
+
139
+ await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
140
+ const state = initialBtwState();
141
+ let cache: { width: number; lines: string[] } | undefined;
142
+ const repaint = () => {
143
+ cache = undefined;
144
+ // A repaint can be requested from an async callback that raced a
145
+ // session teardown; a stale `tui` throws, so never let it propagate.
146
+ try {
147
+ tui.requestRender();
148
+ } catch {}
149
+ };
150
+
151
+ // Kick off the model call once, at mount, so `repaint` is bound.
152
+ void (async () => {
153
+ try {
154
+ const answer = await ask(ctx, question, controller.signal);
155
+ if (controller.signal.aborted) return;
156
+ view.answer = answer;
157
+ } catch (error) {
158
+ if (controller.signal.aborted) return;
159
+ view.error = error instanceof Error ? error.message : String(error);
160
+ }
161
+ repaint();
162
+ })();
163
+
164
+ return {
165
+ render: (width: number) => {
166
+ if (cache?.width === width) return cache.lines;
167
+ const termRows = (tui as { terminal: { rows: number } }).terminal.rows;
168
+ const height = boundedDockHeight(termRows, BTW_MAX_HEIGHT);
169
+ // Belt-and-suspenders truncate on top of renderBtwPanel's own,
170
+ // matching every sibling dock panel: pi-tui crashes the app on
171
+ // a line wider than the terminal.
172
+ const lines = renderBtwPanel({ state, question, body: bodyState(), width, height }, theme).map((line) => truncateLine(line, width));
173
+ cache = { width, lines };
174
+ return lines;
175
+ },
176
+ handleInput: (data: string) => {
177
+ const key = decodeBtwKey(data);
178
+ if (!key) return;
179
+ const effect = applyBtwKey(state, key);
180
+ switch (effect?.kind) {
181
+ case "close":
182
+ done(null);
183
+ return;
184
+ case "copy":
185
+ // Only offer copy once there is a non-empty answer.
186
+ if (view.answer) {
187
+ void copyToClipboard(view.answer).then(() => {
188
+ state.copied = true;
189
+ repaint();
190
+ });
191
+ }
192
+ return;
193
+ default:
194
+ repaint();
195
+ }
196
+ },
197
+ invalidate: () => {
198
+ cache = undefined;
199
+ },
200
+ };
201
+ });
202
+
203
+ controller.abort();
204
+ if (inFlight === controller) inFlight = undefined;
205
+ },
206
+ });
207
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Pure state, key decoding, and rendering for the `/btw` answer overlay — Claude
3
+ * Code's side-question panel: a focused, scrollable view of the answer with the
4
+ * question dim above it. `↑/↓` (and PgUp/PgDn) scroll, `c` copies the answer,
5
+ * `Esc` closes.
6
+ *
7
+ * Kept free of pi imports so the scroll maths and layout are unit-tested; the
8
+ * extension owns the overlay, repaint, and the actual clipboard write. CC's
9
+ * panel also offers `f to fork`; One Code omits it (pi exposes no primitive to
10
+ * branch a session with an injected exchange) — docs/decisions/btw.md.
11
+ */
12
+
13
+ import { panelTopRule, safeThemePaint, truncateLine, wrapPlainText } from "../lib/tui-render.ts";
14
+
15
+ export const BTW_MAX_HEIGHT = 24;
16
+
17
+ /** Key hint shown at the foot of the panel. Every key here is discoverable. */
18
+ export const BTW_HINT = "↑/↓ to scroll · c to copy · Esc to close";
19
+
20
+ /** Most echoed question lines shown in the header, so it can't starve the body. */
21
+ export const MAX_QUESTION_LINES = 4;
22
+
23
+ /**
24
+ * The panel's body state. The pure module owns the text for each phase so every
25
+ * appearance (loading, error, answer) is rendered — and tested — here rather
26
+ * than assembled by the extension wiring.
27
+ */
28
+ export type BtwBody = { kind: "loading" } | { kind: "error"; message: string } | { kind: "answer"; text: string };
29
+
30
+ /** The text shown in the body for a given phase. */
31
+ function bodyText(body: BtwBody): string {
32
+ switch (body.kind) {
33
+ case "loading":
34
+ return "Thinking…";
35
+ case "error":
36
+ return `Side question failed: ${body.message}`;
37
+ case "answer":
38
+ return body.text || "(no answer)";
39
+ }
40
+ }
41
+
42
+ export interface BtwPanelState {
43
+ /** First body line shown (0 = top). */
44
+ offset: number;
45
+ /** Largest valid offset for the current viewport; render keeps it current. */
46
+ maxOffset: number;
47
+ /** Visible body lines in the current viewport; render keeps it current. */
48
+ viewport: number;
49
+ /** Set for one frame after a copy so the footer can confirm it. */
50
+ copied: boolean;
51
+ }
52
+
53
+ export function initialBtwState(): BtwPanelState {
54
+ return { offset: 0, maxOffset: 0, viewport: 1, copied: false };
55
+ }
56
+
57
+ export type BtwKey = { kind: "up" | "down" | "pageUp" | "pageDown" | "top" | "bottom" | "copy" | "close" };
58
+
59
+ export function decodeBtwKey(data: string): BtwKey | undefined {
60
+ switch (data) {
61
+ case "\x1b[A":
62
+ case "\x1bOA":
63
+ case "\x10": // ctrl+p
64
+ return { kind: "up" };
65
+ case "\x1b[B":
66
+ case "\x1bOB":
67
+ case "\x0e": // ctrl+n
68
+ return { kind: "down" };
69
+ case "\x1b[5~":
70
+ return { kind: "pageUp" };
71
+ case "\x1b[6~":
72
+ return { kind: "pageDown" };
73
+ case "\x1b[H":
74
+ case "\x1bOH":
75
+ return { kind: "top" };
76
+ case "\x1b[F":
77
+ case "\x1bOF":
78
+ return { kind: "bottom" };
79
+ case "c":
80
+ case "C":
81
+ return { kind: "copy" };
82
+ case "\x1b":
83
+ case "\x03": // ctrl+c
84
+ case "q":
85
+ return { kind: "close" };
86
+ default:
87
+ return undefined;
88
+ }
89
+ }
90
+
91
+ export type BtwEffect = { kind: "copy" } | { kind: "close" };
92
+
93
+ /** Page size for PgUp/PgDn: nearly a full viewport, one line of overlap for orientation. */
94
+ function pageStep(viewport: number): number {
95
+ return Math.max(1, viewport - 1);
96
+ }
97
+
98
+ /**
99
+ * Apply a key to the scroll state. Returns an effect for the extension to run
100
+ * (copy, close) or undefined for a scroll (repaint only). Offsets are clamped to
101
+ * `state.maxOffset`, and pages by `state.viewport` — both set by the most recent
102
+ * render from the real viewport.
103
+ */
104
+ export function applyBtwKey(state: BtwPanelState, key: BtwKey): BtwEffect | undefined {
105
+ state.copied = false;
106
+ const step = pageStep(state.viewport);
107
+ switch (key.kind) {
108
+ case "up":
109
+ state.offset = Math.max(0, state.offset - 1);
110
+ return undefined;
111
+ case "down":
112
+ state.offset = Math.min(state.maxOffset, state.offset + 1);
113
+ return undefined;
114
+ case "pageUp":
115
+ state.offset = Math.max(0, state.offset - step);
116
+ return undefined;
117
+ case "pageDown":
118
+ state.offset = Math.min(state.maxOffset, state.offset + step);
119
+ return undefined;
120
+ case "top":
121
+ state.offset = 0;
122
+ return undefined;
123
+ case "bottom":
124
+ state.offset = state.maxOffset;
125
+ return undefined;
126
+ case "copy":
127
+ return { kind: "copy" };
128
+ case "close":
129
+ return { kind: "close" };
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Render the panel to at most `height` lines. The header (the dim question) and
135
+ * footer (rule + key hint, with a scroll indicator and a copy confirmation) are
136
+ * fixed; the answer scrolls within what remains. Clamps and writes back
137
+ * `state.offset`/`state.maxOffset` so key handling scrolls against the real
138
+ * body length. Every line is width-truncated (pi-tui crashes on an overwide
139
+ * line).
140
+ */
141
+ export function renderBtwPanel(
142
+ input: { state: BtwPanelState; question: string; body: BtwBody; width: number; height: number },
143
+ theme?: unknown,
144
+ ): string[] {
145
+ const { state, question, body, width, height } = input;
146
+ const paint = safeThemePaint(theme);
147
+ const inner = Math.max(1, width);
148
+
149
+ // Cap the echoed question so a long one (a pasted paragraph or URL) cannot
150
+ // grow the header past the panel height and starve the answer of rows; a
151
+ // truncated last line gets an ellipsis. The answer is what scrolls, not the
152
+ // question.
153
+ const questionLines = wrapPlainText(question.trim(), inner);
154
+ const shownQuestion =
155
+ questionLines.length > MAX_QUESTION_LINES
156
+ ? [...questionLines.slice(0, MAX_QUESTION_LINES - 1), truncateLine(`${questionLines[MAX_QUESTION_LINES - 1]}…`, inner)]
157
+ : questionLines;
158
+ const header = [
159
+ paint("accent", truncateLine("Side question", inner)),
160
+ ...shownQuestion.map((line) => paint("muted", truncateLine(line, inner))),
161
+ panelTopRule(paint, inner),
162
+ ];
163
+
164
+ const bodyAll = wrapPlainText(bodyText(body), inner);
165
+ const capacity = Math.max(1, height - header.length - 2); // 2 = footer rule + hint
166
+ state.viewport = capacity;
167
+ state.maxOffset = Math.max(0, bodyAll.length - capacity);
168
+ state.offset = Math.min(Math.max(0, state.offset), state.maxOffset);
169
+
170
+ const visible = bodyAll.slice(state.offset, state.offset + capacity).map((line) => truncateLine(line, inner));
171
+ // Pad a short answer so the footer sits at the panel's foot, not mid-screen.
172
+ while (visible.length < capacity) visible.push("");
173
+
174
+ const scrollNote = state.maxOffset > 0 ? ` (${state.offset + 1}-${Math.min(state.offset + capacity, bodyAll.length)} of ${bodyAll.length})` : "";
175
+ const hint = state.copied ? "Copied to clipboard" : `${BTW_HINT}${scrollNote}`;
176
+ const footer = [panelTopRule(paint, inner), paint("muted", truncateLine(hint, inner))];
177
+
178
+ return [...header, ...visible, ...footer];
179
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pure pieces of Claude Code's `/btw` ("by the way") side-question feature: a
3
+ * lightweight, one-off question answered against the current conversation's
4
+ * context while the main agent keeps working. Prompt text is verbatim from CC's
5
+ * capture (captures/btw.json, CC 2.1.278): a `<system-reminder>` that frames the
6
+ * answering model as a separate, tool-less instance, prepended to the user's
7
+ * question as the final user message.
8
+ *
9
+ * Kept free of pi imports so the message assembly and text extraction are
10
+ * unit-tested; the extension owns the model call, the spinner, and the panel.
11
+ */
12
+
13
+ /**
14
+ * CC's verbatim side-question reminder (captures/btw.json message 20). It frames
15
+ * the answering instance as a separate, lightweight, tool-less agent so the
16
+ * model answers in one shot from context instead of trying to act. One Code
17
+ * backs this framing for real: it sends name-only tool stubs (never live tools)
18
+ * and does not persist the exchange, so there genuinely is no follow-up turn.
19
+ */
20
+ export const SIDE_QUESTION_REMINDER = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.
21
+
22
+ IMPORTANT CONTEXT:
23
+ - You are a separate, lightweight agent spawned to answer this one question
24
+ - The main agent is NOT interrupted - it continues working independently in the background
25
+ - You share the conversation context but are a completely separate instance
26
+ - Do NOT reference being interrupted or what you were "previously doing" - that framing is incorrect
27
+
28
+ CRITICAL CONSTRAINTS:
29
+ - You have NO tools available - you cannot read files, run commands, search, or take any actions
30
+ - Do NOT write tool calls or tool output as text (for example invoke or function_calls XML blocks) - nothing you write here is executed; if answering would need reading files, running commands, or searching, say that can't be checked from a side question and suggest asking in the main conversation
31
+ - This is a one-off response - there will be no follow-up turns
32
+ - You can ONLY provide information based on what you already know from the conversation context
33
+ - NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
34
+ - If you don't know the answer, say so - do not offer to look it up or investigate
35
+
36
+ Simply answer the question with the information you have.</system-reminder>`;
37
+
38
+ /**
39
+ * The final user message a `/btw` call sends: CC's reminder, a blank line, then
40
+ * the user's question verbatim (captures/btw.json message 20 is exactly this
41
+ * string). The question is trimmed of surrounding whitespace only.
42
+ */
43
+ export function sideQuestionMessage(question: string): string {
44
+ return `${SIDE_QUESTION_REMINDER}\n\n${question.trim()}`;
45
+ }
@@ -13,9 +13,20 @@
13
13
  * a clean context — which is what a `/clear` habit wants. Like `/exit`→`/quit`,
14
14
  * we cannot fold this into pi's `/new` palette entry (pi has no command
15
15
  * aliases), so `/clear` and `/new` are two entries that both reset.
16
+ *
17
+ * Claude Code tells the model the conversation was cleared: the new
18
+ * conversation opens with the `/clear` breadcrumb (caveat, command, empty
19
+ * stdout — lib/local-command.ts). It cannot be queued from the handler: pi's
20
+ * `newSession()` replaces the whole runtime (a `session_shutdown` with reason
21
+ * "new" on the old extension runner, every factory run again — findings §8),
22
+ * so a reminder queued before the call dies with the old queue. The NEW
23
+ * instance announces it instead, from its `session_start` with reason "new" —
24
+ * which is also what pi's own `/new` and an RPC `new_session` produce, and
25
+ * they are the same user action as far as the model is concerned.
16
26
  */
17
27
 
18
28
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
29
+ import { announceLocalCommand } from "../lib/local-command.ts";
19
30
 
20
31
  export default function clearExtension(pi: ExtensionAPI) {
21
32
  pi.registerCommand("clear", {
@@ -24,4 +35,8 @@ export default function clearExtension(pi: ExtensionAPI) {
24
35
  await ctx.newSession();
25
36
  },
26
37
  });
38
+
39
+ pi.on("session_start", (event) => {
40
+ if ((event as { reason?: string }).reason === "new") announceLocalCommand(pi, { name: "clear" });
41
+ });
27
42
  }
@@ -27,16 +27,19 @@
27
27
  *
28
28
  * Both decisions ask pi-ai's `clampMaxTokensToContext` — the very function
29
29
  * `completeSimple` applies to the request — rather than re-deriving its
30
- * arithmetic, so they can never disagree with the clamp they predict. One
31
- * trap in that estimator: it anchors on the last assistant message's
32
- * `usage` total when there is one (`withoutUsage`).
30
+ * arithmetic, so they can never disagree with the clamp they predict. They
31
+ * `normalizeContext` the request first, exactly as `completeSimple` does, so the
32
+ * prompt and tools are counted (pi 0.86.1 carries them in a leading system
33
+ * message, not separate `Context` fields). One trap in that estimator: it
34
+ * anchors on the last assistant message's `usage` total when there is one
35
+ * (`withoutUsage`).
33
36
  */
34
37
 
35
38
  import { type Api, type Context, contentText, type Message, type Model } from "@earendil-works/pi-ai";
36
39
  import { PREVIEW_BYTES } from "../lib/persisted-output.ts";
37
40
  // Vendored from pi-ai: importing its deep subpaths directly breaks under the
38
41
  // bundled app's library loader (distribution review 2026-09-09, H1).
39
- import { clampMaxTokensToContext, estimateMessageTokens } from "../lib/pi-ai-estimate.ts";
42
+ import { clampMaxTokensToContext, estimateMessageTokens, normalizeContext } from "../lib/pi-ai-estimate.ts";
40
43
 
41
44
  /**
42
45
  * The least output room a replay must leave for the summary. Below it the
@@ -60,7 +63,7 @@ export const CLEARED_RESULT_HEAD_CHARS = PREVIEW_BYTES;
60
63
  */
61
64
  export function replayFits(reason: "manual" | "threshold" | "overflow", model: Model<Api>, request: Context, maxTokens: number): boolean {
62
65
  if (reason === "overflow") return false;
63
- return clampMaxTokensToContext(model, request, maxTokens) >= MIN_REPLAY_SUMMARY_TOKENS;
66
+ return clampMaxTokensToContext(model, normalizeContext(request), maxTokens) >= MIN_REPLAY_SUMMARY_TOKENS;
64
67
  }
65
68
 
66
69
  /**
@@ -80,7 +83,7 @@ export function fitToBudget(model: Model<Api>, request: Context, maxTokens: numb
80
83
 
81
84
  let cleared = 0;
82
85
  for (const { index } of candidates) {
83
- if (clampMaxTokensToContext(model, fitted, maxTokens) >= STANDALONE_SUMMARY_TOKENS) break;
86
+ if (clampMaxTokensToContext(model, normalizeContext(fitted), maxTokens) >= STANDALONE_SUMMARY_TOKENS) break;
84
87
  messages[index] = clearToolResult(messages[index]);
85
88
  cleared++;
86
89
  }
@@ -17,7 +17,10 @@ import { readJsonFile } from "../lib/atomic-write.ts";
17
17
  import { oneCodeSettingsPath } from "../lib/one-code-settings.ts";
18
18
  import { webSearchSettingsFrom } from "../web/backends.ts";
19
19
  import { collectCompat, importedConfigSection, mcpSection } from "./compat.ts";
20
- import { checkDependencies, dependenciesSection } from "./dependencies.ts";
20
+ import { checkDependencies, dependenciesSection, type ShellsInput } from "./dependencies.ts";
21
+ import { readSettingsEnv } from "../lib/claude-settings.ts";
22
+ import { resolveBashSpawn, resolvePowerShellSpawn } from "../lib/shell-spawn.ts";
23
+ import { shellToolPolicy } from "../powershell/policy.ts";
21
24
  import { collectModelFacts, modelsSection } from "./models.ts";
22
25
  import { computePresets, presetsSection } from "./presets.ts";
23
26
  import {
@@ -181,7 +184,7 @@ export function buildDoctorReport({ env, registry, session }: BuildInput): Docto
181
184
  sections.push(mcpSection(compat, session, env.home));
182
185
 
183
186
  const webSearchSettings = webSearchSettingsFrom(readJsonFile<{ webSearch?: unknown }>(oneCodeSettingsPath(env.home, env.env))?.webSearch);
184
- const deps = checkDependencies({ cwd: env.cwd, env: env.env, platform: env.platform, mcpServers: compat.mcp.servers, sessionModel: session.model, webSearchSettings });
187
+ const deps = checkDependencies({ cwd: env.cwd, env: env.env, platform: env.platform, agentDir: env.agentDir, mcpServers: compat.mcp.servers, sessionModel: session.model, webSearchSettings, shells: resolveShells(env.env, env.home, env.platform) });
185
188
  findings.push(...deps.findings);
186
189
  sections.push(dependenciesSection(deps));
187
190
 
@@ -224,3 +227,20 @@ function summaryText(input: {
224
227
  export function oneCodeVersion(env: NodeJS.ProcessEnv = process.env): string {
225
228
  return env.CC_VERSION ?? extensionVersion();
226
229
  }
230
+
231
+ /** The shell tools exactly as a session resolves them (bash/index.ts, powershell/index.ts). */
232
+ export function resolveShells(procEnv: NodeJS.ProcessEnv, home: string, platform: string): ShellsInput {
233
+ const plat = platform as NodeJS.Platform;
234
+ const bash = resolveBashSpawn({ env: procEnv, settingsEnv: readSettingsEnv(home) });
235
+ const powershell = resolvePowerShellSpawn({ platform: plat, env: procEnv });
236
+ const policy = shellToolPolicy({ platform: plat, env: procEnv, bash: !!bash.spawn, powershell: !!powershell });
237
+ return {
238
+ bash: bash.spawn?.shell,
239
+ bashWarning: bash.warning,
240
+ powershell: powershell?.shell,
241
+ powershellWanted: policy.powershellWanted,
242
+ bashExpected: policy.bashExpected,
243
+ primary: policy.primary,
244
+ notices: policy.notices,
245
+ };
246
+ }