tau-all-agent 0.1.6 → 0.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 +17 -14
- package/extensions/answer.ts +223 -146
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +121 -108
- package/extensions/btw.ts +79 -33
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +166 -237
- package/extensions/loop.ts +126 -160
- package/extensions/memory.ts +202 -118
- package/extensions/notify.ts +17 -27
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +25 -22
- package/extensions/review/git.ts +99 -103
- package/extensions/review/index.ts +62 -64
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +90 -4
- package/extensions/review/prompts.ts +41 -38
- package/extensions/review/review.ts +164 -171
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +182 -128
- package/extensions/review/triage.ts +43 -48
- package/extensions/sandbox/README.md +5 -0
- package/extensions/sandbox/bash.ts +773 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +145 -3341
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +169 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +31 -0
- package/extensions/sandbox/runtime.ts +650 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +130 -0
- package/extensions/subagent/config.ts +118 -0
- package/extensions/subagent/index.ts +854 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/telegram/README.md +13 -1
- package/extensions/telegram/assistant-result.d.mts +4 -1
- package/extensions/telegram/assistant-result.mjs +31 -16
- package/extensions/telegram/daemon.mjs +351 -255
- package/extensions/telegram/index.ts +47 -55
- package/extensions/tool-display-mode.ts +288 -106
- package/extensions/usage/README.md +5 -0
- package/extensions/usage/index.ts +384 -432
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +13 -41
- package/extensions/websearch/config.ts +6 -2
- package/extensions/websearch/index.ts +111 -117
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai.pi.ts +222 -0
- package/extensions/websearch/providers/pi-model.shared.ts +33 -39
- package/extensions/websearch/providers/shared.ts +94 -2
- package/extensions/websearch/types.ts +2 -4
- package/extensions/worktree.ts +122 -218
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/homeassistant-ops/SKILL.md +30 -161
- package/skills/homeassistant-ops/references/api.md +4 -3
- package/skills/homeassistant-ops/references/commands.md +137 -0
- package/skills/homeassistant-ops/references/id_conventions.md +10 -10
- package/skills/homeassistant-ops/references/playbook.md +22 -15
- package/skills/openscad/SKILL.md +25 -133
- package/skills/openscad/references/authoring.md +38 -0
- package/skills/openscad/references/parameters.md +36 -0
- package/skills/openscad/references/publishing.md +12 -0
- package/skills/openscad/references/rendering.md +40 -0
- package/skills/oracle/SKILL.md +15 -9
- package/skills/oracle/scripts/oracle +51 -18
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +30 -120
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- package/extensions/websearch/providers/openai-codex.pi.ts +0 -49
- package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ pi install -l npm:tau-all-agent
|
|
|
23
23
|
| `answer` | `/answer` | Extract and interactively answer agent questions. |
|
|
24
24
|
| `branch-term` | `/branch` | Open a new terminal on the current session's git branch. |
|
|
25
25
|
| `btw` | `/btw` | Run a one-off side request with read-only tools and no context persistence. |
|
|
26
|
+
| `caffeinate` | — | Prevent system sleep while the agent runs. |
|
|
26
27
|
| `ghostty` | — | Ghostty tab title enhancements while the agent is working, waiting, or idle. |
|
|
27
28
|
| `git-diff-stats` | — | Status bar diff stats for local changes in the current repo. |
|
|
28
29
|
| `git-pr-status` | — | Status bar PR number and link for the current branch. |
|
|
@@ -31,11 +32,12 @@ pi install -l npm:tau-all-agent
|
|
|
31
32
|
| `loop` | `/loop` | Repeat a prompt until the agent signals success. |
|
|
32
33
|
| `memory` | `/memory` | Opt-in project-local memory for learning and continuity across sessions. |
|
|
33
34
|
| `notify` | — | Terminal notification when the agent is waiting for input. |
|
|
34
|
-
| `
|
|
35
|
-
| `openai-verbosity` | `/verbosity` | Set verbosity for supported
|
|
35
|
+
| `fast` | `/fast` | Toggle fast processing for compatible models. |
|
|
36
|
+
| `openai-verbosity` | `/verbosity` | Set verbosity for supported GPT models. |
|
|
36
37
|
| `review` | `/review`, `/triage` | Multi-focus review and PR feedback triage for PRs, branches, commits, and local changes, with integrated follow-up fixes. |
|
|
37
38
|
| `sandbox` | `/sandbox` | OS-level sandboxing for bash commands with runtime overrides. |
|
|
38
|
-
| `
|
|
39
|
+
| `subagent` | — | Delegate work to background Pi agents and follow up with them. |
|
|
40
|
+
| `tool-display-mode` | `ctrl+o` | Cycle tool output modes and combine background activity in the composer. |
|
|
39
41
|
| `telegram` | `/telegram` | Interact with Pi via a Telegram bot and local daemon. |
|
|
40
42
|
| `usage` | `/usage` | Historical provider usage breakdown with all-provider history and live quota snapshots. |
|
|
41
43
|
| `websearch` | — | Web search via Gemini, OpenAI, or Claude, leveraging Pi or browser session credentials. |
|
|
@@ -43,17 +45,18 @@ pi install -l npm:tau-all-agent
|
|
|
43
45
|
|
|
44
46
|
## Skills
|
|
45
47
|
|
|
46
|
-
| Skill
|
|
47
|
-
|
|
|
48
|
-
| `browser-tools`
|
|
49
|
-
| `git-clean-history`
|
|
50
|
-
| `git-commit`
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
48
|
+
| Skill | Description |
|
|
49
|
+
| --------------------- | ---------------------------------------------------------------------------------- |
|
|
50
|
+
| `browser-tools` | Interactive browser automation via Chrome DevTools Protocol. |
|
|
51
|
+
| `git-clean-history` | Reimplement a branch on a fresh branch off `main` with a clean commit history. |
|
|
52
|
+
| `git-commit` | Tidy, focused commits with clear rationale in messages. |
|
|
53
|
+
| `github-pull-request` | Create and steward pull requests through CI, review, and merge. |
|
|
54
|
+
| `homeassistant-ops` | Operate a Home Assistant instance via REST/WebSocket APIs. |
|
|
55
|
+
| `openscad` | Create and render OpenSCAD 3D models, export STL. |
|
|
56
|
+
| `oracle` | Second opinion from another LLM for debugging, refactors, design, or code reviews. |
|
|
57
|
+
| `sentry` | Fetch and analyze Sentry issues, events, transactions, and logs. |
|
|
58
|
+
| `update-changelog` | Update CHANGELOG.md following Keep a Changelog. |
|
|
59
|
+
| `web-design` | Distinctive, production-ready web interfaces. |
|
|
57
60
|
|
|
58
61
|
## Themes
|
|
59
62
|
|
package/extensions/answer.ts
CHANGED
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
* 4. Submits the compiled answers when done
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { complete } from "@earendil-works/pi-ai/compat";
|
|
14
13
|
import type { Model, Api, UserMessage } from "@earendil-works/pi-ai";
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
BorderedLoader,
|
|
16
|
+
type ExtensionAPI,
|
|
17
|
+
type KeybindingsManager,
|
|
18
|
+
type ModelRegistry,
|
|
19
|
+
type Theme,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
21
|
import {
|
|
17
22
|
type Component,
|
|
18
23
|
Editor,
|
|
@@ -20,12 +25,13 @@ import {
|
|
|
20
25
|
type Focusable,
|
|
21
26
|
Key,
|
|
22
27
|
matchesKey,
|
|
23
|
-
truncateToWidth,
|
|
24
28
|
type TUI,
|
|
25
29
|
visibleWidth,
|
|
26
30
|
wrapTextWithAnsi,
|
|
27
31
|
} from "@earendil-works/pi-tui";
|
|
28
32
|
|
|
33
|
+
const MIN_QUESTIONNAIRE_WIDTH = 40;
|
|
34
|
+
|
|
29
35
|
const SYSTEM_PROMPT = `You extract items that need user input from assistant text:
|
|
30
36
|
|
|
31
37
|
- Direct questions
|
|
@@ -78,8 +84,8 @@ Example output:
|
|
|
78
84
|
"questions": []
|
|
79
85
|
}`;
|
|
80
86
|
|
|
81
|
-
const OPENAI_FAST_MODEL_ID = "gpt-
|
|
82
|
-
const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-
|
|
87
|
+
const OPENAI_FAST_MODEL_ID = "gpt-6-luna";
|
|
88
|
+
const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-5-5";
|
|
83
89
|
|
|
84
90
|
// Structured output format for question extraction
|
|
85
91
|
interface ExtractedQuestion {
|
|
@@ -91,25 +97,16 @@ interface ExtractionResult {
|
|
|
91
97
|
questions: ExtractedQuestion[];
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
type ExtractionOutcome =
|
|
101
|
+
| { status: "success"; result: ExtractionResult }
|
|
102
|
+
| { status: "cancelled" }
|
|
103
|
+
| { status: "error"; message: string };
|
|
104
|
+
|
|
105
|
+
interface ModelSelection {
|
|
95
106
|
model: Model<Api>;
|
|
96
|
-
apiKey?: string;
|
|
97
|
-
headers?: Record<string, string>;
|
|
98
|
-
env?: Record<string, string>;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
type
|
|
102
|
-
find: (provider: string, modelId: string) => Model<Api> | undefined;
|
|
103
|
-
getApiKeyAndHeaders: (model: Model<Api>) => Promise<
|
|
104
|
-
| {
|
|
105
|
-
ok: true;
|
|
106
|
-
apiKey?: string;
|
|
107
|
-
headers?: Record<string, string>;
|
|
108
|
-
env?: Record<string, string>;
|
|
109
|
-
}
|
|
110
|
-
| { ok: false; error: string }
|
|
111
|
-
>;
|
|
112
|
-
};
|
|
109
|
+
type ModelLookup = Pick<ModelRegistry, "find" | "getApiKeyAndHeaders">;
|
|
113
110
|
|
|
114
111
|
type ModelFamily = "openai" | "anthropic";
|
|
115
112
|
|
|
@@ -125,10 +122,10 @@ function detectModelFamily(provider: string): ModelFamily | null {
|
|
|
125
122
|
*/
|
|
126
123
|
async function selectExtractionModel(
|
|
127
124
|
currentModel: Model<Api>,
|
|
128
|
-
modelRegistry:
|
|
129
|
-
): Promise<
|
|
125
|
+
modelRegistry: ModelLookup,
|
|
126
|
+
): Promise<ModelSelection | null> {
|
|
130
127
|
const family = detectModelFamily(currentModel.provider);
|
|
131
|
-
if (!family) return
|
|
128
|
+
if (!family) return resolveModelSelection(modelRegistry, currentModel);
|
|
132
129
|
|
|
133
130
|
const modelId = family === "openai" ? OPENAI_FAST_MODEL_ID : ANTHROPIC_FAST_MODEL_ID;
|
|
134
131
|
const providerCandidates =
|
|
@@ -140,21 +137,21 @@ async function selectExtractionModel(
|
|
|
140
137
|
const model = modelRegistry.find(provider, modelId);
|
|
141
138
|
if (!model) continue;
|
|
142
139
|
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
return
|
|
140
|
+
const selection = await resolveModelSelection(modelRegistry, model);
|
|
141
|
+
if (selection) {
|
|
142
|
+
return selection;
|
|
146
143
|
}
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
return
|
|
146
|
+
return resolveModelSelection(modelRegistry, currentModel);
|
|
150
147
|
}
|
|
151
148
|
|
|
152
|
-
async function
|
|
153
|
-
modelRegistry:
|
|
149
|
+
async function resolveModelSelection(
|
|
150
|
+
modelRegistry: ModelLookup,
|
|
154
151
|
model: Model<Api>,
|
|
155
|
-
): Promise<
|
|
152
|
+
): Promise<ModelSelection | null> {
|
|
156
153
|
const auth = await modelRegistry.getApiKeyAndHeaders(model);
|
|
157
|
-
return auth.ok ? { model
|
|
154
|
+
return auth.ok ? { model } : null;
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/**
|
|
@@ -171,11 +168,25 @@ function parseExtractionResult(text: string): ExtractionResult | null {
|
|
|
171
168
|
jsonStr = jsonMatch[1].trim();
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
const parsed = JSON.parse(jsonStr);
|
|
175
|
-
if (
|
|
176
|
-
|
|
171
|
+
const parsed: unknown = JSON.parse(jsonStr);
|
|
172
|
+
if (
|
|
173
|
+
!parsed ||
|
|
174
|
+
typeof parsed !== "object" ||
|
|
175
|
+
!("questions" in parsed) ||
|
|
176
|
+
!Array.isArray(parsed.questions) ||
|
|
177
|
+
!parsed.questions.every(
|
|
178
|
+
(question: unknown): question is ExtractedQuestion =>
|
|
179
|
+
question !== null &&
|
|
180
|
+
typeof question === "object" &&
|
|
181
|
+
"question" in question &&
|
|
182
|
+
typeof question.question === "string" &&
|
|
183
|
+
question.question.trim().length > 0 &&
|
|
184
|
+
(!("context" in question) || typeof question.context === "string"),
|
|
185
|
+
)
|
|
186
|
+
) {
|
|
187
|
+
return null;
|
|
177
188
|
}
|
|
178
|
-
return
|
|
189
|
+
return { questions: parsed.questions };
|
|
179
190
|
} catch {
|
|
180
191
|
return null;
|
|
181
192
|
}
|
|
@@ -193,9 +204,15 @@ class QnAComponent implements Component, Focusable {
|
|
|
193
204
|
private theme: Theme;
|
|
194
205
|
private onDone: (result: string | null) => void;
|
|
195
206
|
private showingConfirmation: boolean = false;
|
|
207
|
+
private questionOffset = 0;
|
|
208
|
+
private readingHeight = 0;
|
|
209
|
+
private maxQuestionOffset = 0;
|
|
210
|
+
private usable = false;
|
|
196
211
|
|
|
197
212
|
// Cache
|
|
213
|
+
private renderWidth?: number;
|
|
198
214
|
private cachedWidth?: number;
|
|
215
|
+
private cachedHeight?: number;
|
|
199
216
|
private cachedLines?: string[];
|
|
200
217
|
|
|
201
218
|
private _focused = false;
|
|
@@ -207,12 +224,14 @@ class QnAComponent implements Component, Focusable {
|
|
|
207
224
|
set focused(value: boolean) {
|
|
208
225
|
this._focused = value;
|
|
209
226
|
this.editor.focused = value;
|
|
227
|
+
this.invalidate();
|
|
210
228
|
}
|
|
211
229
|
|
|
212
230
|
constructor(
|
|
213
231
|
questions: ExtractedQuestion[],
|
|
214
232
|
tui: TUI,
|
|
215
233
|
theme: Theme,
|
|
234
|
+
private keybindings: KeybindingsManager,
|
|
216
235
|
onDone: (result: string | null) => void,
|
|
217
236
|
) {
|
|
218
237
|
this.questions = questions;
|
|
@@ -244,13 +263,14 @@ class QnAComponent implements Component, Focusable {
|
|
|
244
263
|
}
|
|
245
264
|
|
|
246
265
|
private saveCurrentAnswer(): void {
|
|
247
|
-
this.answers[this.currentIndex] = this.editor.
|
|
266
|
+
this.answers[this.currentIndex] = this.editor.getExpandedText();
|
|
248
267
|
}
|
|
249
268
|
|
|
250
269
|
private navigateTo(index: number): void {
|
|
251
270
|
if (index < 0 || index >= this.questions.length) return;
|
|
252
271
|
this.saveCurrentAnswer();
|
|
253
272
|
this.currentIndex = index;
|
|
273
|
+
this.questionOffset = 0;
|
|
254
274
|
this.editor.setText(this.answers[index] || "");
|
|
255
275
|
this.invalidate();
|
|
256
276
|
}
|
|
@@ -283,21 +303,57 @@ class QnAComponent implements Component, Focusable {
|
|
|
283
303
|
|
|
284
304
|
invalidate(): void {
|
|
285
305
|
this.cachedWidth = undefined;
|
|
306
|
+
this.cachedHeight = undefined;
|
|
286
307
|
this.cachedLines = undefined;
|
|
287
308
|
}
|
|
288
309
|
|
|
289
310
|
handleInput(data: string): void {
|
|
311
|
+
// Recheck current geometry before accepting input, including resize before repaint.
|
|
312
|
+
// Both dimensions reserve the one-cell margins from overlayOptions below.
|
|
313
|
+
this.render(
|
|
314
|
+
Math.min(this.renderWidth ?? this.tui.terminal.columns - 2, this.tui.terminal.columns - 2),
|
|
315
|
+
);
|
|
316
|
+
if (!this.usable) {
|
|
317
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) this.cancel();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
322
|
+
if (this.showingConfirmation) {
|
|
323
|
+
this.showingConfirmation = false;
|
|
324
|
+
this.invalidate();
|
|
325
|
+
this.tui.requestRender();
|
|
326
|
+
} else {
|
|
327
|
+
this.cancel();
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Viewport navigation reads the question. Unmatched keys retain native editing behavior.
|
|
333
|
+
const scrollActions = [
|
|
334
|
+
["tui.altScreen.pageUp", -this.readingHeight],
|
|
335
|
+
["tui.altScreen.pageDown", this.readingHeight],
|
|
336
|
+
["tui.altScreen.top", -Infinity],
|
|
337
|
+
["tui.altScreen.bottom", Infinity],
|
|
338
|
+
] as const;
|
|
339
|
+
for (const [action, delta] of scrollActions) {
|
|
340
|
+
if (!this.keybindings.matches(data, action)) continue;
|
|
341
|
+
this.questionOffset = Math.max(
|
|
342
|
+
0,
|
|
343
|
+
Math.min(this.maxQuestionOffset, this.questionOffset + delta),
|
|
344
|
+
);
|
|
345
|
+
this.invalidate();
|
|
346
|
+
this.tui.requestRender();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
290
350
|
// Handle confirmation dialog
|
|
291
351
|
if (this.showingConfirmation) {
|
|
292
352
|
if (matchesKey(data, Key.enter) || data.toLowerCase() === "y") {
|
|
293
353
|
this.submit();
|
|
294
354
|
return;
|
|
295
355
|
}
|
|
296
|
-
if (
|
|
297
|
-
matchesKey(data, Key.escape) ||
|
|
298
|
-
matchesKey(data, Key.ctrl("c")) ||
|
|
299
|
-
data.toLowerCase() === "n"
|
|
300
|
-
) {
|
|
356
|
+
if (data.toLowerCase() === "n") {
|
|
301
357
|
this.showingConfirmation = false;
|
|
302
358
|
this.invalidate();
|
|
303
359
|
this.tui.requestRender();
|
|
@@ -306,12 +362,6 @@ class QnAComponent implements Component, Focusable {
|
|
|
306
362
|
return;
|
|
307
363
|
}
|
|
308
364
|
|
|
309
|
-
// Global navigation and commands
|
|
310
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
311
|
-
this.cancel();
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
365
|
// Tab / Shift+Tab for navigation
|
|
316
366
|
if (matchesKey(data, Key.tab)) {
|
|
317
367
|
if (this.currentIndex < this.questions.length - 1) {
|
|
@@ -368,14 +418,48 @@ class QnAComponent implements Component, Focusable {
|
|
|
368
418
|
}
|
|
369
419
|
|
|
370
420
|
render(width: number): string[] {
|
|
371
|
-
|
|
421
|
+
this.renderWidth = width;
|
|
422
|
+
const height = Math.max(1, this.tui.terminal.rows - 2);
|
|
423
|
+
if (this.cachedLines && this.cachedWidth === width && this.cachedHeight === height) {
|
|
424
|
+
this.usable = true;
|
|
372
425
|
return this.cachedLines;
|
|
373
426
|
}
|
|
374
427
|
|
|
375
428
|
const theme = this.theme;
|
|
376
429
|
const lines: string[] = [];
|
|
377
|
-
const boxWidth = Math.min(width - 4, 120);
|
|
378
|
-
const contentWidth = boxWidth - 4;
|
|
430
|
+
const boxWidth = Math.min(Math.max(MIN_QUESTIONNAIRE_WIDTH, width) - 4, 120);
|
|
431
|
+
const contentWidth = boxWidth - 4;
|
|
432
|
+
const editorLines = this.editor.render(contentWidth - 7);
|
|
433
|
+
const readingKeys = [
|
|
434
|
+
...this.keybindings.getKeys("tui.altScreen.pageUp"),
|
|
435
|
+
...this.keybindings.getKeys("tui.altScreen.pageDown"),
|
|
436
|
+
...this.keybindings.getKeys("tui.altScreen.top"),
|
|
437
|
+
...this.keybindings.getKeys("tui.altScreen.bottom"),
|
|
438
|
+
].join("/");
|
|
439
|
+
const controls = this.showingConfirmation
|
|
440
|
+
? `${theme.fg("warning", "Submit all answers?")} Enter/y confirm · Esc/n back`
|
|
441
|
+
: "Tab/Enter next · Shift+Tab prev · Shift+Enter newline · Esc cancel";
|
|
442
|
+
const footer = [
|
|
443
|
+
...wrapTextWithAnsi(
|
|
444
|
+
theme.fg("dim", `Read question: ${readingKeys || "navigation keys unbound"}`),
|
|
445
|
+
contentWidth,
|
|
446
|
+
),
|
|
447
|
+
...wrapTextWithAnsi(controls, contentWidth),
|
|
448
|
+
];
|
|
449
|
+
// Six frame rows (top/title/divider, editor spacer, footer divider, bottom),
|
|
450
|
+
// the full native editor and wrapped footer, plus at least one reading row.
|
|
451
|
+
const minimumHeight = 6 + editorLines.length + footer.length + 1;
|
|
452
|
+
this.usable = width >= MIN_QUESTIONNAIRE_WIDTH && height >= minimumHeight;
|
|
453
|
+
if (!this.usable) {
|
|
454
|
+
this.invalidate();
|
|
455
|
+
return wrapTextWithAnsi(
|
|
456
|
+
theme.fg(
|
|
457
|
+
"muted",
|
|
458
|
+
`Resize to ${MIN_QUESTIONNAIRE_WIDTH + 2}+ columns and more rows. Esc cancels.`,
|
|
459
|
+
),
|
|
460
|
+
Math.max(1, width),
|
|
461
|
+
).slice(0, height);
|
|
462
|
+
}
|
|
379
463
|
|
|
380
464
|
// Helper to create horizontal lines (dim the whole thing at once)
|
|
381
465
|
const horizontalLine = (count: number) => "─".repeat(count);
|
|
@@ -388,10 +472,6 @@ class QnAComponent implements Component, Focusable {
|
|
|
388
472
|
return theme.fg("dim", "│") + paddedContent + " ".repeat(rightPad) + theme.fg("dim", "│");
|
|
389
473
|
};
|
|
390
474
|
|
|
391
|
-
const emptyBoxLine = (): string => {
|
|
392
|
-
return theme.fg("dim", "│") + " ".repeat(boxWidth - 2) + theme.fg("dim", "│");
|
|
393
|
-
};
|
|
394
|
-
|
|
395
475
|
const padToWidth = (line: string): string => {
|
|
396
476
|
const len = visibleWidth(line);
|
|
397
477
|
return line + " ".repeat(Math.max(0, width - len));
|
|
@@ -403,7 +483,14 @@ class QnAComponent implements Component, Focusable {
|
|
|
403
483
|
lines.push(padToWidth(boxLine(title)));
|
|
404
484
|
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
405
485
|
|
|
406
|
-
//
|
|
486
|
+
// Start with the question even when only one reading row fits.
|
|
487
|
+
const q = this.questions[this.currentIndex];
|
|
488
|
+
const reading = wrapTextWithAnsi(`> ${q.question}`, contentWidth);
|
|
489
|
+
if (q.context) {
|
|
490
|
+
reading.push("", ...wrapTextWithAnsi(theme.fg("muted", q.context), contentWidth - 2));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Progress belongs to the reading region so every indicator remains reachable.
|
|
407
494
|
const progressParts: string[] = [];
|
|
408
495
|
for (let i = 0; i < this.questions.length; i++) {
|
|
409
496
|
const answered = (this.answers[i]?.trim() || "").length > 0;
|
|
@@ -416,59 +503,30 @@ class QnAComponent implements Component, Focusable {
|
|
|
416
503
|
progressParts.push(theme.fg("dim", "○"));
|
|
417
504
|
}
|
|
418
505
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
506
|
+
reading.push("", ...wrapTextWithAnsi(progressParts.join(" "), contentWidth));
|
|
507
|
+
|
|
508
|
+
this.readingHeight = height - minimumHeight + 1;
|
|
509
|
+
this.maxQuestionOffset = Math.max(0, reading.length - this.readingHeight);
|
|
510
|
+
this.questionOffset = Math.min(this.questionOffset, this.maxQuestionOffset);
|
|
511
|
+
for (const line of reading.slice(
|
|
512
|
+
this.questionOffset,
|
|
513
|
+
this.questionOffset + this.readingHeight,
|
|
514
|
+
)) {
|
|
427
515
|
lines.push(padToWidth(boxLine(line)));
|
|
428
516
|
}
|
|
517
|
+
lines.push(padToWidth(boxLine("")));
|
|
429
518
|
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
lines.push(padToWidth(
|
|
433
|
-
const contextText = theme.fg("muted", `${q.context}`);
|
|
434
|
-
const wrappedContext = wrapTextWithAnsi(contextText, contentWidth - 2);
|
|
435
|
-
for (const line of wrappedContext) {
|
|
436
|
-
lines.push(padToWidth(boxLine(line)));
|
|
437
|
-
}
|
|
519
|
+
// Keep the entire native editor, including its cursor and scrolling indicators.
|
|
520
|
+
for (let i = 0; i < editorLines.length; i++) {
|
|
521
|
+
lines.push(padToWidth(boxLine((i === 1 ? theme.bold("A: ") : " ") + editorLines[i])));
|
|
438
522
|
}
|
|
439
523
|
|
|
440
|
-
lines.push(padToWidth(
|
|
441
|
-
|
|
442
|
-
// Render the editor component (multi-line input) with padding
|
|
443
|
-
// Skip the first and last lines (editor's own border lines)
|
|
444
|
-
const answerPrefix = theme.bold("A: ");
|
|
445
|
-
const editorWidth = contentWidth - 4 - 3; // Extra padding + space for "A: "
|
|
446
|
-
const editorLines = this.editor.render(editorWidth);
|
|
447
|
-
for (let i = 1; i < editorLines.length - 1; i++) {
|
|
448
|
-
if (i === 1) {
|
|
449
|
-
// First content line gets the "A: " prefix
|
|
450
|
-
lines.push(padToWidth(boxLine(answerPrefix + editorLines[i])));
|
|
451
|
-
} else {
|
|
452
|
-
// Subsequent lines get padding to align with the first line
|
|
453
|
-
lines.push(padToWidth(boxLine(" " + editorLines[i])));
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
lines.push(padToWidth(emptyBoxLine()));
|
|
458
|
-
|
|
459
|
-
// Confirmation dialog or footer with controls
|
|
460
|
-
if (this.showingConfirmation) {
|
|
461
|
-
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
462
|
-
const confirmMsg = `${theme.fg("warning", "Submit all answers?")} ${theme.fg("dim", "(Enter/y to confirm, Esc/n to cancel)")}`;
|
|
463
|
-
lines.push(padToWidth(boxLine(truncateToWidth(confirmMsg, contentWidth))));
|
|
464
|
-
} else {
|
|
465
|
-
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
466
|
-
const controls = `${theme.fg("dim", "Tab/Enter")} next · ${theme.fg("dim", "Shift+Tab")} prev · ${theme.fg("dim", "Shift+Enter")} newline · ${theme.fg("dim", "Esc")} cancel`;
|
|
467
|
-
lines.push(padToWidth(boxLine(truncateToWidth(controls, contentWidth))));
|
|
468
|
-
}
|
|
524
|
+
lines.push(padToWidth(theme.fg("dim", "├" + horizontalLine(boxWidth - 2) + "┤")));
|
|
525
|
+
for (const line of footer) lines.push(padToWidth(boxLine(line)));
|
|
469
526
|
lines.push(padToWidth(theme.fg("dim", "╰" + horizontalLine(boxWidth - 2) + "╯")));
|
|
470
527
|
|
|
471
528
|
this.cachedWidth = width;
|
|
529
|
+
this.cachedHeight = height;
|
|
472
530
|
this.cachedLines = lines;
|
|
473
531
|
return lines;
|
|
474
532
|
}
|
|
@@ -525,67 +583,86 @@ export default function (pi: ExtensionAPI) {
|
|
|
525
583
|
}
|
|
526
584
|
|
|
527
585
|
// Run extraction with loader UI
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
586
|
+
const extraction = await ctx.ui.custom<ExtractionOutcome>((tui, theme, _kb, done) => {
|
|
587
|
+
const loader = new BorderedLoader(
|
|
588
|
+
tui,
|
|
589
|
+
theme,
|
|
590
|
+
`Extracting questions using ${extractionSelection.model.id}...`,
|
|
591
|
+
);
|
|
592
|
+
loader.onAbort = () => done({ status: "cancelled" });
|
|
593
|
+
|
|
594
|
+
const doExtract = async (): Promise<ExtractionOutcome> => {
|
|
595
|
+
const userMessage: UserMessage = {
|
|
596
|
+
role: "user",
|
|
597
|
+
content: [{ type: "text", text: lastAssistantText! }],
|
|
598
|
+
timestamp: Date.now(),
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const response = await ctx.modelRegistry.complete(
|
|
602
|
+
extractionSelection.model,
|
|
603
|
+
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
|
604
|
+
{ signal: loader.signal },
|
|
534
605
|
);
|
|
535
|
-
loader.onAbort = () => done(null);
|
|
536
606
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
607
|
+
if (loader.signal.aborted || response.stopReason === "aborted") {
|
|
608
|
+
return { status: "cancelled" };
|
|
609
|
+
}
|
|
610
|
+
if (response.stopReason !== "stop") {
|
|
611
|
+
return {
|
|
612
|
+
status: "error",
|
|
613
|
+
message: response.errorMessage
|
|
614
|
+
? `Question extraction failed: ${response.errorMessage}`
|
|
615
|
+
: `Question extraction did not complete (${response.stopReason})`,
|
|
542
616
|
};
|
|
617
|
+
}
|
|
543
618
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
619
|
+
const responseText = response.content
|
|
620
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
621
|
+
.map((c) => c.text)
|
|
622
|
+
.join("\n");
|
|
623
|
+
|
|
624
|
+
const result = parseExtractionResult(responseText);
|
|
625
|
+
return result
|
|
626
|
+
? { status: "success", result }
|
|
627
|
+
: { status: "error", message: "Question extraction returned an invalid response" };
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
doExtract()
|
|
631
|
+
.then(done)
|
|
632
|
+
.catch((error: unknown) => {
|
|
633
|
+
done(
|
|
634
|
+
loader.signal.aborted
|
|
635
|
+
? { status: "cancelled" }
|
|
636
|
+
: {
|
|
637
|
+
status: "error",
|
|
638
|
+
message: `Question extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
639
|
+
},
|
|
553
640
|
);
|
|
641
|
+
});
|
|
554
642
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const responseText = response.content
|
|
560
|
-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
561
|
-
.map((c) => c.text)
|
|
562
|
-
.join("\n");
|
|
563
|
-
|
|
564
|
-
return parseExtractionResult(responseText);
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
doExtract()
|
|
568
|
-
.then(done)
|
|
569
|
-
.catch(() => done(null));
|
|
570
|
-
|
|
571
|
-
return loader;
|
|
572
|
-
},
|
|
573
|
-
);
|
|
643
|
+
return loader;
|
|
644
|
+
});
|
|
574
645
|
|
|
575
|
-
if (
|
|
646
|
+
if (extraction.status === "cancelled") {
|
|
576
647
|
ctx.ui.notify("Cancelled", "info");
|
|
577
648
|
return;
|
|
578
649
|
}
|
|
650
|
+
if (extraction.status === "error") {
|
|
651
|
+
ctx.ui.notify(extraction.message, "error");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
579
654
|
|
|
580
|
-
|
|
655
|
+
const { questions } = extraction.result;
|
|
656
|
+
if (questions.length === 0) {
|
|
581
657
|
ctx.ui.notify("No questions found in the last message", "info");
|
|
582
658
|
return;
|
|
583
659
|
}
|
|
584
660
|
|
|
585
661
|
// Show the Q&A component
|
|
586
|
-
const answersResult = await ctx.ui.custom<string | null>(
|
|
587
|
-
|
|
588
|
-
|
|
662
|
+
const answersResult = await ctx.ui.custom<string | null>(
|
|
663
|
+
(tui, theme, kb, done) => new QnAComponent(questions, tui, theme, kb, done),
|
|
664
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
|
|
665
|
+
);
|
|
589
666
|
|
|
590
667
|
if (answersResult === null) {
|
|
591
668
|
ctx.ui.notify("Cancelled", "info");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Branch terminals
|
|
2
|
+
|
|
3
|
+
`/branch` forks the selected conversation without switching the current session. Inside tmux, `--branch-tmux-layout` chooses `window`, `split-right`, or `split-down`; a `/branch` argument overrides that setting.
|
|
4
|
+
|
|
5
|
+
`--branch-term` takes precedence over tmux and accepts a Bash launcher template. Use unquoted `{cwd}` and `{session}` placeholders for path arguments. `{command}` supplies a single command-string argument, suitable for a launcher's command option or `bash -c`. Do not add shell quotes around placeholders; they are already quoted. If neither `{session}` nor `{command}` appears, the fork path is appended. The launcher runs in the session's working directory with the current environment.
|
|
6
|
+
|
|
7
|
+
A failed launcher produces a recovery command. Reloading or closing the original Pi session stops observing launcher failures but does not close user-owned terminals.
|