pi-btw-cc 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/docs/btw-browse.png +0 -0
- package/docs/btw-browse.svg +24 -0
- package/docs/btw-overlay.png +0 -0
- package/docs/btw-overlay.svg +24 -0
- package/docs/btw-promote.png +0 -0
- package/docs/btw-promote.svg +16 -0
- package/package.json +73 -0
- package/src/context.ts +136 -0
- package/src/index.ts +319 -0
- package/src/overlay.ts +623 -0
- package/src/thread.ts +178 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nouvelle-Lune
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# pi-btw-cc
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pi-btw-cc)
|
|
4
|
+
[](https://github.com/Nouvelle-Lune/pi-btw-cc/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
`/btw` side questions for [pi](https://pi.dev): ask about the work in progress without derailing it. The answer lands in a floating overlay instead of the main conversation, and only reaches it if you promote it with `f`.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/btw what does the retry wrapper do on a 429?
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
<img src="docs/btw-overlay.png" alt="The /btw overlay showing a side question, its answer, the side thread and the key hints" width="880">
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:pi-btw-cc
|
|
19
|
+
pi install git:github.com/Nouvelle-Lune/pi-btw-cc # or from GitHub
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Needs Node >= 22.19 and pi 0.85.x.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
| Command | |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `/btw <question>` | Ask a side question — opens the overlay and starts the request right away |
|
|
29
|
+
| `/btw` | Reopen the overlay on the existing side thread (no model call) |
|
|
30
|
+
|
|
31
|
+
| Key | |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `↑` `↓` | Scroll the answer |
|
|
34
|
+
| `⇧←` `⇧→` | Browse the side thread |
|
|
35
|
+
| `c` | Copy the selected answer |
|
|
36
|
+
| `f` | Promote the selected exchange into the main conversation |
|
|
37
|
+
| `x` | Delete the selected side question |
|
|
38
|
+
| `Esc` `Enter` | Close |
|
|
39
|
+
|
|
40
|
+
The selected exchange is the one expanded in the body and the one `⇧←/⇧→`, `c`, `f` and `x` act on. The thread list under it marks the selection with `❯`, and the footer shows the position as `2/3`.
|
|
41
|
+
|
|
42
|
+
<img src="docs/btw-browse.png" alt="Browsing to an older side question: the thread list marks the selection and the footer shows 1/3" width="880">
|
|
43
|
+
|
|
44
|
+
`f` adds the selected exchange to the main conversation, and the main agent answers it on its next turn:
|
|
45
|
+
|
|
46
|
+
<img src="docs/btw-promote.png" alt="The promoted exchange in the main transcript" width="880">
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- A side request sees the session's conversation (compaction-aware) but has **no tools**: it answers from context only, in one response, and cannot read files or run commands.
|
|
51
|
+
- Side questions stay in the session, so the thread continues across restarts, `/reload`, `/resume` and `/tree` branches.
|
|
52
|
+
- Promotion only appends, so the main conversation's cached prompt prefix stays valid. While a side request is still running, `f` waits instead of aborting it.
|
|
53
|
+
- Side requests don't share the main conversation's prompt cache, so their input tokens are billed in full.
|
|
54
|
+
- On narrow terminals the key hints wrap onto more rows and the answer area shrinks to keep them visible.
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install
|
|
60
|
+
npm run typecheck
|
|
61
|
+
npm test
|
|
62
|
+
npm run docs:images # regenerate docs/*.svg + docs/*.png (needs rsvg-convert)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="708" height="419.2" viewBox="0 0 708 419.2" font-family="Menlo, ui-monospace, monospace" font-size="14">
|
|
2
|
+
<rect width="708" height="419.2" rx="10" fill="#18181e"/>
|
|
3
|
+
<rect y="0" width="708" height="34" rx="10" fill="#505050" opacity="0.25"/>
|
|
4
|
+
<circle cx="24" cy="17" r="5" fill="#ff5f57"/><circle cx="42" cy="17" r="5" fill="#febc2e"/><circle cx="60" cy="17" r="5" fill="#28c840"/>
|
|
5
|
+
<text x="80" y="22" fill="#808080" font-size="13">pi — /btw ⇧← browses the side thread (position 1/3)</text>
|
|
6
|
+
<text x="18" y="66.0" fill="#d4d4d4" xml:space="preserve"><tspan>╭─</tspan><tspan fill="#8abeb7"> btw </tspan><tspan fill="#5f87ff">─</tspan><tspan fill="#5f87ff">────────────────────────────────────────────────</tspan><tspan fill="#5f87ff">─</tspan><tspan> </tspan><tspan fill="#666666">opencode/gpt-5.6-sol</tspan><tspan> </tspan><tspan fill="#5f87ff">╮</tspan></text>
|
|
7
|
+
<text x="18" y="85.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#666666">3 side questions</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
8
|
+
<text x="18" y="104.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
9
|
+
<text x="18" y="124.2" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#8abeb7">❯ </tspan><tspan fill="#d4d4d4">what does the retry wrapper do on a 429?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
10
|
+
<text x="18" y="143.6" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
11
|
+
<text x="18" y="163.0" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
12
|
+
<text x="18" y="182.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#8abeb7">withRetry</tspan><tspan> reads </tspan><tspan fill="#8abeb7">Retry-After</tspan><tspan> when the provider sends it and otherwise backs </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
13
|
+
<text x="18" y="201.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> off exponentially from 500 ms with jitter, up to four attempts. It only </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
14
|
+
<text x="18" y="221.2" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> retries status 429/5xx and network timeouts; a 400 is rethrown immediately. </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
15
|
+
<text x="18" y="240.6" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
16
|
+
<text x="18" y="260.0" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
17
|
+
<text x="18" y="279.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#505050">────────────────────────────────────────────────────────────────────────────</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
18
|
+
<text x="18" y="298.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
19
|
+
<text x="18" y="318.2" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#8abeb7">❯</tspan><tspan> </tspan><tspan fill="#808080">what does the retry wrapper do on a 429?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
20
|
+
<text x="18" y="337.6" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#808080">·</tspan><tspan> </tspan><tspan fill="#808080">why is the cache keyed by branch id instead of session id?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
21
|
+
<text x="18" y="357.0" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#808080">·</tspan><tspan> </tspan><tspan fill="#808080">did the compaction threshold change in this branch?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
22
|
+
<text x="18" y="376.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#666666">↑/↓ to scroll · ⇧←/→ to browse · c to copy · f to fork · Esc to close 1/3</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
23
|
+
<text x="18" y="395.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">╰──────────────────────────────────────────────────────────────────────────────╯</tspan></text>
|
|
24
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="708" height="419.2" viewBox="0 0 708 419.2" font-family="Menlo, ui-monospace, monospace" font-size="14">
|
|
2
|
+
<rect width="708" height="419.2" rx="10" fill="#18181e"/>
|
|
3
|
+
<rect y="0" width="708" height="34" rx="10" fill="#505050" opacity="0.25"/>
|
|
4
|
+
<circle cx="24" cy="17" r="5" fill="#ff5f57"/><circle cx="42" cy="17" r="5" fill="#febc2e"/><circle cx="60" cy="17" r="5" fill="#28c840"/>
|
|
5
|
+
<text x="80" y="22" fill="#808080" font-size="13">pi — /btw (3 side questions, newest selected)</text>
|
|
6
|
+
<text x="18" y="66.0" fill="#d4d4d4" xml:space="preserve"><tspan>╭─</tspan><tspan fill="#8abeb7"> btw </tspan><tspan fill="#5f87ff">─</tspan><tspan fill="#5f87ff">────────────────────────────────────────────────</tspan><tspan fill="#5f87ff">─</tspan><tspan> </tspan><tspan fill="#666666">opencode/gpt-5.6-sol</tspan><tspan> </tspan><tspan fill="#5f87ff">╮</tspan></text>
|
|
7
|
+
<text x="18" y="85.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#666666">3 side questions</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
8
|
+
<text x="18" y="104.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
9
|
+
<text x="18" y="124.2" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#8abeb7">❯ </tspan><tspan fill="#d4d4d4">did the compaction threshold change in this branch?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
10
|
+
<text x="18" y="143.6" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
11
|
+
<text x="18" y="163.0" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
12
|
+
<text x="18" y="182.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> Yes. </tspan><tspan fill="#8abeb7">COMPACT_AT_TOKENS</tspan><tspan> moved from 120k to 140k, and the trigger now uses the </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
13
|
+
<text x="18" y="201.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> prompt token count instead of the last response's total, so it fires about </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
14
|
+
<text x="18" y="221.2" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> one turn later than before. </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
15
|
+
<text x="18" y="240.6" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
16
|
+
<text x="18" y="260.0" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
17
|
+
<text x="18" y="279.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#505050">────────────────────────────────────────────────────────────────────────────</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
18
|
+
<text x="18" y="298.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
19
|
+
<text x="18" y="318.2" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#808080">·</tspan><tspan> </tspan><tspan fill="#808080">what does the retry wrapper do on a 429?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
20
|
+
<text x="18" y="337.6" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#808080">·</tspan><tspan> </tspan><tspan fill="#808080">why is the cache keyed by branch id instead of session id?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
21
|
+
<text x="18" y="357.0" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#8abeb7">❯</tspan><tspan> </tspan><tspan fill="#808080">did the compaction threshold change in this branch?</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
22
|
+
<text x="18" y="376.4" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">│</tspan><tspan> </tspan><tspan fill="#666666">↑/↓ to scroll · ⇧←/→ to browse · c to copy · f to fork · Esc to close 3/3</tspan><tspan> </tspan><tspan fill="#5f87ff">│</tspan></text>
|
|
23
|
+
<text x="18" y="395.8" fill="#d4d4d4" xml:space="preserve"><tspan fill="#5f87ff">╰──────────────────────────────────────────────────────────────────────────────╯</tspan></text>
|
|
24
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="741.6" height="264" viewBox="0 0 741.6 264" font-family="Menlo, ui-monospace, monospace" font-size="14">
|
|
2
|
+
<rect width="741.6" height="264" rx="10" fill="#18181e"/>
|
|
3
|
+
<rect y="0" width="741.6" height="34" rx="10" fill="#505050" opacity="0.25"/>
|
|
4
|
+
<circle cx="24" cy="17" r="5" fill="#ff5f57"/><circle cx="42" cy="17" r="5" fill="#febc2e"/><circle cx="60" cy="17" r="5" fill="#28c840"/>
|
|
5
|
+
<text x="80" y="22" fill="#808080" font-size="13">pi — f promotes the selected exchange into the main conversation</text>
|
|
6
|
+
<text x="18" y="66.0" fill="#d4d4d4" xml:space="preserve"><tspan> </tspan><tspan fill="#8abeb7">❯ btw</tspan><tspan fill="#666666"> · promoted into the main conversation</tspan><tspan> </tspan></text>
|
|
7
|
+
<text x="18" y="85.4" fill="#d4d4d4" xml:space="preserve"><tspan> [btw] The user promoted this side exchange into the main conversation. The answer </tspan></text>
|
|
8
|
+
<text x="18" y="104.8" fill="#d4d4d4" xml:space="preserve"><tspan> was produced without tools from conversation context only; verify it before </tspan></text>
|
|
9
|
+
<text x="18" y="124.2" fill="#d4d4d4" xml:space="preserve"><tspan> relying on it. </tspan></text>
|
|
10
|
+
<text x="18" y="143.6" fill="#d4d4d4" xml:space="preserve"><tspan> </tspan></text>
|
|
11
|
+
<text x="18" y="163.0" fill="#d4d4d4" xml:space="preserve"><tspan> </tspan><tspan fill="#808080">│ </tspan><tspan fill="#808080">did the compaction threshold change in this branch?</tspan><tspan> </tspan></text>
|
|
12
|
+
<text x="18" y="182.4" fill="#d4d4d4" xml:space="preserve"><tspan> </tspan></text>
|
|
13
|
+
<text x="18" y="201.8" fill="#d4d4d4" xml:space="preserve"><tspan> Yes. </tspan><tspan fill="#8abeb7">COMPACT_AT_TOKENS</tspan><tspan> moved from 120k to 140k, and the trigger now uses the </tspan></text>
|
|
14
|
+
<text x="18" y="221.2" fill="#d4d4d4" xml:space="preserve"><tspan> prompt token count instead of the last response's total, so it fires about one </tspan></text>
|
|
15
|
+
<text x="18" y="240.6" fill="#d4d4d4" xml:space="preserve"><tspan> turn later than before. </tspan></text>
|
|
16
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-btw-cc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code-style /btw side questions for pi: a no-tools side thread in a floating overlay, persisted per session and promotable into the main conversation with one key.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Nouvelle-Lune",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Nouvelle-Lune/pi-btw-cc.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Nouvelle-Lune/pi-btw-cc#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Nouvelle-Lune/pi-btw-cc/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi",
|
|
19
|
+
"pi-coding-agent",
|
|
20
|
+
"pi-extension",
|
|
21
|
+
"btw",
|
|
22
|
+
"side-question",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"overlay",
|
|
25
|
+
"tui"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.19.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src/**/*.ts",
|
|
32
|
+
"docs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
42
|
+
"docs:images": "node --experimental-strip-types tools/make-doc-images.mjs"
|
|
43
|
+
},
|
|
44
|
+
"pi": {
|
|
45
|
+
"extensions": [
|
|
46
|
+
"./src/index.ts"
|
|
47
|
+
],
|
|
48
|
+
"image": "https://raw.githubusercontent.com/Nouvelle-Lune/pi-btw-cc/main/docs/btw-overlay.png"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@earendil-works/pi-ai": "*",
|
|
52
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
53
|
+
"@earendil-works/pi-tui": "*"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"@earendil-works/pi-ai": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@earendil-works/pi-coding-agent": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@earendil-works/pi-tui": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
68
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
69
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
70
|
+
"@types/node": "^22.20.2",
|
|
71
|
+
"typescript": "5.9.3"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt and message construction for /btw side requests.
|
|
3
|
+
*
|
|
4
|
+
* A side request is one no-tools, single-response call that sees the main
|
|
5
|
+
* session's conversation plus the earlier exchanges of the side thread.
|
|
6
|
+
*
|
|
7
|
+
* Only types are imported from pi-ai, so the message-building rules are unit
|
|
8
|
+
* tested outside the pi runtime.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AssistantMessage, Message, Usage } from "@earendil-works/pi-ai";
|
|
12
|
+
import type { BtwExchange } from "./thread.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Instructions attached as the system prompt of every side request.
|
|
16
|
+
*
|
|
17
|
+
* Claude Code reuses the main system prompt and keeps the full tool schema in
|
|
18
|
+
* the request while denying every tool call client-side. This extension sends
|
|
19
|
+
* a short standalone block instead: the side answer must not inherit the main
|
|
20
|
+
* agent's tool-oriented prompt, and it must answer from context only. The
|
|
21
|
+
* trade-off is that a side request shares no prompt-cache prefix with the main
|
|
22
|
+
* conversation (different system prompt, no tools), so its input is billed in
|
|
23
|
+
* full.
|
|
24
|
+
*/
|
|
25
|
+
export const BTW_SYSTEM_PROMPT = `This is a side question ("/btw") from the user about an ongoing coding session. Answer it directly in a single response.
|
|
26
|
+
|
|
27
|
+
CRITICAL CONSTRAINTS:
|
|
28
|
+
- You have NO tools available: you cannot read files, run commands, search, or take any actions.
|
|
29
|
+
- This is a one-off response. The user sees it in a side overlay; no follow-up turn follows inside this response.
|
|
30
|
+
- Use only information already present in the conversation context. Earlier <btw_question> exchanges are part of this side thread and may be referenced.
|
|
31
|
+
- NEVER say "Let me check...", "I'll now...", "Let me look...", or promise to do anything.
|
|
32
|
+
- If the context does not contain the answer, say so directly. Do not offer to investigate.
|
|
33
|
+
- Answer in the same language as the question. Be concise and specific; prefer one short paragraph or a short list over a long essay.`;
|
|
34
|
+
|
|
35
|
+
/** Model identity needed to fabricate replayed assistant replies. */
|
|
36
|
+
export interface SideModelRef {
|
|
37
|
+
api: string;
|
|
38
|
+
provider: string;
|
|
39
|
+
id: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SideRequestInput {
|
|
43
|
+
/** Main-session context converted to provider messages, oldest first. */
|
|
44
|
+
context: readonly Message[];
|
|
45
|
+
/** Earlier exchanges of the side thread, oldest first. */
|
|
46
|
+
replayed: readonly BtwExchange[];
|
|
47
|
+
/** The new side question. */
|
|
48
|
+
question: string;
|
|
49
|
+
/** Active model, used for the replayed assistant replies' metadata. */
|
|
50
|
+
model: SideModelRef;
|
|
51
|
+
/** Timestamp for the new question message. */
|
|
52
|
+
now: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the provider messages for one side request.
|
|
57
|
+
*
|
|
58
|
+
* Promoted exchanges are part of the main context at the position where they
|
|
59
|
+
* were appended, so a promoted Q&A can precede older replayed side questions.
|
|
60
|
+
* Both orders are valid: the main context is chronological, and the replay
|
|
61
|
+
* block only carries side questions that never entered it.
|
|
62
|
+
*/
|
|
63
|
+
export function buildSideRequest(input: SideRequestInput): Message[] {
|
|
64
|
+
const messages = trimOpenToolCalls(input.context);
|
|
65
|
+
for (const exchange of input.replayed) {
|
|
66
|
+
messages.push({
|
|
67
|
+
role: "user",
|
|
68
|
+
content: wrapSideQuestion(exchange.question),
|
|
69
|
+
timestamp: exchange.answeredAt,
|
|
70
|
+
});
|
|
71
|
+
messages.push(assistantReply(exchange, input.model));
|
|
72
|
+
}
|
|
73
|
+
messages.push({ role: "user", content: wrapSideQuestion(input.question), timestamp: input.now });
|
|
74
|
+
return messages;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Wrap a side question so replayed and current questions are unambiguous. */
|
|
78
|
+
export function wrapSideQuestion(question: string): string {
|
|
79
|
+
return `<btw_question>\n${question.trim()}\n</btw_question>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Keep the longest context prefix in which every tool call is resolved.
|
|
84
|
+
*
|
|
85
|
+
* While the main agent executes tools, the session branch can end with an
|
|
86
|
+
* assistant message whose tool calls have no matching tool result yet.
|
|
87
|
+
* Providers reject a trailing unresolved tool call, so the side request drops
|
|
88
|
+
* that unfinished tail instead of failing.
|
|
89
|
+
*/
|
|
90
|
+
export function trimOpenToolCalls(messages: readonly Message[]): Message[] {
|
|
91
|
+
const open = new Set<string>();
|
|
92
|
+
let safeLength = 0;
|
|
93
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
94
|
+
const message = messages[index]!;
|
|
95
|
+
if (message.role === "assistant") {
|
|
96
|
+
for (const block of message.content) {
|
|
97
|
+
if (block.type === "toolCall") open.add(block.id);
|
|
98
|
+
}
|
|
99
|
+
} else if (message.role === "toolResult") {
|
|
100
|
+
open.delete(message.toolCallId);
|
|
101
|
+
}
|
|
102
|
+
if (open.size === 0) safeLength = index + 1;
|
|
103
|
+
}
|
|
104
|
+
return safeLength === messages.length ? [...messages] : messages.slice(0, safeLength);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fabricate the assistant half of a replayed exchange.
|
|
109
|
+
*
|
|
110
|
+
* The recorded answer becomes a normal assistant turn so the model treats the
|
|
111
|
+
* side thread as an alternating conversation. Token usage is zero because
|
|
112
|
+
* replaying carries no billing information of its own.
|
|
113
|
+
*/
|
|
114
|
+
function assistantReply(exchange: BtwExchange, model: SideModelRef): AssistantMessage {
|
|
115
|
+
return {
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content: [{ type: "text", text: exchange.answer }],
|
|
118
|
+
api: model.api,
|
|
119
|
+
provider: model.provider,
|
|
120
|
+
model: model.id,
|
|
121
|
+
usage: emptyUsage(),
|
|
122
|
+
stopReason: "stop",
|
|
123
|
+
timestamp: exchange.answeredAt,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function emptyUsage(): Usage {
|
|
128
|
+
return {
|
|
129
|
+
input: 0,
|
|
130
|
+
output: 0,
|
|
131
|
+
cacheRead: 0,
|
|
132
|
+
cacheWrite: 0,
|
|
133
|
+
totalTokens: 0,
|
|
134
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
135
|
+
};
|
|
136
|
+
}
|