pi-better-btw-plus 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,7 +32,7 @@ You're in the middle of a longer task and want to ask something small without de
32
32
  Everything in [`@yceachan/pi-better-btw`](https://www.npmjs.com/package/@yceachan/pi-better-btw) is here, plus:
33
33
 
34
34
  - **Right-click copy & paste** — drag-select chat text and right-click to copy (Windows Terminal muscle memory); right-click inside the input editor pastes the system clipboard through the editor's own normalization and `[paste #N …]` markers for large pastes. No more hotkey-only copying.
35
- - **Fork model switching (`Alt+M`)** — pick any authenticated model for the side chat without rebuilding the fork; fork-local only (ADR 0002), thinking level auto-clamped to the new model's capabilities.
35
+ - **Fork model switching (`Ctrl+L`)** — pick any authenticated model for the side chat without rebuilding the fork; fork-local only (ADR 0002), thinking level auto-clamped to the new model's capabilities.
36
36
  - **Turn-level auto-retry** — shares the main session's `settings.retry` budget: transient provider errors back off and retry with a live countdown; `Esc` cancels.
37
37
  - **Feature kill switches** — the layered config's `features` block turns any of the above off (`rightClickCopyPaste` / `modelSwitch` / `retry`), plus `readOnlyExtensionAllowlistExclude` to drop bundled allowlist defaults.
38
38
 
@@ -54,7 +54,7 @@ See [Feat](#feat) for the full feature set.
54
54
 
55
55
  - `TUI scroll, select, copy`: subscribes to mouse/hotkey events in the TUI overlay for scrolling, text selection, and `Ctrl+C` copy.
56
56
 
57
- - `Readonly/Edit Mode`: read-only by default to answer btw questions; if you want the agent to make small edits along the way, `Ctrl+T` switches to edit mode.
57
+ - `Readonly/Edit Mode`: read-only by default to answer btw questions; if you want the agent to make small edits along the way, `Alt+T` switches to edit mode.
58
58
 
59
59
  - ToolAllowList: bundle + config.json custom
60
60
 
@@ -72,13 +72,13 @@ Press `Esc` to close it. Reopen with `/btw` or `Alt+W` to continue where you lef
72
72
  | Shortcut | Action |
73
73
  | -------- | ------ |
74
74
  | `Alt+W` | Open (when closed) / background (when visible) / restore (when hidden) |
75
- | `Ctrl+T` | Toggle read-only / edit mode |
75
+ | `Alt+T` | Toggle read-only / edit mode |
76
76
  | `Alt+R` | Re-fork from the latest main context |
77
77
  | `Alt+N` | Start an empty conversation |
78
78
  | `Alt+E` | Export the transcript to `$CWD/.agents/eval/pi-better-btw-<timestamp>.md` |
79
- | `Alt+M` | Open the fork model picker (scoped + authenticated models; `↑/↓` select, `Enter` confirm, `Esc` cancel) |
79
+ | `Ctrl+L` | Open the fork model picker (scoped + authenticated models; `↑/↓` select, `Enter` confirm, `Esc` cancel) |
80
80
 
81
- In Read-only Mode (default), the read-only lane is **enforced**: attempting an out-of-lane tool call is hard-blocked with a prompt injection; a second violation escalates the wording and aborts the turn (a `🚧 lane blocked` status line). Executed-but-failed read-only calls are re-grounded by an `afterToolCall` note. Edit mode (`Ctrl+T`) is unaffected.
81
+ In Read-only Mode (default), the read-only lane is **enforced**: attempting an out-of-lane tool call is hard-blocked with a prompt injection; a second violation escalates the wording and aborts the turn (a `🚧 lane blocked` status line). Executed-but-failed read-only calls are re-grounded by an `afterToolCall` note. Edit mode (`Alt+T`) is unaffected.
82
82
 
83
83
  **Peek at the main agent** — the `peek_main` tool reads recent activity from the main session.
84
84
 
@@ -98,7 +98,7 @@ What changed since I opened this side chat?
98
98
  **Right-click paste in the input box** — right-click inside the editor pastes the system clipboard at the cursor through the editor's built-in paste entry: line endings/tabs are normalized (`\r`→`\n`, `\t`→4 spaces), large pastes (>10 lines or >1000 chars) collapse to a `[paste #N +X lines]` / `[paste #N X chars]` marker that expands back to full text on submit, and the paste is a single undo step. When the clipboard can't be read (platform channel + OSC 52 fallback both unavailable) or holds no text, a one-line hint appears and the editor is left untouched.
99
99
 
100
100
  **Transcript export** — `Alt+E` dumps the btw history (forked context, framing block, conversation, in-flight stream) to `$CWD/.agents/eval/pi-better-btw-<timestamp>.md` as a markdown diagnostic artifact, useful for debugging feature work.
101
- **Fork model switching** — `Alt+M` opens a model picker inside the overlay (`↑/↓` move, `Enter` confirm, `Esc` cancel). It lists the session's scoped models first (`--models` / `enabledModels`), falling back to the available catalogue, and only shows models with configured auth. Confirming swaps the fork agent's runtime model — the next turn uses it without rebuilding the fork — and clamps the thinking level to the new model's capabilities (no-reasoning models go to `off`). The choice is fork-local (ADR 0002): the main session's model is never touched. It survives backgrounding (`Alt+W`) and resets on `Alt+R`/`Alt+N`/`Esc` close. The header shows the current fork model; opening is rejected while streaming.
101
+ **Fork model switching** — `Ctrl+L` opens a model picker inside the overlay (`↑/↓` move, `Enter` confirm, `Esc` cancel). It lists the session's scoped models first (`--models` / `enabledModels`), falling back to the available catalogue, and only shows models with configured auth. Confirming swaps the fork agent's runtime model — the next turn uses it without rebuilding the fork — and clamps the thinking level to the new model's capabilities (no-reasoning models go to `off`). The choice is fork-local (ADR 0002): the main session's model is never touched. It survives backgrounding (`Alt+W`) and resets on `Alt+R`/`Alt+N`/`Esc` close. The header shows the current fork model; opening is rejected while streaming.
102
102
  **Auto-retry (turn-level)** — reads pi's `settings.retry` budget (`enabled` / `maxRetries` / `baseDelayMs`, same defaults as the main session). Transient provider errors (overloaded / rate limit / 5xx) auto-retry with exponential backoff — the status area shows `Retrying (n/m) in Xs…` with a live countdown — and the failed assistant message is stripped before the retry so it never re-enters the next request. Context overflow and aborts never retry. `Esc` during the backoff cancels the wait and surfaces the last error as the final result; budget exhaustion does the same. With `enabled: false` (e.g. local-model debugging) errors surface immediately with zero overhead.
103
103
 
104
104
 
@@ -112,14 +112,16 @@ What changed since I opened this side chat?
112
112
  | `Alt+R` | Re-fork from latest main context |
113
113
  | `Alt+N` | Start empty conversation |
114
114
  | `Alt+E` | Export the btw chat history to `$CWD/.agents/eval/pi-better-btw-<timestamp>.md` |
115
- | `Alt+M` | Open the fork model picker (`↑/↓` select, `Enter` confirm, `Esc` cancel) |
116
- | `Ctrl+T` | Toggle read-only / edit mode |
115
+ | `Ctrl+L` | Open the fork model picker (`↑/↓` select, `Enter` confirm, `Esc` cancel) |
116
+ | `Alt+T` | Toggle read-only / edit mode |
117
117
  | `PgUp` / `PgDn` | Scroll history by a page |
118
118
  | `Shift+↑` / `Shift+↓` | Scroll by a few lines |
119
119
  | Mouse wheel | Scroll when the pointer is over the chat |
120
120
  | Mouse drag | Select text in the chat area (inverse-video highlight); no copy on release |
121
121
  | Double-click | Select the whole rendered line |
122
122
  | `Ctrl+C` / `Ctrl+Shift+C` | Copy the active mouse selection (the selection is kept until you click elsewhere, so repeated presses re-copy) |
123
+ | `Ctrl+X` | Copy the last assistant message |
124
+ | `Ctrl+V` / `Alt+V` | Paste the system clipboard at the cursor (same normalization + `[paste #N …]` markers as right-click paste) |
123
125
  | Mouse right-click (chat area) | Copy the retained mouse selection (fires on release, keeps the highlight) |
124
126
  | Mouse right-click (input editor) | Paste the system clipboard at the cursor (editor normalization + `[paste #N …]` markers for large pastes) |
125
127
  ## Command Reference
@@ -161,7 +163,7 @@ Keys:
161
163
  | Switch | Behavior when `false` |
162
164
  |--------|------------------------|
163
165
  | `rightClickCopyPaste` | right-click copy (chat) / paste (editor) is inert — the hotkeys still work |
164
- | `modelSwitch` | `Alt+M` does nothing |
166
+ | `modelSwitch` | `Ctrl+L` does nothing |
165
167
  | `retry` | fork turns run a single attempt with zero backoff, even if pi's `settings.retry` is enabled |
166
168
 
167
169
  Example (user or project layer):
@@ -215,8 +217,8 @@ Structure:
215
217
  │ ├── side-chat-mouse.ts # minimal SGR mouse parsing
216
218
  │ ├── clipboard-read.ts # platform clipboard read (win32 / darwin / linux + OSC 52 fallback)
217
219
  │ ├── retry.ts # turn-level retry: classifyRetryable + runWithRetry
218
- │ ├── shortcuts.ts # hotkey bindings (Alt+W / Ctrl+T)
219
- │ ├── model-switch.ts # Alt+M fork model picker: list building + thinking clamp
220
+ │ ├── shortcuts.ts # hotkey bindings (Alt+W / Alt+T)
221
+ │ ├── model-switch.ts # Ctrl+L fork model picker: list building + thinking clamp
220
222
  │ ├── side-chat-export.ts # Alt+E transcript export
221
223
  │ ├── tool-wrapper.ts # write-path overlap warnings
222
224
  │ └── file-activity-tracker.ts
package/README.zh-CN.md CHANGED
@@ -32,7 +32,7 @@ pi install npm:pi-better-btw-plus
32
32
  [`@yceachan/pi-better-btw`](https://www.npmjs.com/package/@yceachan/pi-better-btw) 的全部功能都在,另新增:
33
33
 
34
34
  - **右键复制与粘贴** —— 聊天区拖选后右键复制(延续 Windows Terminal 肌肉记忆);输入框右键从系统剪贴板粘贴,走编辑器内置归一化与大段 `[paste #N …]` 折叠标记。不再是 hotkey-only。
35
- - **Fork 模型切换(`Alt+M`)** —— 侧聊内选择任意已认证模型,无需重建 fork;仅作用于 fork 本身(ADR 0002),thinking level 按新模型能力自动钳制。
35
+ - **Fork 模型切换(`Ctrl+L`)** —— 侧聊内选择任意已认证模型,无需重建 fork;仅作用于 fork 本身(ADR 0002),thinking level 按新模型能力自动钳制。
36
36
  - **Turn 级自动重试** —— 与主会话共用 `settings.retry` 预算:瞬时 provider 错误指数退避重试,带实时倒计时;`Esc` 取消。
37
37
  - **功能开关** —— 分层配置的 `features` 块可关闭以上任意功能(`rightClickCopyPaste` / `modelSwitch` / `retry`),另有 `readOnlyExtensionAllowlistExclude` 移除内置 allowlist 默认项。
38
38
 
@@ -54,7 +54,7 @@ pi install npm:pi-better-btw-plus
54
54
 
55
55
  - `TUI scroll,select,copy` :在TUI-overlay 自订阅鼠标/hotkey事件,实现滚屏 ,text选中 , Ctrl +C 复制功能
56
56
 
57
- - `Readonly/Edit Mode` : 默认只读来回应btw question,如果你希望Agent顺手做些小修改,Ctrl + t To Edit Mode.
57
+ - `Readonly/Edit Mode` : 默认只读来回应btw question,如果你希望Agent顺手做些小修改,Alt + T To Edit Mode.
58
58
 
59
59
  - ToolAllowList : bundle + config.json custom
60
60
 
@@ -74,13 +74,13 @@ pi install npm:pi-better-btw-plus
74
74
  | 快捷键 | 作用 |
75
75
  | ------ | ---- |
76
76
  | `Alt+W` | 打开(关闭时)/ 后台化(显示时)/ 恢复(隐藏时) |
77
- | `Ctrl+T` | 切换只读 / 编辑模式 |
77
+ | `Alt+T` | 切换只读 / 编辑模式 |
78
78
  | `Alt+R` | 从最新主线上下文重新 fork |
79
79
  | `Alt+N` | 开始空白对话 |
80
80
  | `Alt+E` | 导出对话记录到 `$CWD/.agents/eval/pi-better-btw-<timestamp>.md` |
81
- | `Alt+M` | 打开 fork 模型选择器(scoped + 已认证模型;`↑/↓` 选择,`Enter` 确认,`Esc` 取消) |
81
+ | `Ctrl+L` | 打开 fork 模型选择器(scoped + 已认证模型;`↑/↓` 选择,`Enter` 确认,`Esc` 取消) |
82
82
 
83
- 在Readonly Mode(default),只读车道是**强制的**:越权调用工具会被硬阻断并注入prompt;第二次违规会升级措辞并中止该轮,提示(`🚧 lane blocked` 状态行)。已执行但失败的只读调用会被 `afterToolCall` 备注再次归位。编辑模式(`Ctrl+T`)不受影响。
83
+ 在Readonly Mode(default),只读车道是**强制的**:越权调用工具会被硬阻断并注入prompt;第二次违规会升级措辞并中止该轮,提示(`🚧 lane blocked` 状态行)。已执行但失败的只读调用会被 `afterToolCall` 备注再次归位。编辑模式(`Alt+T`)不受影响。
84
84
 
85
85
  **窥视主线 agent** —— `peek_main` 工具读取主线会话的近期活动。
86
86
 
@@ -101,7 +101,7 @@ What changed since I opened this side chat?
101
101
 
102
102
  **对话导出** —— `Alt+E` 把 btw 历史(fork 上下文、framing 块、对话、流式中内容)导出为 `$CWD/.agents/eval/pi-better-btw-<timestamp>.md` 的 markdown 诊断产物,便于调试功能开发。
103
103
 
104
- **Fork 模型切换** —— `Alt+M` 在浮层内打开模型选择器(`↑/↓` 移动,`Enter` 确认,`Esc` 取消)。列表优先展示会话作用域模型(`--models` / `enabledModels`),为空时回退可用模型目录,且只显示已配置认证的模型。确认后直接替换 fork agent 的运行时模型——下一 turn 生效,无需重建 fork——并按新模型能力钳制 thinking level(无 reasoning 的模型钳制为 `off`)。选择为 fork 局部状态(ADR 0002):主会话模型不受影响。后台化(`Alt+W`)保留选择;`Alt+R` / `Alt+N` / `Esc` 关闭后随实例重置。头部显示当前 fork 模型;流式期间拒绝打开。
104
+ **Fork 模型切换** —— `Ctrl+L` 在浮层内打开模型选择器(`↑/↓` 移动,`Enter` 确认,`Esc` 取消)。列表优先展示会话作用域模型(`--models` / `enabledModels`),为空时回退可用模型目录,且只显示已配置认证的模型。确认后直接替换 fork agent 的运行时模型——下一 turn 生效,无需重建 fork——并按新模型能力钳制 thinking level(无 reasoning 的模型钳制为 `off`)。选择为 fork 局部状态(ADR 0002):主会话模型不受影响。后台化(`Alt+W`)保留选择;`Alt+R` / `Alt+N` / `Esc` 关闭后随实例重置。头部显示当前 fork 模型;流式期间拒绝打开。
105
105
 
106
106
  ## 快捷键
107
107
 
@@ -113,14 +113,16 @@ What changed since I opened this side chat?
113
113
  | `Alt+R` | 从最新主线上下文重新 fork |
114
114
  | `Alt+N` | 开始空白对话 |
115
115
  | `Alt+E` | 导出 btw 对话历史到 `$CWD/.agents/eval/pi-better-btw-<timestamp>.md` |
116
- | `Alt+M` | 打开 fork 模型选择器(`↑/↓` 选择,`Enter` 确认,`Esc` 取消) |
117
- | `Ctrl+T` | 切换只读 / 编辑模式 |
116
+ | `Ctrl+L` | 打开 fork 模型选择器(`↑/↓` 选择,`Enter` 确认,`Esc` 取消) |
117
+ | `Alt+T` | 切换只读 / 编辑模式 |
118
118
  | `PgUp` / `PgDn` | 整页滚动历史 |
119
119
  | `Shift+↑` / `Shift+↓` | 按行滚动 |
120
120
  | 鼠标滚轮 | 指针位于聊天区域时滚动 |
121
121
  | 鼠标拖拽 | 选择聊天文本(反色高亮);松开不自动复制 |
122
122
  | 双击 | 选择整行 |
123
123
  | `Ctrl+C` / `Ctrl+Shift+C` | 复制当前鼠标选择(选择保留到下次点击,可重复复制) |
124
+ | `Ctrl+X` | 复制最后一条 assistant 消息 |
125
+ | `Ctrl+V` / `Alt+V` | 把系统剪贴板粘贴到光标处(归一化 + `[paste #N …]` 标记,同右键粘贴) |
124
126
  | 鼠标右键(聊天区) | 复制保留的鼠标选择(松开时触发,保持高亮) |
125
127
  | 鼠标右键(输入框) | 把系统剪贴板粘贴到光标处(编辑器归一化 + 大段 `[paste #N …]` 标记) |
126
128
  ## 命令参考
@@ -162,7 +164,7 @@ pi-better-btw 从三个位置按优先级递增读取 `config.json` —— 每
162
164
  | 开关 | 为 `false` 时的行为 |
163
165
  | ---- | ---- |
164
166
  | `rightClickCopyPaste` | 右键复制(聊天区)/ 粘贴(输入框)失效——快捷键不受影响 |
165
- | `modelSwitch` | `Alt+M` 无反应 |
167
+ | `modelSwitch` | `Ctrl+L` 无反应 |
166
168
  | `retry` | fork 每轮只跑一次尝试、零退避,即使 pi 的 `settings.retry` 开启 |
167
169
 
168
170
  示例(用户或项目层):
@@ -216,8 +218,8 @@ btw 上下文保留主线的 system prompt 于 system 槽位,并逐字注入 f
216
218
  │ ├── side-chat-mouse.ts # 最小 SGR 鼠标解析
217
219
  │ ├── clipboard-read.ts # 平台剪贴板读取(win32 / darwin / linux + OSC 52 兜底)
218
220
  │ ├── retry.ts # turn 级重试引擎:classifyRetryable + runWithRetry
219
- │ ├── model-switch.ts # Alt+M fork 模型选择器:列表构建 + thinking 钳制
220
- │ ├── shortcuts.ts # 快捷键绑定(Alt+W / Ctrl+T)
221
+ │ ├── model-switch.ts # Ctrl+L fork 模型选择器:列表构建 + thinking 钳制
222
+ │ ├── shortcuts.ts # 快捷键绑定(Alt+W / Alt+T)
221
223
  │ ├── side-chat-export.ts # Alt+E 对话导出
222
224
  │ ├── tool-wrapper.ts # 写路径重叠警告
223
225
  │ └── file-activity-tracker.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-btw-plus",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "pi extension: /btw side-chat overlay — maintained fork of @yceachan/pi-better-btw (+ right-click copy/paste, fork model switch, turn-level retry)",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1 +1 @@
1
- You are in the read-only lane: only read/grep/find/ls, allowlisted tools and peek_main are available here. You cannot write files, edit, or execute commands — that is the main lane's job. If a write is genuinely needed, tell the user to switch to edit mode (Ctrl+T) or ask the main agent.
1
+ You are in the read-only lane: only read/grep/find/ls, allowlisted tools and peek_main are available here. You cannot write files, edit, or execute commands — that is the main lane's job. If a write is genuinely needed, tell the user to switch to edit mode (Alt+T) or ask the main agent.
@@ -1 +1 @@
1
- 🚧 Lane blocked: `{{tool}}` is not available in this side chat. You are the btw side chat — a quick-question lane parallel to the main agent; the main line is the main agent's job. Answer the latest user message only, with read-only tools (read/grep/find/ls + allowlisted tools). If a write is genuinely needed, tell the user to switch to edit mode (Ctrl+T).
1
+ 🚧 Lane blocked: `{{tool}}` is not available in this side chat. You are the btw side chat — a quick-question lane parallel to the main agent; the main line is the main agent's job. Answer the latest user message only, with read-only tools (read/grep/find/ls + allowlisted tools). If a write is genuinely needed, tell the user to switch to edit mode (Alt+T).
@@ -4,10 +4,13 @@
4
4
  *
5
5
  * The write side is pi's own `copyToClipboard` (public export); the read side
6
6
  * is self-built because `readClipboardText` is not re-exported from the
7
- * package entry. Each platform maps to a primary channel:
7
+ * package entry. The PRIMARY channel mirrors pi's own `readClipboardText` (the
8
+ * `@mariozechner/clipboard` native addon); the remaining channels — PowerShell
9
+ * `Get-Clipboard -Raw` (win32), `pbpaste` (darwin), and an OSC 52 query — are
10
+ * extension-only fallbacks pi's read side does not have:
8
11
  *
9
- * - win32 → native addon `getText` (pi's own clipboard read), then
10
- * PowerShell `Get-Clipboard -Raw`, then an OSC 52 query;
12
+ * - win32 → native addon `getText`, then PowerShell `Get-Clipboard -Raw`,
13
+ * then an OSC 52 query;
11
14
  * - darwin → native addon `getText`, then `pbpaste`, then OSC 52 query;
12
15
  * - linux → OSC 52 query (`\x1b]52;c;?\x07`, read the reply).
13
16
  *
@@ -124,11 +127,11 @@ export interface CommandChannelOptions {
124
127
  }
125
128
 
126
129
  /**
127
- * PowerShell channel: `powershell.exe -NoProfile -Command "Get-Clipboard
128
- * -Raw"`, mirroring pi's clipboard-image PowerShell channel family. The
129
- * `-Raw` flag preserves the exact text (no extra newline), and the output
130
- * encoding is pinned to UTF-8 because the text travels over the console
131
- * pipe (pi's image reader sidesteps this by writing a temp file).
130
+ * PowerShell read fallback (extension-only; pi's readClipboardText has no
131
+ * PowerShell channel): `powershell.exe -NoProfile -Command "Get-Clipboard
132
+ * -Raw"`. The `-Raw` flag preserves the exact text (no extra newline), and the
133
+ * output encoding is pinned to UTF-8 because the text travels over the console
134
+ * pipe.
132
135
  */
133
136
  const PS_GET_CLIPBOARD_SCRIPT =
134
137
  "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard -Raw";
@@ -244,11 +247,11 @@ export function makeNativeChannel(
244
247
  }
245
248
 
246
249
  /**
247
- * PowerShell channel: `powershell.exe -NoProfile -Command "Get-Clipboard
248
- * -Raw"`, mirroring pi's clipboard-image PowerShell channel family. The
249
- * `-Raw` flag preserves the exact text (no extra newline), and the output
250
- * encoding is pinned to UTF-8 because the text travels over the console
251
- * pipe (pi's image reader sidesteps this by writing a temp file).
250
+ * PowerShell read fallback (extension-only; pi's readClipboardText has no
251
+ * PowerShell channel): `powershell.exe -NoProfile -Command "Get-Clipboard
252
+ * -Raw"`. The `-Raw` flag preserves the exact text (no extra newline), and the
253
+ * output encoding is pinned to UTF-8 because the text travels over the console
254
+ * pipe.
252
255
  */
253
256
  export function makePowerShellChannel(
254
257
  options: CommandChannelOptions = {},
package/srcs/config.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join } from "node:path";
4
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
4
5
  import type { PromptPackManifest } from "./prompt-pack.ts";
6
+ import type { ProviderRetrySettings } from "./provider-retry.ts";
5
7
  import type { RetryPolicy } from "./retry.ts";
6
8
  /**
7
9
  * Layered config resolution for pi-better-btw.
@@ -36,7 +38,7 @@ import type { RetryPolicy } from "./retry.ts";
36
38
  export interface SideChatFeatures {
37
39
  /** Right-click copy (chat selection) / paste (editor). Default: true. */
38
40
  rightClickCopyPaste: boolean;
39
- /** Alt+M fork model picker. Default: true. */
41
+ /** Ctrl+L fork model picker. Default: true. */
40
42
  modelSwitch: boolean;
41
43
  /** Turn-level auto-retry of transient provider errors. Default: true. */
42
44
  retry: boolean;
@@ -65,13 +67,15 @@ export const USER_CONFIG_DIR = join(homedir(), ".pi", "agent", CONFIG_SUBDIR);
65
67
  export const AGENT_CONFIG_DIR = join(homedir(), ".pi", "agent");
66
68
 
67
69
  /**
68
- * Read pi's `settings.retry` budget (D8). The fork shares pi's settings files
69
- * rather than re-declaring them: global <agentConfigDir>/settings.json merged
70
- * with project <cwd>/.pi/settings.json (project wins per key, mirroring pi's
71
- * deepMergeSettings), then the `retry` block is extracted with pi's defaults
72
- * (settingsManager.getRetrySettings: enabled=true, maxRetries=3,
73
- * baseDelayMs=2000). Invalid/absent files contribute nothing; a present-but-
74
- * unreadable file warns instead of failing the fork.
70
+ * Read pi's `settings.retry` budget (D8) via pi's own `SettingsManager` —
71
+ * file reading, deep merge (project wins per key), legacy migration
72
+ * (`retry.maxDelayMs` `retry.provider.maxRetryDelayMs`) and the canonical
73
+ * defaults (enabled=true, maxRetries=3, baseDelayMs=2000) all come from pi; no
74
+ * hand-rolled duplicate. The `retry.provider` block (spec #20 D4: timeoutMs /
75
+ * maxRetries / maxRetryDelayMs) is forwarded only when a provider block is
76
+ * actually configured, and only number keys; it is consumed solely by the
77
+ * overlay's stream assembly, never by the turn loop. A present-but-unreadable
78
+ * settings file warns and contributes nothing (pi's own fallback).
75
79
  */
76
80
  export interface LoadRetryPolicyOptions {
77
81
  /** Agent config dir holding pi's global settings.json (~/.pi/agent). */
@@ -85,53 +89,44 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
85
89
  return typeof value === "object" && value !== null && !Array.isArray(value);
86
90
  }
87
91
 
88
- /** Deep merge like pi's deepMergeSettings: nested plain objects merge, arrays/others replace. */
89
- function deepMergeSettings(
90
- base: Record<string, unknown>,
91
- overrides: Record<string, unknown>,
92
- ): Record<string, unknown> {
93
- const result = { ...base };
94
- for (const [key, value] of Object.entries(overrides)) {
95
- const baseValue = result[key];
96
- result[key] =
97
- isPlainRecord(baseValue) && isPlainRecord(value)
98
- ? deepMergeSettings(baseValue, value)
99
- : value;
100
- }
101
- return result;
102
- }
103
-
104
- function readSettingsFile(
105
- path: string,
106
- onWarning?: (message: string) => void,
107
- ): Record<string, unknown> {
108
- try {
109
- const raw: unknown = JSON.parse(readFileSync(path, "utf-8"));
110
- return isPlainRecord(raw) ? raw : {};
111
- } catch {
112
- if (existsSync(path) && onWarning) {
113
- onWarning(`pi-better-btw: ignoring invalid settings ${path}`);
114
- }
115
- return {};
116
- }
117
- }
118
92
 
119
93
  export function loadRetryPolicy(options: LoadRetryPolicyOptions = {}): RetryPolicy {
120
94
  const agentConfigDir = options.agentConfigDir ?? AGENT_CONFIG_DIR;
121
- const merged = deepMergeSettings(
122
- readSettingsFile(join(agentConfigDir, "settings.json"), options.onWarning),
123
- options.cwd
124
- ? readSettingsFile(join(options.cwd, ".pi", "settings.json"), options.onWarning)
125
- : {},
126
- );
127
- const retry = isPlainRecord(merged.retry) ? merged.retry : {};
95
+ const settings = SettingsManager.create(options.cwd ?? process.cwd(), agentConfigDir, {
96
+ projectTrusted: true,
97
+ });
98
+ // Surface load errors (invalid JSON etc.) through the caller's warning hook,
99
+ // mirroring the old loader's present-but-unreadable → warn behavior.
100
+ for (const err of settings.drainErrors()) {
101
+ options.onWarning?.(`pi-better-btw: ignoring invalid settings (${err.scope})`);
102
+ }
103
+ const retry = settings.getRetrySettings();
104
+ // Provider block (spec #20): forward only number keys from an actually
105
+ // configured `retry.provider`. getProviderRetrySettings() defaults
106
+ // maxRetryDelayMs to 60000 even when no block exists, so configured-ness is
107
+ // detected from the (already-migrated) per-scope settings; an absent block
108
+ // keeps `provider` undefined so the overlay's stream assembly stays identity
109
+ // (D3: unconfigured users keep the bare streamSimple, zero overhead).
110
+ const providerRetry = settings.getProviderRetrySettings();
111
+ const providerConfigured = [
112
+ settings.getGlobalSettings(),
113
+ settings.getProjectSettings(),
114
+ ].some((scope) => isPlainRecord(scope.retry) && isPlainRecord(scope.retry.provider));
115
+ const providerBlock: ProviderRetrySettings = {};
116
+ if (providerConfigured) {
117
+ // Number filter kept (vs. blindly forwarding getProviderRetrySettings) to
118
+ // keep the D3 identity contract stable: non-number keys never reach
119
+ // injectProviderRetry, and an unconfigured provider stays undefined.
120
+ if (typeof providerRetry.timeoutMs === "number") providerBlock.timeoutMs = providerRetry.timeoutMs;
121
+ if (typeof providerRetry.maxRetries === "number") providerBlock.maxRetries = providerRetry.maxRetries;
122
+ if (typeof providerRetry.maxRetryDelayMs === "number")
123
+ providerBlock.maxRetryDelayMs = providerRetry.maxRetryDelayMs;
124
+ }
128
125
  return {
129
- enabled:
130
- typeof retry.enabled === "boolean" ? retry.enabled : true,
131
- maxRetries:
132
- typeof retry.maxRetries === "number" ? retry.maxRetries : 3,
133
- baseDelayMs:
134
- typeof retry.baseDelayMs === "number" ? retry.baseDelayMs : 2000,
126
+ enabled: retry.enabled,
127
+ maxRetries: retry.maxRetries,
128
+ baseDelayMs: retry.baseDelayMs,
129
+ ...(Object.keys(providerBlock).length > 0 ? { provider: providerBlock } : {}),
135
130
  };
136
131
  }
137
132