pi-btw 0.3.8 → 0.4.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 +1 -1
- package/extensions/btw.ts +97 -31
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-btw
|
|
2
2
|
|
|
3
|
-
A small [pi](https://github.com/
|
|
3
|
+
A small [pi](https://github.com/earendil-works/pi-mono) extension that adds a `/btw` side conversation channel.
|
|
4
4
|
|
|
5
5
|
`/btw` opens a real pi sub-session with coding-tool access, and it runs immediately even while the main agent is still busy.
|
|
6
6
|
|
package/extensions/btw.ts
CHANGED
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
buildSessionContext,
|
|
3
3
|
createAgentSession,
|
|
4
4
|
createExtensionRuntime,
|
|
5
|
-
codingTools,
|
|
6
5
|
SessionManager,
|
|
7
6
|
type AgentSession,
|
|
8
7
|
type AgentSessionEvent,
|
|
@@ -10,8 +9,8 @@ import {
|
|
|
10
9
|
type ExtensionCommandContext,
|
|
11
10
|
type ExtensionContext,
|
|
12
11
|
type ResourceLoader,
|
|
13
|
-
} from "@
|
|
14
|
-
import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel, type UserMessage } from "@
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel, type UserMessage } from "@earendil-works/pi-ai";
|
|
15
14
|
import {
|
|
16
15
|
Box,
|
|
17
16
|
Container,
|
|
@@ -26,7 +25,7 @@ import {
|
|
|
26
25
|
type KeybindingsManager,
|
|
27
26
|
type OverlayHandle,
|
|
28
27
|
type TUI,
|
|
29
|
-
} from "@
|
|
28
|
+
} from "@earendil-works/pi-tui";
|
|
30
29
|
|
|
31
30
|
const BTW_MESSAGE_TYPE = "btw-note";
|
|
32
31
|
const BTW_ENTRY_TYPE = "btw-thread-entry";
|
|
@@ -56,6 +55,11 @@ const BTW_CONTINUE_THREAD_ASSISTANT_TEXT = "Understood, continuing our side conv
|
|
|
56
55
|
type SessionThinkingLevel = "off" | AiThinkingLevel;
|
|
57
56
|
type BtwThreadMode = "contextual" | "tangent";
|
|
58
57
|
type SessionModel = NonNullable<ExtensionCommandContext["model"]>;
|
|
58
|
+
/**
|
|
59
|
+
* Loose model reference parsed from `/btw:model <provider> <id> <api>` and persisted to
|
|
60
|
+
* session entries. Resolved to a full SessionModel via ctx.modelRegistry.find(...).
|
|
61
|
+
*/
|
|
62
|
+
type BtwModelRef = Pick<SessionModel, "provider" | "id" | "api">;
|
|
59
63
|
|
|
60
64
|
type BtwDetails = {
|
|
61
65
|
question: string;
|
|
@@ -216,7 +220,7 @@ function parseBtwArgs(args: string): ParsedBtwArgs {
|
|
|
216
220
|
function parseBtwModelArgs(args: string):
|
|
217
221
|
| { action: "show" }
|
|
218
222
|
| { action: "clear" }
|
|
219
|
-
| { action: "set"; model:
|
|
223
|
+
| { action: "set"; model: BtwModelRef }
|
|
220
224
|
| { action: "invalid"; message: string } {
|
|
221
225
|
const trimmed = args.trim();
|
|
222
226
|
if (!trimmed) {
|
|
@@ -233,7 +237,7 @@ function parseBtwModelArgs(args: string):
|
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
const [provider, id, api] = parts;
|
|
236
|
-
return { action: "set", model: { provider, id, api } };
|
|
240
|
+
return { action: "set", model: { provider, id, api } as BtwModelRef };
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
function parseBtwThinkingArgs(args: string):
|
|
@@ -997,6 +1001,9 @@ function notify(ctx: ExtensionContext | ExtensionCommandContext, message: string
|
|
|
997
1001
|
}
|
|
998
1002
|
}
|
|
999
1003
|
|
|
1004
|
+
/** Fixed overlay rows outside the transcript viewport (must match render() structure). */
|
|
1005
|
+
const BTW_OVERLAY_CHROME_LINES = 9;
|
|
1006
|
+
|
|
1000
1007
|
function getOverlayTitle(mode: BtwThreadMode): string {
|
|
1001
1008
|
return mode === "tangent" ? "BTW tangent" : "BTW";
|
|
1002
1009
|
}
|
|
@@ -1081,6 +1088,9 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1081
1088
|
|
|
1082
1089
|
this.hintsText = new Text("", 1, 0);
|
|
1083
1090
|
|
|
1091
|
+
// Enable SGR mouse reporting so wheel/touchpad events reach handleInput().
|
|
1092
|
+
this.tui.terminal?.write?.("\x1b[?1000h\x1b[?1006h");
|
|
1093
|
+
|
|
1084
1094
|
const originalHandleInput = this.input.handleInput.bind(this.input);
|
|
1085
1095
|
this.input.handleInput = (data: string) => {
|
|
1086
1096
|
if (keybindings.matches(data, "app.clear")) {
|
|
@@ -1107,17 +1117,17 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1107
1117
|
private frameLine(content: string, innerWidth: number): string {
|
|
1108
1118
|
const truncated = truncateToWidth(content, innerWidth, "");
|
|
1109
1119
|
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
1110
|
-
return `${this.theme.fg("
|
|
1120
|
+
return `${this.theme.fg("border", "│")}${truncated}${" ".repeat(padding)}${this.theme.fg("border", "│")}`;
|
|
1111
1121
|
}
|
|
1112
1122
|
|
|
1113
1123
|
private ruleLine(innerWidth: number): string {
|
|
1114
|
-
return this.theme.fg("
|
|
1124
|
+
return this.theme.fg("border", `├${"─".repeat(innerWidth)}┤`);
|
|
1115
1125
|
}
|
|
1116
1126
|
|
|
1117
1127
|
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
1118
1128
|
const left = edge === "top" ? "┌" : "└";
|
|
1119
1129
|
const right = edge === "top" ? "┐" : "┘";
|
|
1120
|
-
return this.theme.fg("
|
|
1130
|
+
return this.theme.fg("border", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
1121
1131
|
}
|
|
1122
1132
|
|
|
1123
1133
|
private wrapTranscript(innerWidth: number): string[] {
|
|
@@ -1137,22 +1147,53 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1137
1147
|
return Math.max(18, Math.min(32, Math.floor(terminalRows * 0.78)));
|
|
1138
1148
|
}
|
|
1139
1149
|
|
|
1150
|
+
private scrollTranscript(delta: number): void {
|
|
1151
|
+
if (delta < 0) {
|
|
1152
|
+
this.followTranscript = false;
|
|
1153
|
+
}
|
|
1154
|
+
this.transcriptScrollOffset = Math.max(0, this.transcriptScrollOffset + delta);
|
|
1155
|
+
this.tui.requestRender();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
dispose(): void {
|
|
1159
|
+
this.tui.terminal?.write?.("\x1b[?1000l\x1b[?1006l");
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
private getMouseScrollDelta(data: string): number | null {
|
|
1163
|
+
const match = data.match(/^\x1b\[<(\d+);\d+;\d+[Mm]$/);
|
|
1164
|
+
if (!match) {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const button = Number(match[1]);
|
|
1169
|
+
if ((button & 64) !== 64) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return (button & 1) === 0 ? -3 : 3;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1140
1176
|
handleInput(data: string): void {
|
|
1141
1177
|
if (matchesBtwFocusShortcut(data)) {
|
|
1142
1178
|
this.onUnfocusCallback();
|
|
1143
1179
|
return;
|
|
1144
1180
|
}
|
|
1145
1181
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
this.
|
|
1149
|
-
|
|
1182
|
+
const mouseScrollDelta = this.getMouseScrollDelta(data);
|
|
1183
|
+
if (mouseScrollDelta !== null) {
|
|
1184
|
+
this.scrollTranscript(mouseScrollDelta);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.up)) {
|
|
1189
|
+
const step = matchesKey(data, Key.pageUp) ? Math.max(1, this.transcriptViewportHeight - 1) : 1;
|
|
1190
|
+
this.scrollTranscript(-step);
|
|
1150
1191
|
return;
|
|
1151
1192
|
}
|
|
1152
1193
|
|
|
1153
|
-
if (matchesKey(data, Key.pageDown)) {
|
|
1154
|
-
|
|
1155
|
-
this.
|
|
1194
|
+
if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.down)) {
|
|
1195
|
+
const step = matchesKey(data, Key.pageDown) ? Math.max(1, this.transcriptViewportHeight - 1) : 1;
|
|
1196
|
+
this.scrollTranscript(step);
|
|
1156
1197
|
return;
|
|
1157
1198
|
}
|
|
1158
1199
|
|
|
@@ -1168,19 +1209,25 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1168
1209
|
// the row stays geometrically stable while the overlay still owns keyboard input.
|
|
1169
1210
|
this.input.focused = false;
|
|
1170
1211
|
try {
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1212
|
+
const renderedInputLine = this.input.render(targetWidth)[0] ?? "";
|
|
1213
|
+
const inputLine = truncateToWidth(renderedInputLine, targetWidth, "");
|
|
1214
|
+
const padding = Math.max(0, targetWidth - visibleWidth(inputLine));
|
|
1215
|
+
return `${this.theme.fg("border", "│")}${inputLine}${" ".repeat(padding)}${this.theme.fg("border", "│")}`;
|
|
1173
1216
|
} finally {
|
|
1174
1217
|
this.input.focused = previousFocused;
|
|
1175
1218
|
}
|
|
1176
1219
|
}
|
|
1177
1220
|
|
|
1221
|
+
private fitRenderedLine(line: string, width: number): string {
|
|
1222
|
+
return visibleWidth(line) > width ? truncateToWidth(line, width, "") : line;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1178
1225
|
override render(width: number): string[] {
|
|
1179
1226
|
const dialogWidth = Math.max(24, width);
|
|
1180
1227
|
const innerWidth = Math.max(22, dialogWidth - 2);
|
|
1181
1228
|
const transcriptLines = this.wrapTranscript(innerWidth);
|
|
1182
1229
|
const dialogHeight = this.getDialogHeight();
|
|
1183
|
-
const chromeHeight =
|
|
1230
|
+
const chromeHeight = BTW_OVERLAY_CHROME_LINES;
|
|
1184
1231
|
const transcriptHeight = Math.max(6, dialogHeight - chromeHeight);
|
|
1185
1232
|
this.transcriptViewportHeight = transcriptHeight;
|
|
1186
1233
|
|
|
@@ -1225,7 +1272,7 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1225
1272
|
lines.push(this.frameLine(this.theme.fg("dim", this.hintsTextValue.trim()), innerWidth));
|
|
1226
1273
|
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
1227
1274
|
|
|
1228
|
-
return lines;
|
|
1275
|
+
return lines.map((line) => this.fitRenderedLine(line, width));
|
|
1229
1276
|
}
|
|
1230
1277
|
|
|
1231
1278
|
setDraft(value: string): void {
|
|
@@ -1259,7 +1306,7 @@ class BtwOverlayComponent extends Container implements Focusable {
|
|
|
1259
1306
|
const status = this.getStatus() ?? "Ready. Enter submits; Escape dismisses without clearing.";
|
|
1260
1307
|
this.statusTextValue = status;
|
|
1261
1308
|
this.statusText.setText(this.statusTextValue);
|
|
1262
|
-
this.hintsTextValue = "
|
|
1309
|
+
this.hintsTextValue = "Scroll wheel ↑↓ PgUp/PgDn · Enter · Alt+/ focus · Esc";
|
|
1263
1310
|
this.hintsText.setText(this.hintsTextValue);
|
|
1264
1311
|
this.tui.requestRender();
|
|
1265
1312
|
}
|
|
@@ -1555,7 +1602,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1555
1602
|
model: settings.model,
|
|
1556
1603
|
modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"],
|
|
1557
1604
|
thinkingLevel: settings.thinkingLevel,
|
|
1558
|
-
|
|
1605
|
+
// Match pi's default coding-agent toolset (read/bash/edit/write).
|
|
1606
|
+
tools: ["read", "bash", "edit", "write"],
|
|
1559
1607
|
resourceLoader: createBtwResourceLoader(ctx),
|
|
1560
1608
|
});
|
|
1561
1609
|
|
|
@@ -1650,6 +1698,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1650
1698
|
};
|
|
1651
1699
|
runtime.close = () => {
|
|
1652
1700
|
overlayDraft = overlay.getDraft();
|
|
1701
|
+
overlay.dispose();
|
|
1653
1702
|
closeRuntime();
|
|
1654
1703
|
};
|
|
1655
1704
|
|
|
@@ -1760,7 +1809,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1760
1809
|
return true;
|
|
1761
1810
|
}
|
|
1762
1811
|
|
|
1763
|
-
|
|
1812
|
+
if (parsed.action === "clear") {
|
|
1813
|
+
await setBtwModelOverride(ctx, null);
|
|
1814
|
+
return true;
|
|
1815
|
+
}
|
|
1816
|
+
const ref = parsed.model;
|
|
1817
|
+
const resolved = ctx.modelRegistry.find(ref.provider, ref.id);
|
|
1818
|
+
if (!resolved) {
|
|
1819
|
+
const message = `Unknown model ${ref.provider}/${ref.id}. Use /login or /models to add it before setting it as the BTW override.`;
|
|
1820
|
+
setOverlayStatus(message, ctx);
|
|
1821
|
+
notify(ctx, message, "error");
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
await setBtwModelOverride(ctx, resolved);
|
|
1764
1825
|
return true;
|
|
1765
1826
|
}
|
|
1766
1827
|
|
|
@@ -1911,17 +1972,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
1911
1972
|
|
|
1912
1973
|
for (let i = 0; i < branch.length; i++) {
|
|
1913
1974
|
if (isCustomEntry(branch[i], BTW_MODEL_OVERRIDE_TYPE)) {
|
|
1914
|
-
const details = branch[i]
|
|
1915
|
-
|
|
1916
|
-
details
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1975
|
+
const details = (branch[i] as unknown as { data?: BtwModelOverrideDetails }).data;
|
|
1976
|
+
if (details?.action === "set") {
|
|
1977
|
+
const resolved = ctx.modelRegistry.find(details.provider, details.id);
|
|
1978
|
+
if (resolved) {
|
|
1979
|
+
btwModelOverride = resolved;
|
|
1980
|
+
} else {
|
|
1981
|
+
// Configured override is no longer in the registry; drop it on restore.
|
|
1982
|
+
btwModelOverride = null;
|
|
1983
|
+
}
|
|
1984
|
+
} else if (details?.action === "clear") {
|
|
1985
|
+
btwModelOverride = null;
|
|
1986
|
+
}
|
|
1921
1987
|
}
|
|
1922
1988
|
|
|
1923
1989
|
if (isCustomEntry(branch[i], BTW_THINKING_OVERRIDE_TYPE)) {
|
|
1924
|
-
const details = branch[i]
|
|
1990
|
+
const details = (branch[i] as unknown as { data?: BtwThinkingOverrideDetails }).data;
|
|
1925
1991
|
btwThinkingOverride =
|
|
1926
1992
|
details?.action === "set"
|
|
1927
1993
|
? details.thinkingLevel
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-btw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "A pi extension for parallel side conversations with /btw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"image": "https://raw.githubusercontent.com/dbachelder/pi-btw/main/docs/btw-overlay.png"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
47
|
-
"@
|
|
45
|
+
"@earendil-works/pi-ai": ">=0.74.0 <1",
|
|
46
|
+
"@earendil-works/pi-coding-agent": ">=0.74.0 <1",
|
|
47
|
+
"@earendil-works/pi-tui": ">=0.74.0 <1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"typescript": "^6.0.2",
|