@narumitw/pi-btw 0.56.2 → 0.57.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 +20 -139
- package/dist/index.ts +150 -13
- package/dist/index.ts.map +3 -3
- package/docs/workflows.md +73 -0
- package/package.json +5 -4
- package/src/btw.ts +8 -0
- package/src/fullscreen-ui.ts +126 -1
- package/src/side-thread.ts +57 -5
- package/src/text.ts +18 -0
- package/src/transcript-pager.ts +5 -16
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Only context you explicitly bring back is loaded into the main editor.
|
|
|
9
9
|
|
|
10
10
|
- Starts a side thread immediately with `/btw <question>` or opens the manager with `/btw`.
|
|
11
11
|
- Uses any persisted main-session branch as context without switching branches.
|
|
12
|
-
- Supports scrollable answers, transcript search, follow-up questions, queued steering, and in-memory resume.
|
|
12
|
+
- Supports scrollable answers, transcript search, a clickable jump-to-latest control, follow-up questions, queued steering, and in-memory resume.
|
|
13
13
|
- Keeps side questions and answers out of the main conversation by default.
|
|
14
14
|
- Brings back the latest answer, a question suffix, an exact range, or the complete thread only when requested.
|
|
15
15
|
- Uses Pi's current model and thinking level or saved pi-btw choices.
|
|
@@ -43,114 +43,16 @@ The side thread stays separate until you explicitly bring context to the main ed
|
|
|
43
43
|
|
|
44
44
|
## 💬 Commands
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
| Command | Purpose |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `/btw` | Choose context, start or resume a side thread, or change settings. |
|
|
49
|
+
| `/btw <question>` | Start a new side thread immediately with the supplied question. |
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Examples:
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
/btw
|
|
58
|
-
/btw what does this TypeScript error mean?
|
|
59
|
-
/btw summarize the current implementation before we continue
|
|
60
|
-
/btw is this API name idiomatic?
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Choose context or resume a thread
|
|
64
|
-
|
|
65
|
-
Running `/btw` opens a manager with **Start side thread** selected first.
|
|
66
|
-
**Start from main thread tree…** opens Pi's session tree and uses the root-to-selected-entry path, including the selected entry, as context.
|
|
67
|
-
This choice preserves the main editor draft and does not navigate, fork, append to, or switch the main conversation.
|
|
68
|
-
The tree is a snapshot of persisted entries, and the side thread keeps immutable context even if the main conversation later changes.
|
|
69
|
-
Escape returns to the manager, and Ctrl+C closes the flow.
|
|
70
|
-
Native tree copying reports success or failure.
|
|
71
|
-
An explicit `Shift+L` label edit is the only main-session mutation available from this selector and persists through Pi.
|
|
72
|
-
|
|
73
|
-
When non-empty threads exist in memory, **Resume side thread** opens a bounded searchable list.
|
|
74
|
-
Search matches the first question and question count while retaining each raw thread ID.
|
|
75
|
-
Each row uses the first question as its fixed title and shows the question count.
|
|
76
|
-
Rows are ordered by the newest recorded answer or visible error; opening and closing without a new result does not reorder them.
|
|
77
|
-
**Settings** controls the starting thinking level, fixed-level shortcut memory, and automatic selection copying.
|
|
78
|
-
`/btw <question>` bypasses the manager and always starts a new thread.
|
|
79
|
-
|
|
80
|
-
### Use the side-thread workspace
|
|
81
|
-
|
|
82
|
-
The side thread uses a dedicated full-screen terminal view with answers above the editor.
|
|
83
|
-
The main agent can continue running, but main-screen rendering stays suspended until `/btw` closes so new output cannot move a mouse selection.
|
|
84
|
-
Returning to Pi redraws everything produced while the main view was hidden.
|
|
85
|
-
|
|
86
|
-
A fixed `btw · side thread` header identifies the workspace while scrolling.
|
|
87
|
-
Messages use Pi's normal user and assistant presentation without turn numbers or role labels.
|
|
88
|
-
Type a question and press Enter for each turn.
|
|
89
|
-
Previous successful questions and answers remain visible and available to the side model.
|
|
90
|
-
|
|
91
|
-
Drag the primary mouse button across transcript text to select it.
|
|
92
|
-
Automatic selection copying is on by default and immediately requests a copy through Pi's host clipboard helper.
|
|
93
|
-
When **Copy selection automatically** is off, the selection stays highlighted and Pi's effective `app.message.copy` binding copies it.
|
|
94
|
-
Manual mode requires Pi's current fullscreen selection APIs.
|
|
95
|
-
If those APIs are unavailable, pi-btw restores the main TUI and asks you to update Pi or re-enable automatic copying.
|
|
96
|
-
The view reports `No selection to copy` when that binding is used without an active selection.
|
|
97
|
-
It reports `Copied!` when Pi accepts a clipboard request and `Copy failed` when Pi rejects it.
|
|
98
|
-
Actual clipboard access still depends on the operating system and terminal.
|
|
99
|
-
Ctrl+C always cancels the side flow, even when `app.message.copy` is also mapped to Ctrl+C.
|
|
100
|
-
|
|
101
|
-
Press `Ctrl+Shift+F` to search completed or in-progress transcript content.
|
|
102
|
-
Press Enter or `Ctrl+G` for the next match, `Shift+Enter` or `Ctrl+Shift+G` for the previous match, and Escape to close search.
|
|
103
|
-
Search uses Pi's active theme, excludes fixed header and footer text, and returns focus to the composer when closed.
|
|
104
|
-
|
|
105
|
-
### Change thinking level or queue steering
|
|
106
|
-
|
|
107
|
-
The header shows the side thread's current thinking level.
|
|
108
|
-
In the composer, use Pi's configured `app.thinking.cycle` shortcut (`Shift+Tab` by default) to cycle levels supported by the side model.
|
|
109
|
-
Later questions use the displayed level until it changes again.
|
|
110
|
-
For a fixed starting level, shortcut changes are saved to `pi-btw.json` by default.
|
|
111
|
-
Turn **Remember thinking level changes** off to keep them local to the thread.
|
|
112
|
-
With **Same as main thread**, shortcut changes are always local.
|
|
113
|
-
Neither setting changes the main session's thinking level.
|
|
114
|
-
|
|
115
|
-
During a response, the transcript and composer remain visible above `Answering…`.
|
|
116
|
-
Submit another question to queue it as `Steering`.
|
|
117
|
-
Queued questions appear in submission order and run one at a time after the active response.
|
|
118
|
-
Each queued question uses the side thread's thinking level when its turn starts.
|
|
119
|
-
A failed response remains visible and does not discard later queued questions.
|
|
120
|
-
Steering never appends to the main conversation or editor.
|
|
121
|
-
|
|
122
|
-
Use the mouse wheel, trackpad, or `PgUp`/`PgDn` to scroll transcript history.
|
|
123
|
-
The footer shows history keys only when scrolling is available.
|
|
124
|
-
Ctrl+C cancels the active response and discards the current draft and steering queue.
|
|
125
|
-
Completed questions, answers, and visible errors remain available through Resume until the extension instance ends.
|
|
126
|
-
|
|
127
|
-
### Bring context to the main editor
|
|
128
|
-
|
|
129
|
-
After a successful answer, press `Ctrl+R` to choose context for the main editor.
|
|
130
|
-
The scope menu shows the size of the latest question and answer and the full thread.
|
|
131
|
-
Choose the latest question and answer, everything from one question onward, an exact range, or the full thread.
|
|
132
|
-
Question-suffix, exact-range, and full-thread choices preview the editable context block before the side thread closes.
|
|
133
|
-
Escape returns, while Ctrl+C closes without bringing context back.
|
|
134
|
-
|
|
135
|
-
The exact-range selector supports whole-line and editor-style character selection.
|
|
136
|
-
It reports selected line, message, and approximate token counts.
|
|
137
|
-
Press Space to select the current raw source line, use Up or Down to extend by lines, and press Space again to clear.
|
|
138
|
-
Alternatively, move with arrow keys and extend a character selection with Shift plus an arrow key.
|
|
139
|
-
Starting a Shift selection replaces an active line selection.
|
|
140
|
-
Selected lines show a `●` marker as well as highlighting.
|
|
141
|
-
Pi's configured keys control vertical navigation, bringing, and going back, with Up, Down, Enter, and Escape as defaults.
|
|
142
|
-
Selection follows raw source text rather than terminal-wrapped rows.
|
|
143
|
-
|
|
144
|
-
Bringing context closes the side thread and loads a deterministic, editable block into Pi's main editor without sending it.
|
|
145
|
-
If a draft already exists, append is the recommended default.
|
|
146
|
-
Replace is marked destructive and requires a second confirmation.
|
|
147
|
-
Cancel returns to the side thread without changing either draft, and concurrent editor updates are preserved.
|
|
148
|
-
A success message reports whether context was loaded, appended, or replaced and gives its approximate size.
|
|
149
|
-
|
|
150
|
-
Without an explicit bring action, closing `/btw` never changes the main conversation.
|
|
151
|
-
Non-empty threads remain in memory only for Resume during the current extension instance.
|
|
152
|
-
`/new`, Pi `/resume`, `/reload`, extension replacement, and process restart discard retained threads.
|
|
153
|
-
Unsent drafts, steering queues, interrupted answers, and model credentials are not retained.
|
|
51
|
+
Both routes require TUI mode and a model with usable credentials; see [Settings](#-settings).
|
|
52
|
+
Side questions and selected conversation context are sent to that model's provider.
|
|
53
|
+
Bringing context back fills the main editor without submitting; replacing an existing draft requires confirmation.
|
|
54
|
+
Ctrl+C cancels the response and discards the current draft and queued questions, but completed exchanges remain resumable in memory.
|
|
55
|
+
Read the [workflow guide](./docs/workflows.md) for context selection, copying, search, steering, and draft recovery; `/new`, `/resume`, `/reload`, and restart discard retained threads.
|
|
154
56
|
|
|
155
57
|
## ⚙️ Settings
|
|
156
58
|
|
|
@@ -216,43 +118,22 @@ The file is read for every `/btw` invocation, so edits apply without `/reload`.
|
|
|
216
118
|
- Resume state is memory-only and lasts only for the current extension instance.
|
|
217
119
|
- A side thread retains the latest 40,000 characters of main-conversation context and adds a truncation notice when earlier content is omitted.
|
|
218
120
|
- Clipboard access depends on Pi's host helper, the operating system, and the terminal.
|
|
121
|
+
- Pi versions before 0.85 omit the clickable jump-to-latest control.
|
|
219
122
|
|
|
220
123
|
## 🗂️ Package layout
|
|
221
124
|
|
|
222
|
-
```
|
|
125
|
+
```text
|
|
223
126
|
packages/pi-btw/
|
|
224
|
-
├──
|
|
225
|
-
├──
|
|
226
|
-
│ └──
|
|
227
|
-
├──
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
│ ├── fullscreen-ui.ts
|
|
232
|
-
│ ├── main-tree-picker.ts
|
|
233
|
-
│ ├── menu.ts
|
|
234
|
-
│ ├── settings.ts
|
|
235
|
-
│ ├── side-thread.ts
|
|
236
|
-
│ ├── text.ts
|
|
237
|
-
│ └── transcript-pager.ts
|
|
238
|
-
├── test/
|
|
239
|
-
├── README.md
|
|
240
|
-
├── LICENSE
|
|
241
|
-
├── tsconfig.json
|
|
242
|
-
└── package.json
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
The package exposes its Pi extension through `package.json`:
|
|
246
|
-
|
|
247
|
-
```json
|
|
248
|
-
{
|
|
249
|
-
"pi": {
|
|
250
|
-
"extensions": ["./dist/index.ts"]
|
|
251
|
-
}
|
|
252
|
-
}
|
|
127
|
+
├── src/ # Authoritative implementation and helpers
|
|
128
|
+
│ ├── index.ts # Thin Pi entrypoint
|
|
129
|
+
│ └── btw.ts # Side-thread lifecycle and command
|
|
130
|
+
├── dist/ # Generated Jiti runtime
|
|
131
|
+
├── docs/ # Side-thread workflows and controls
|
|
132
|
+
├── scripts/build-runtime.mjs # Runtime builder
|
|
133
|
+
└── test/ # Behavior and lifecycle coverage
|
|
253
134
|
```
|
|
254
135
|
|
|
255
|
-
The generated runtime is built from
|
|
136
|
+
The generated runtime is built from `src/index.ts` and does not import back into `src`.
|
|
256
137
|
|
|
257
138
|
## 🔎 Keywords
|
|
258
139
|
|
package/dist/index.ts
CHANGED
|
@@ -482,8 +482,10 @@ import {
|
|
|
482
482
|
} from "@earendil-works/pi-coding-agent";
|
|
483
483
|
import {
|
|
484
484
|
isKeyRelease,
|
|
485
|
+
isKittyProtocolActive,
|
|
485
486
|
Key as Key2,
|
|
486
487
|
matchesKey as matchesKey2,
|
|
488
|
+
parseKey,
|
|
487
489
|
TuiAltScreen,
|
|
488
490
|
truncateToWidth as truncateToWidth2
|
|
489
491
|
} from "@earendil-works/pi-tui";
|
|
@@ -495,6 +497,18 @@ function sanitizeSingleLine(text) {
|
|
|
495
497
|
return code > 31 && (code < 127 || code > 159);
|
|
496
498
|
}).join("").replace(/ +/gu, " ").trim();
|
|
497
499
|
}
|
|
500
|
+
function formatKeyLabel2(key) {
|
|
501
|
+
const sanitized = sanitizeSingleLine(key);
|
|
502
|
+
if (!sanitized) return "";
|
|
503
|
+
return sanitized.split("+").map((part) => {
|
|
504
|
+
const lower = part.toLowerCase();
|
|
505
|
+
if (lower === "shift") return "Shift";
|
|
506
|
+
if (lower === "ctrl") return "Ctrl";
|
|
507
|
+
if (lower === "alt") return "Alt";
|
|
508
|
+
if (lower === "super") return "Super";
|
|
509
|
+
return part.length === 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`;
|
|
510
|
+
}).join("+");
|
|
511
|
+
}
|
|
498
512
|
|
|
499
513
|
// src/fullscreen-ui.ts
|
|
500
514
|
var FullscreenUiDisposedError = class extends Error {
|
|
@@ -607,6 +621,88 @@ var BtwTuiAltScreen = class extends TuiAltScreen {
|
|
|
607
621
|
};
|
|
608
622
|
var BRACKETED_PASTE_START = "\x1B[200~";
|
|
609
623
|
var BRACKETED_PASTE_END = "\x1B[201~";
|
|
624
|
+
var ALT_SCREEN_ACTIONS_BEFORE_BOTTOM = [
|
|
625
|
+
"tui.altScreen.search",
|
|
626
|
+
"tui.altScreen.searchNext",
|
|
627
|
+
"tui.altScreen.searchPrevious",
|
|
628
|
+
"tui.altScreen.searchClose",
|
|
629
|
+
"tui.altScreen.pageUp",
|
|
630
|
+
"tui.altScreen.pageDown",
|
|
631
|
+
"tui.altScreen.halfPageUp",
|
|
632
|
+
"tui.altScreen.halfPageDown",
|
|
633
|
+
"tui.altScreen.lineUp",
|
|
634
|
+
"tui.altScreen.lineDown",
|
|
635
|
+
"tui.altScreen.previousPrompt",
|
|
636
|
+
"tui.altScreen.nextPrompt",
|
|
637
|
+
"tui.altScreen.top"
|
|
638
|
+
];
|
|
639
|
+
var KEY_MODIFIER_ORDER = ["shift", "ctrl", "alt", "super"];
|
|
640
|
+
var MATCHABLE_SPECIAL_KEYS = /* @__PURE__ */ new Set([
|
|
641
|
+
"space",
|
|
642
|
+
"tab",
|
|
643
|
+
"enter",
|
|
644
|
+
"backspace",
|
|
645
|
+
"delete",
|
|
646
|
+
"insert",
|
|
647
|
+
"home",
|
|
648
|
+
"end",
|
|
649
|
+
"pageup",
|
|
650
|
+
"pagedown",
|
|
651
|
+
"up",
|
|
652
|
+
"down",
|
|
653
|
+
"left",
|
|
654
|
+
"right"
|
|
655
|
+
]);
|
|
656
|
+
var MATCHABLE_SYMBOL_KEYS = new Set("`-=[]\\;',./!@#$%^&*()_+|~{}:<>?");
|
|
657
|
+
function normalizedKeyId(key) {
|
|
658
|
+
const parts = key.toLowerCase().split("+");
|
|
659
|
+
const base = parts.at(-1);
|
|
660
|
+
if (!base) return "";
|
|
661
|
+
const normalizedBase = base === "esc" ? "escape" : base === "return" ? "enter" : base;
|
|
662
|
+
const modifiers = KEY_MODIFIER_ORDER.filter((modifier) => parts.includes(modifier));
|
|
663
|
+
return [...modifiers, normalizedBase].join("+");
|
|
664
|
+
}
|
|
665
|
+
function formatEffectiveKeyLabel(key) {
|
|
666
|
+
const parts = key.split("+");
|
|
667
|
+
const base = parts.at(-1);
|
|
668
|
+
if (base === "pageup") parts[parts.length - 1] = "pageUp";
|
|
669
|
+
if (base === "pagedown") parts[parts.length - 1] = "pageDown";
|
|
670
|
+
return formatKeyLabel2(parts.join("+"));
|
|
671
|
+
}
|
|
672
|
+
function canMatchKeyInput(key) {
|
|
673
|
+
const parts = key.split("+");
|
|
674
|
+
const base = parts.at(-1) ?? "";
|
|
675
|
+
const modifiers = parts.slice(0, -1);
|
|
676
|
+
if (base === "escape") return modifiers.length === 0;
|
|
677
|
+
if (base === "clear") {
|
|
678
|
+
return modifiers.length === 0 || modifiers.length === 1 && (modifiers[0] === "shift" || modifiers[0] === "ctrl");
|
|
679
|
+
}
|
|
680
|
+
if (/^f(?:[1-9]|1[0-2])$/u.test(base)) return modifiers.length === 0;
|
|
681
|
+
return MATCHABLE_SPECIAL_KEYS.has(base) || base.length === 1 && (/^[a-z0-9]$/u.test(base) || MATCHABLE_SYMBOL_KEYS.has(base));
|
|
682
|
+
}
|
|
683
|
+
function rawCtrlInput(base) {
|
|
684
|
+
if (base.length !== 1) return void 0;
|
|
685
|
+
const rawBase = base === "-" ? "_" : base;
|
|
686
|
+
if (!"abcdefghijklmnopqrstuvwxyz[\\]_".includes(rawBase)) return void 0;
|
|
687
|
+
return String.fromCharCode(rawBase.charCodeAt(0) & 31);
|
|
688
|
+
}
|
|
689
|
+
function legacyRawInput(key) {
|
|
690
|
+
const parts = key.split("+");
|
|
691
|
+
const base = parts.at(-1) ?? "";
|
|
692
|
+
if (parts.length === 2 && parts[0] === "ctrl") return rawCtrlInput(base);
|
|
693
|
+
if (isKittyProtocolActive()) return void 0;
|
|
694
|
+
if (parts.length === 2 && parts[0] === "alt" && base.length === 1) return `\x1B${base}`;
|
|
695
|
+
if (parts.length === 3 && parts[0] === "ctrl" && parts[1] === "alt") {
|
|
696
|
+
const input = rawCtrlInput(base);
|
|
697
|
+
return input ? `\x1B${input}` : void 0;
|
|
698
|
+
}
|
|
699
|
+
return void 0;
|
|
700
|
+
}
|
|
701
|
+
function keyInputIdentity(key) {
|
|
702
|
+
const identity = normalizedKeyId(key);
|
|
703
|
+
const input = legacyRawInput(identity);
|
|
704
|
+
return input ? normalizedKeyId(parseKey(input) ?? identity) : identity;
|
|
705
|
+
}
|
|
610
706
|
function hasManualSelectionCopyApi() {
|
|
611
707
|
return typeof TuiAltScreen.prototype.hasActiveSelection === "function" && typeof TuiAltScreen.prototype.copyActiveSelectionToClipboard === "function";
|
|
612
708
|
}
|
|
@@ -625,6 +721,25 @@ function createBtwFullscreenTui(parent, theme, keybindings, copyOnSelect, manual
|
|
|
625
721
|
mouse: true,
|
|
626
722
|
copyOnSelect,
|
|
627
723
|
searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
|
|
724
|
+
scrollToEndIndicator: () => {
|
|
725
|
+
const unavailableKeyIdentities = /* @__PURE__ */ new Set([keyInputIdentity(Key2.ctrl("c"))]);
|
|
726
|
+
for (const action of ALT_SCREEN_ACTIONS_BEFORE_BOTTOM) {
|
|
727
|
+
for (const actionKey of keybindings.getKeys(action)) {
|
|
728
|
+
unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (!copyOnSelect) {
|
|
732
|
+
for (const copyKey of keybindings.getKeys("app.message.copy")) {
|
|
733
|
+
unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const key = keybindings.getKeys("tui.altScreen.bottom").map((candidate) => keyInputIdentity(String(candidate))).find(
|
|
737
|
+
(identity) => identity && canMatchKeyInput(identity) && !unavailableKeyIdentities.has(identity) && formatEffectiveKeyLabel(identity)
|
|
738
|
+
);
|
|
739
|
+
const label = theme.fg("text", " \u2193 Jump to latest message");
|
|
740
|
+
const shortcut = key ? theme.fg("muted", ` \xB7 ${formatEffectiveKeyLabel(key)}`) : "";
|
|
741
|
+
return theme.bg("selectedBg", `${label}${shortcut} `);
|
|
742
|
+
},
|
|
628
743
|
searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
|
|
629
744
|
openUrl,
|
|
630
745
|
copySelection: async (text) => {
|
|
@@ -1010,14 +1125,15 @@ async function completeSideThreadTurn({
|
|
|
1010
1125
|
thinkingLevel,
|
|
1011
1126
|
auth,
|
|
1012
1127
|
signal,
|
|
1013
|
-
completeSimple
|
|
1128
|
+
completeSimple,
|
|
1129
|
+
sessionId
|
|
1014
1130
|
}) {
|
|
1015
1131
|
if (signal?.aborted) return { kind: "aborted" };
|
|
1016
1132
|
try {
|
|
1017
1133
|
const response = await completeSimple(
|
|
1018
1134
|
model,
|
|
1019
1135
|
{ systemPrompt: SYSTEM_PROMPT, messages: buildSideThreadMessages(thread, question) },
|
|
1020
|
-
buildStreamOptions(auth, thinkingLevel, signal)
|
|
1136
|
+
buildStreamOptions(auth, { thinkingLevel, signal, model, sessionId })
|
|
1021
1137
|
);
|
|
1022
1138
|
if (signal?.aborted || response?.stopReason === "aborted") return { kind: "aborted" };
|
|
1023
1139
|
if (!isAssistantMessage(response)) {
|
|
@@ -1076,10 +1192,31 @@ function createUserMessage(text) {
|
|
|
1076
1192
|
timestamp: Date.now()
|
|
1077
1193
|
};
|
|
1078
1194
|
}
|
|
1079
|
-
|
|
1195
|
+
var OPENCODE_HOST = "opencode.ai";
|
|
1196
|
+
function matchesOpencodeHost(baseUrl) {
|
|
1197
|
+
if (!baseUrl) return false;
|
|
1198
|
+
try {
|
|
1199
|
+
return new URL(baseUrl).hostname === OPENCODE_HOST;
|
|
1200
|
+
} catch {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function getOpencodeSessionHeaders(model, sessionId) {
|
|
1205
|
+
if (!sessionId) return void 0;
|
|
1206
|
+
if (model.provider !== "opencode" && model.provider !== "opencode-go" && !matchesOpencodeHost(model.baseUrl)) {
|
|
1207
|
+
return void 0;
|
|
1208
|
+
}
|
|
1209
|
+
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
|
1210
|
+
}
|
|
1211
|
+
function mergeSessionHeaders(authHeaders, sessionHeaders) {
|
|
1212
|
+
if (!sessionHeaders && !authHeaders) return void 0;
|
|
1213
|
+
return { ...sessionHeaders, ...authHeaders };
|
|
1214
|
+
}
|
|
1215
|
+
function buildStreamOptions(auth, { thinkingLevel, signal, model, sessionId }) {
|
|
1216
|
+
const sessionHeaders = model ? getOpencodeSessionHeaders(model, sessionId) : void 0;
|
|
1080
1217
|
const options = {
|
|
1081
1218
|
apiKey: auth.apiKey,
|
|
1082
|
-
headers: auth.headers,
|
|
1219
|
+
headers: mergeSessionHeaders(auth.headers, sessionHeaders),
|
|
1083
1220
|
env: auth.env,
|
|
1084
1221
|
signal
|
|
1085
1222
|
};
|
|
@@ -2376,14 +2513,7 @@ function renderSideThreadHeader(width, theme, thinkingLevel) {
|
|
|
2376
2513
|
return theme.fg("muted", `${title}${"\u2500".repeat(ruleWidth)}`);
|
|
2377
2514
|
}
|
|
2378
2515
|
function thinkingKeyLabel(keybindings) {
|
|
2379
|
-
|
|
2380
|
-
return key.split("+").map((part) => {
|
|
2381
|
-
const lower = part.toLowerCase();
|
|
2382
|
-
if (lower === "shift") return "Shift";
|
|
2383
|
-
if (lower === "ctrl") return "Ctrl";
|
|
2384
|
-
if (lower === "alt") return "Alt";
|
|
2385
|
-
return part.length === 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`;
|
|
2386
|
-
}).join("+");
|
|
2516
|
+
return formatKeyLabel2(String(keybindings.getKeys("app.thinking.cycle")[0] ?? "shift+tab")) || "Shift+Tab";
|
|
2387
2517
|
}
|
|
2388
2518
|
function fitComposerLayout(header, contentLines, footer, editorLines, availableRows, statusLines = []) {
|
|
2389
2519
|
const lines = [header, ...contentLines, ...statusLines, footer, ...editorLines];
|
|
@@ -2507,6 +2637,12 @@ function providerHeadersHaveValue(headers) {
|
|
|
2507
2637
|
function formatError5(error) {
|
|
2508
2638
|
return error instanceof Error ? error.message : String(error);
|
|
2509
2639
|
}
|
|
2640
|
+
function readBtwSessionId(ctx) {
|
|
2641
|
+
const getSessionId = ctx.sessionManager.getSessionId;
|
|
2642
|
+
if (typeof getSessionId !== "function") return void 0;
|
|
2643
|
+
const sessionId = getSessionId.call(ctx.sessionManager);
|
|
2644
|
+
return sessionId.length > 0 ? sessionId : void 0;
|
|
2645
|
+
}
|
|
2510
2646
|
function notifySafely3(ctx, message, level) {
|
|
2511
2647
|
try {
|
|
2512
2648
|
ctx.ui.notify(sanitizeSingleLine(message), level);
|
|
@@ -3041,7 +3177,8 @@ async function askThreadQuestion(thread, question, selected, thinkingLevel, ctx,
|
|
|
3041
3177
|
thinkingLevel,
|
|
3042
3178
|
auth: selected.auth,
|
|
3043
3179
|
signal: view.signal,
|
|
3044
|
-
completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry)
|
|
3180
|
+
completeSimple: createModelRegistryCompleteSimple(ctx.modelRegistry),
|
|
3181
|
+
sessionId: readBtwSessionId(ctx)
|
|
3045
3182
|
}).then((result) => {
|
|
3046
3183
|
if (settled) return;
|
|
3047
3184
|
settled = true;
|