pi-better-btw-plus 1.1.1 → 1.3.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/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/package.json +1 -1
- package/srcs/shortcuts.ts +8 -1
- package/srcs/side-chat-messages.ts +3 -2
- package/srcs/side-chat-overlay.ts +34 -7
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@ Press `Esc` to close it. Reopen with `/btw` or `Alt+W` to continue where you lef
|
|
|
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
79
|
| `Ctrl+L` | Open the fork model picker (scoped + authenticated models; `↑/↓` select, `Enter` confirm, `Esc` cancel) |
|
|
80
|
+
| `Alt+Shift+C` | Copy the whole input editor text (paste markers expanded — exactly what a submit would send) |
|
|
80
81
|
|
|
81
82
|
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
83
|
|
|
@@ -121,6 +122,7 @@ What changed since I opened this side chat?
|
|
|
121
122
|
| Double-click | Select the whole rendered line |
|
|
122
123
|
| `Ctrl+C` / `Ctrl+Shift+C` | Copy the active mouse selection (the selection is kept until you click elsewhere, so repeated presses re-copy) |
|
|
123
124
|
| `Ctrl+X` | Copy the last assistant message |
|
|
125
|
+
| `Alt+Shift+C` | Copy the whole input editor text (paste markers expanded — exactly what a submit would send; empty input shows `Input is empty`) |
|
|
124
126
|
| `Ctrl+V` / `Alt+V` | Paste the system clipboard at the cursor (same normalization + `[paste #N …]` markers as right-click paste) |
|
|
125
127
|
| Mouse right-click (chat area) | Copy the retained mouse selection (fires on release, keeps the highlight) |
|
|
126
128
|
| Mouse right-click (input editor) | Paste the system clipboard at the cursor (editor normalization + `[paste #N …]` markers for large pastes) |
|
|
@@ -217,7 +219,7 @@ Structure:
|
|
|
217
219
|
│ ├── side-chat-mouse.ts # minimal SGR mouse parsing
|
|
218
220
|
│ ├── clipboard-read.ts # platform clipboard read (win32 / darwin / linux + OSC 52 fallback)
|
|
219
221
|
│ ├── retry.ts # turn-level retry: classifyRetryable + runWithRetry
|
|
220
|
-
│ ├── shortcuts.ts # hotkey bindings (Alt+W / Alt+T)
|
|
222
|
+
│ ├── shortcuts.ts # hotkey bindings (Alt+W bg / Alt+T mode / Alt+Shift+C copy input)
|
|
221
223
|
│ ├── model-switch.ts # Ctrl+L fork model picker: list building + thinking clamp
|
|
222
224
|
│ ├── side-chat-export.ts # Alt+E transcript export
|
|
223
225
|
│ ├── tool-wrapper.ts # write-path overlap warnings
|
package/README.zh-CN.md
CHANGED
|
@@ -79,6 +79,7 @@ pi install npm:pi-better-btw-plus
|
|
|
79
79
|
| `Alt+N` | 开始空白对话 |
|
|
80
80
|
| `Alt+E` | 导出对话记录到 `$CWD/.agents/eval/pi-better-btw-<timestamp>.md` |
|
|
81
81
|
| `Ctrl+L` | 打开 fork 模型选择器(scoped + 已认证模型;`↑/↓` 选择,`Enter` 确认,`Esc` 取消) |
|
|
82
|
+
| `Alt+Shift+C` | 复制输入编辑器全部文本(paste 标记展开——与提交给 agent 的内容一致) |
|
|
82
83
|
|
|
83
84
|
在Readonly Mode(default),只读车道是**强制的**:越权调用工具会被硬阻断并注入prompt;第二次违规会升级措辞并中止该轮,提示(`🚧 lane blocked` 状态行)。已执行但失败的只读调用会被 `afterToolCall` 备注再次归位。编辑模式(`Alt+T`)不受影响。
|
|
84
85
|
|
|
@@ -122,6 +123,7 @@ What changed since I opened this side chat?
|
|
|
122
123
|
| 双击 | 选择整行 |
|
|
123
124
|
| `Ctrl+C` / `Ctrl+Shift+C` | 复制当前鼠标选择(选择保留到下次点击,可重复复制) |
|
|
124
125
|
| `Ctrl+X` | 复制最后一条 assistant 消息 |
|
|
126
|
+
| `Alt+Shift+C` | 复制输入编辑器全部文本(paste 标记展开——与提交给 agent 的内容一致;空输入提示 `Input is empty`) |
|
|
125
127
|
| `Ctrl+V` / `Alt+V` | 把系统剪贴板粘贴到光标处(归一化 + `[paste #N …]` 标记,同右键粘贴) |
|
|
126
128
|
| 鼠标右键(聊天区) | 复制保留的鼠标选择(松开时触发,保持高亮) |
|
|
127
129
|
| 鼠标右键(输入框) | 把系统剪贴板粘贴到光标处(编辑器归一化 + 大段 `[paste #N …]` 标记) |
|
|
@@ -219,7 +221,7 @@ btw 上下文保留主线的 system prompt 于 system 槽位,并逐字注入 f
|
|
|
219
221
|
│ ├── clipboard-read.ts # 平台剪贴板读取(win32 / darwin / linux + OSC 52 兜底)
|
|
220
222
|
│ ├── retry.ts # turn 级重试引擎:classifyRetryable + runWithRetry
|
|
221
223
|
│ ├── model-switch.ts # Ctrl+L fork 模型选择器:列表构建 + thinking 钳制
|
|
222
|
-
│ ├── shortcuts.ts # 快捷键绑定(Alt+W / Alt+T
|
|
224
|
+
│ ├── shortcuts.ts # 快捷键绑定(Alt+W 后台 / Alt+T 模式 / Alt+Shift+C 复制输入)
|
|
223
225
|
│ ├── side-chat-export.ts # Alt+E 对话导出
|
|
224
226
|
│ ├── tool-wrapper.ts # 写路径重叠警告
|
|
225
227
|
│ └── file-activity-tracker.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-btw-plus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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": [
|
package/srcs/shortcuts.ts
CHANGED
|
@@ -28,9 +28,16 @@ export const KEYBINDINGS = {
|
|
|
28
28
|
/** Read-only / edit mode toggle (Ctrl+T freed for pi's thinking toggle). */
|
|
29
29
|
toggleMode: { keys: ["alt+t"], hint: "A+t" },
|
|
30
30
|
/** Copy the active mouse selection (pi `tui.input.copy` parity). */
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Copy the active mouse selection (pi `tui.input.copy` parity). Without
|
|
33
|
+
* one, bare Ctrl+C clears the input box (pi `app.clear` parity, spec #22);
|
|
34
|
+
* Ctrl+Shift+C stays a forced copy.
|
|
35
|
+
*/
|
|
36
|
+
copySelection: { keys: ["ctrl+c", "ctrl+shift+c"], hint: "C+c copy/clear" },
|
|
32
37
|
/** Copy the last side-chat assistant message (pi `app.message.copy` parity). */
|
|
33
38
|
copyLastMessage: { keys: ["ctrl+x"], hint: "C+x last" },
|
|
39
|
+
/** Copy all input editor text (expanded paste markers — submit semantics). */
|
|
40
|
+
copyInput: { keys: ["alt+shift+c"], hint: "A+⇧C all" },
|
|
34
41
|
/** Paste clipboard text (pi `app.clipboard.pasteImage` parity). */
|
|
35
42
|
paste: { keys: ["ctrl+v", "alt+v"], hint: "C+v paste" },
|
|
36
43
|
} as const satisfies Record<string, Keybinding>;
|
|
@@ -57,8 +57,9 @@ export class SideChatMessages implements Component {
|
|
|
57
57
|
private totalLines = 0;
|
|
58
58
|
/**
|
|
59
59
|
* Active mouse selection over the visible chat lines ({@link CellPos} in
|
|
60
|
-
* rendered-line coordinates). Cleared on scroll / content changes
|
|
61
|
-
*
|
|
60
|
+
* rendered-line coordinates). Cleared on scroll / content changes, on the
|
|
61
|
+
* next press, and after a successful copy (spec #22: Ctrl+C with no
|
|
62
|
+
* selection clears the input instead of re-copying).
|
|
62
63
|
*/
|
|
63
64
|
private selection: { anchor: CellPos; focus: CellPos } | null = null;
|
|
64
65
|
/** Plain (ANSI-stripped) text of the last rendered lines, for hit-testing and copy. */
|
|
@@ -134,6 +134,8 @@ const PASTE_EMPTY_STATUS = "Clipboard is empty";
|
|
|
134
134
|
const PASTE_STATUS_CLEAR_MS = 1200;
|
|
135
135
|
/** Ctrl+X hint when the side chat has produced no assistant reply yet. */
|
|
136
136
|
const NO_ASSISTANT_MESSAGE_STATUS = "No assistant message to copy yet";
|
|
137
|
+
/** Alt+Shift+C hint when the input editor is empty (issue #23). */
|
|
138
|
+
const INPUT_EMPTY_STATUS = "Input is empty";
|
|
137
139
|
|
|
138
140
|
/**
|
|
139
141
|
* Shared-prefix layout (#9, reverses decision #6): the main lane's system
|
|
@@ -269,14 +271,20 @@ export class SideChatOverlay implements Component, Focusable {
|
|
|
269
271
|
* Copy the current mouse selection to the clipboard (native → wl-copy /
|
|
270
272
|
* xclip → OSC 52 cascade via {@link copyToClipboard}, matching the main
|
|
271
273
|
* app's tree selector) and show a transient status. Copying is hotkey-only:
|
|
272
|
-
* `Ctrl+C` / `Ctrl+Shift+C` with an active mouse selection.
|
|
273
|
-
*
|
|
274
|
-
*
|
|
274
|
+
* `Ctrl+C` / `Ctrl+Shift+C` with an active mouse selection. A successful
|
|
275
|
+
* copy consumes the selection (spec #22: Ctrl+C then returns to clearing
|
|
276
|
+
* the input); a failed copy keeps it so the user can retry. Returns false
|
|
277
|
+
* when there is nothing to copy.
|
|
275
278
|
*/
|
|
276
279
|
async copySelectionToClipboard(): Promise<boolean> {
|
|
277
280
|
const text = this.messages.getSelectedText();
|
|
278
281
|
if (!text) return false;
|
|
279
|
-
|
|
282
|
+
const ok = await this.copyTextWithFeedback(text);
|
|
283
|
+
if (ok) {
|
|
284
|
+
this.messages.clearSelection();
|
|
285
|
+
this.options.tui.requestRender();
|
|
286
|
+
}
|
|
287
|
+
return ok;
|
|
280
288
|
}
|
|
281
289
|
|
|
282
290
|
/**
|
|
@@ -902,14 +910,33 @@ export class SideChatOverlay implements Component, Focusable {
|
|
|
902
910
|
this.openModelPicker();
|
|
903
911
|
return;
|
|
904
912
|
}
|
|
913
|
+
if (matchesAnyKey(data, KEYBINDINGS.copyInput.keys)) {
|
|
914
|
+
// Copy the whole input editor text (issue #23): expanded paste
|
|
915
|
+
// markers — exactly what a submit would send. Read-only, unlike
|
|
916
|
+
// Ctrl+C's clear lane it never touches the draft; an empty input
|
|
917
|
+
// flashes a hint instead of copying.
|
|
918
|
+
const text = this.editor.getExpandedText();
|
|
919
|
+
if (!text) {
|
|
920
|
+
this.status.flash(INPUT_EMPTY_STATUS, COPIED_STATUS_CLEAR_MS);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
void this.copyTextWithFeedback(text, "input");
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
905
926
|
if (matchesAnyKey(data, KEYBINDINGS.copySelection.keys)) {
|
|
906
927
|
// Hotkey copy: with an active mouse selection, Ctrl+C / Ctrl+Shift+C
|
|
907
|
-
// copies it. Without one,
|
|
908
|
-
//
|
|
928
|
+
// copies it. Without one, Ctrl+C clears the input box (pi `app.clear`
|
|
929
|
+
// parity, spec #22); Ctrl+Shift+C falls through — pi binds no such
|
|
930
|
+
// key, so it stays a forced copy (terminal habit).
|
|
909
931
|
if (this.messages.hasSelection()) {
|
|
910
932
|
void this.copySelectionToClipboard();
|
|
911
933
|
return;
|
|
912
934
|
}
|
|
935
|
+
if (matchesKey(data, KEYBINDINGS.copySelection.keys[0])) {
|
|
936
|
+
this.editor.setText("");
|
|
937
|
+
this.options.tui.requestRender();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
913
940
|
}
|
|
914
941
|
if (matchesAnyKey(data, KEYBINDINGS.copyLastMessage.keys)) {
|
|
915
942
|
// app.message.copy parity: copy the last assistant message — no
|
|
@@ -1206,7 +1233,7 @@ export function buildSideChatHintLines(options: {
|
|
|
1206
1233
|
const rightClickHint = features.rightClickCopyPaste
|
|
1207
1234
|
? " · R-click copy/paste"
|
|
1208
1235
|
: "";
|
|
1209
|
-
const primary = `${scrollHint} · ${KEYBINDINGS.copySelection.hint} · ${KEYBINDINGS.copyLastMessage.hint} · ${KEYBINDINGS.paste.hint}${rightClickHint} · ${modeHint} · ${escHint} · Enter send`;
|
|
1236
|
+
const primary = `${scrollHint} · ${KEYBINDINGS.copySelection.hint} · ${KEYBINDINGS.copyLastMessage.hint} · ${KEYBINDINGS.copyInput.hint} · ${KEYBINDINGS.paste.hint}${rightClickHint} · ${modeHint} · ${escHint} · Enter send`;
|
|
1210
1237
|
const secondary = `${ALT_ACTIONS_BASE}${features.modelSwitch ? ` · ${KEYBINDINGS.modelPicker.hint}` : ""}`;
|
|
1211
1238
|
return [primary, secondary];
|
|
1212
1239
|
}
|