tau-all-agent 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 +45 -0
- package/extensions/answer.ts +601 -0
- package/extensions/branch-term.ts +405 -0
- package/extensions/btw.ts +444 -0
- package/extensions/ghostty.ts +301 -0
- package/extensions/git-diff-stats.ts +277 -0
- package/extensions/git-pr-status.ts +286 -0
- package/extensions/insights.ts +2367 -0
- package/extensions/interlude.ts +144 -0
- package/extensions/loop.ts +529 -0
- package/extensions/memory.ts +1889 -0
- package/extensions/notify.ts +161 -0
- package/extensions/openai-fast.ts +227 -0
- package/extensions/openai-verbosity.ts +223 -0
- package/extensions/review.ts +4347 -0
- package/extensions/sandbox/index.ts +2578 -0
- package/extensions/telegram/README.md +81 -0
- package/extensions/telegram/daemon.mjs +1942 -0
- package/extensions/telegram/index.ts +1073 -0
- package/extensions/telegram/message-text.d.mts +1 -0
- package/extensions/telegram/message-text.mjs +16 -0
- package/extensions/usage/anthropic.ts +198 -0
- package/extensions/usage/github-copilot.ts +204 -0
- package/extensions/usage/google-gemini-cli.ts +232 -0
- package/extensions/usage/index.ts +2310 -0
- package/extensions/usage/minimax.ts +208 -0
- package/extensions/usage/openai-codex.ts +180 -0
- package/extensions/usage/openrouter.ts +168 -0
- package/extensions/usage/providers.ts +26 -0
- package/extensions/usage/shared.ts +156 -0
- package/extensions/usage/types.ts +53 -0
- package/extensions/usage/zai.ts +186 -0
- package/extensions/websearch/README.md +68 -0
- package/extensions/websearch/browser/chromium.ts +330 -0
- package/extensions/websearch/browser/discovery.ts +75 -0
- package/extensions/websearch/browser/firefox.ts +150 -0
- package/extensions/websearch/browser/sqlite.ts +38 -0
- package/extensions/websearch/config.ts +76 -0
- package/extensions/websearch/index.ts +312 -0
- package/extensions/websearch/normalize.ts +155 -0
- package/extensions/websearch/providers/anthropic.pi.ts +139 -0
- package/extensions/websearch/providers/gemini.browser.ts +185 -0
- package/extensions/websearch/providers/gemini.pi.ts +108 -0
- package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
- package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
- package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
- package/extensions/websearch/providers/pi-model.shared.ts +86 -0
- package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
- package/extensions/websearch/providers/shared.ts +76 -0
- package/extensions/websearch/types.ts +52 -0
- package/extensions/worktree.ts +2223 -0
- package/package.json +76 -0
- package/skills/browser-tools/SKILL.md +253 -0
- package/skills/browser-tools/scripts/browser-content.js +100 -0
- package/skills/browser-tools/scripts/browser-cookies.js +33 -0
- package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
- package/skills/browser-tools/scripts/browser-eval.js +75 -0
- package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
- package/skills/browser-tools/scripts/browser-nav.js +28 -0
- package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
- package/skills/browser-tools/scripts/browser-pick.js +133 -0
- package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
- package/skills/browser-tools/scripts/browser-start.js +281 -0
- package/skills/browser-tools/scripts/browser-watch.js +321 -0
- package/skills/browser-tools/scripts/utils.js +60 -0
- package/skills/git-clean-history/SKILL.md +45 -0
- package/skills/git-commit/SKILL.md +55 -0
- package/skills/homeassistant-ops/SKILL.md +184 -0
- package/skills/homeassistant-ops/references/api.md +64 -0
- package/skills/homeassistant-ops/references/id_conventions.md +71 -0
- package/skills/homeassistant-ops/references/playbook.md +150 -0
- package/skills/homeassistant-ops/scripts/ha_ops.js +2942 -0
- package/skills/openscad/SKILL.md +149 -0
- package/skills/openscad/examples/parametric_box.scad +92 -0
- package/skills/openscad/examples/phone_stand.scad +95 -0
- package/skills/openscad/scripts/common.sh +49 -0
- package/skills/openscad/scripts/export-stl.sh +56 -0
- package/skills/openscad/scripts/extract-params.sh +147 -0
- package/skills/openscad/scripts/multi-preview.sh +68 -0
- package/skills/openscad/scripts/preview.sh +74 -0
- package/skills/openscad/scripts/render-with-params.sh +91 -0
- package/skills/openscad/scripts/validate.sh +46 -0
- package/skills/oracle/SKILL.md +64 -0
- package/skills/oracle/scripts/oracle +249 -0
- package/skills/oracle/scripts/oracle-bundle +227 -0
- package/skills/sentry/SKILL.md +198 -0
- package/skills/sentry/lib/auth.js +99 -0
- package/skills/sentry/scripts/fetch-event.js +325 -0
- package/skills/sentry/scripts/fetch-issue.js +368 -0
- package/skills/sentry/scripts/list-issues.js +245 -0
- package/skills/sentry/scripts/search-events.js +291 -0
- package/skills/sentry/scripts/search-logs.js +234 -0
- package/skills/update-changelog/SKILL.md +135 -0
- package/skills/web-design/SKILL.md +117 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { AssistantMessage, ThinkingLevel } from "@mariozechner/pi-ai";
|
|
3
|
+
import {
|
|
4
|
+
DefaultResourceLoader,
|
|
5
|
+
SessionManager,
|
|
6
|
+
buildSessionContext,
|
|
7
|
+
createAgentSession,
|
|
8
|
+
getAgentDir,
|
|
9
|
+
getMarkdownTheme,
|
|
10
|
+
type ExtensionAPI,
|
|
11
|
+
type ExtensionCommandContext,
|
|
12
|
+
type ExtensionContext,
|
|
13
|
+
type Theme,
|
|
14
|
+
} from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import {
|
|
16
|
+
Key,
|
|
17
|
+
Markdown,
|
|
18
|
+
matchesKey,
|
|
19
|
+
truncateToWidth,
|
|
20
|
+
type Component,
|
|
21
|
+
type TUI,
|
|
22
|
+
visibleWidth,
|
|
23
|
+
wrapTextWithAnsi,
|
|
24
|
+
} from "@mariozechner/pi-tui";
|
|
25
|
+
|
|
26
|
+
const STATUS_KEY = "0-btw";
|
|
27
|
+
const RESULT_MARKDOWN_THEME = getMarkdownTheme();
|
|
28
|
+
const STATUS_SPINNER_INTERVAL_MS = 80;
|
|
29
|
+
const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
30
|
+
|
|
31
|
+
const BTW_APPEND_SYSTEM_PROMPT = `## BTW mode
|
|
32
|
+
- This is a one-off side request. The result is shown transiently and is not added to the main conversation.
|
|
33
|
+
- Answer the user's request directly and concisely.
|
|
34
|
+
- Prefer answering directly. Use tools only when they materially help.
|
|
35
|
+
- Tooling override for this BTW run: only these read-only tools are available: read, grep, find, ls.
|
|
36
|
+
- Ignore any inherited prompt text that suggests other tools are available.
|
|
37
|
+
- Never attempt edits, writes, or mutating shell commands in BTW mode.
|
|
38
|
+
- If the request requires changes or mutating commands, say so briefly and tell the user to ask in the main conversation.`;
|
|
39
|
+
const BTW_TOOL_NAMES = ["read", "grep", "find", "ls"];
|
|
40
|
+
|
|
41
|
+
type BtwResult = {
|
|
42
|
+
question: string;
|
|
43
|
+
answer: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type BtwSnapshot = {
|
|
47
|
+
request: string;
|
|
48
|
+
cwd: string;
|
|
49
|
+
systemPrompt: string;
|
|
50
|
+
seedMessages: AgentMessage[];
|
|
51
|
+
model: NonNullable<ExtensionCommandContext["model"]>;
|
|
52
|
+
thinkingLevel: ThinkingLevel;
|
|
53
|
+
modelRegistry: ExtensionCommandContext["modelRegistry"];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type ActiveBtwRequest = {
|
|
57
|
+
id: string;
|
|
58
|
+
sessionKey: string;
|
|
59
|
+
abort?: () => Promise<void>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default function btwExtension(pi: ExtensionAPI): void {
|
|
63
|
+
let activeRequest: ActiveBtwRequest | null = null;
|
|
64
|
+
let statusTimer: ReturnType<typeof setInterval> | null = null;
|
|
65
|
+
const cancelledRequestIds = new Set<string>();
|
|
66
|
+
|
|
67
|
+
function getCurrentSessionKey(ctx: ExtensionContext): string {
|
|
68
|
+
return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isActiveRequest(requestId: string): boolean {
|
|
72
|
+
return activeRequest?.id === requestId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearStatus(ctx: ExtensionContext): void {
|
|
76
|
+
if (statusTimer) {
|
|
77
|
+
clearInterval(statusTimer);
|
|
78
|
+
statusTimer = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (ctx.hasUI) {
|
|
82
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function startStatus(ctx: ExtensionCommandContext): void {
|
|
87
|
+
clearStatus(ctx);
|
|
88
|
+
if (!ctx.hasUI) return;
|
|
89
|
+
|
|
90
|
+
let frame = 0;
|
|
91
|
+
const render = () => {
|
|
92
|
+
const spinner = STATUS_SPINNER_FRAMES[frame % STATUS_SPINNER_FRAMES.length];
|
|
93
|
+
ctx.ui.setStatus(STATUS_KEY, `${spinner} answering...`);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
render();
|
|
97
|
+
statusTimer = setInterval(() => {
|
|
98
|
+
frame = (frame + 1) % STATUS_SPINNER_FRAMES.length;
|
|
99
|
+
render();
|
|
100
|
+
}, STATUS_SPINNER_INTERVAL_MS);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function cancelActiveRequest(): Promise<void> {
|
|
104
|
+
if (!activeRequest) return;
|
|
105
|
+
|
|
106
|
+
cancelledRequestIds.add(activeRequest.id);
|
|
107
|
+
await activeRequest.abort?.().catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pi.registerCommand("btw", {
|
|
111
|
+
description: "Run a one-off side request with read-only tools and no main-context persistence",
|
|
112
|
+
handler: async (args, ctx) => {
|
|
113
|
+
const request = args.trim();
|
|
114
|
+
if (!request) {
|
|
115
|
+
ctx.ui.notify("Usage: /btw <request>", "warning");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!ctx.hasUI) {
|
|
120
|
+
ctx.ui.notify("btw requires interactive mode", "error");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!ctx.model) {
|
|
125
|
+
ctx.ui.notify("No model selected", "error");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sessionKey = getCurrentSessionKey(ctx);
|
|
130
|
+
if (activeRequest?.sessionKey === sessionKey) {
|
|
131
|
+
ctx.ui.notify("A BTW request is already active in this session.", "warning");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const snapshot = buildSnapshot(ctx, pi, request);
|
|
136
|
+
const requestId = `${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
137
|
+
activeRequest = { id: requestId, sessionKey };
|
|
138
|
+
startStatus(ctx);
|
|
139
|
+
|
|
140
|
+
void (async () => {
|
|
141
|
+
try {
|
|
142
|
+
const result = await runBtwRequest(snapshot, (abort) => {
|
|
143
|
+
const isCancelled = cancelledRequestIds.has(requestId);
|
|
144
|
+
if (isCancelled) {
|
|
145
|
+
void abort().catch(() => undefined);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!isActiveRequest(requestId)) return;
|
|
149
|
+
const current = activeRequest;
|
|
150
|
+
if (!current) return;
|
|
151
|
+
activeRequest = { ...current, abort };
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const isCurrentRequest = isActiveRequest(requestId);
|
|
155
|
+
const wasCancelled = cancelledRequestIds.delete(requestId);
|
|
156
|
+
if (isCurrentRequest) {
|
|
157
|
+
activeRequest = null;
|
|
158
|
+
clearStatus(ctx);
|
|
159
|
+
}
|
|
160
|
+
if (!isCurrentRequest || wasCancelled) return;
|
|
161
|
+
|
|
162
|
+
await showResultDialog(ctx, result);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const isCurrentRequest = isActiveRequest(requestId);
|
|
165
|
+
const wasCancelled = cancelledRequestIds.delete(requestId);
|
|
166
|
+
if (isCurrentRequest) {
|
|
167
|
+
activeRequest = null;
|
|
168
|
+
clearStatus(ctx);
|
|
169
|
+
}
|
|
170
|
+
if (!isCurrentRequest || wasCancelled) return;
|
|
171
|
+
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
ctx.ui.notify(message, "error");
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
180
|
+
await cancelActiveRequest();
|
|
181
|
+
activeRequest = null;
|
|
182
|
+
cancelledRequestIds.clear();
|
|
183
|
+
clearStatus(ctx);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
187
|
+
await cancelActiveRequest();
|
|
188
|
+
clearStatus(ctx);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildSnapshot(
|
|
193
|
+
ctx: ExtensionCommandContext,
|
|
194
|
+
pi: ExtensionAPI,
|
|
195
|
+
request: string,
|
|
196
|
+
): BtwSnapshot {
|
|
197
|
+
return {
|
|
198
|
+
request,
|
|
199
|
+
cwd: ctx.cwd,
|
|
200
|
+
systemPrompt: ctx.getSystemPrompt(),
|
|
201
|
+
seedMessages: structuredClone(
|
|
202
|
+
buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages,
|
|
203
|
+
) as AgentMessage[],
|
|
204
|
+
model: ctx.model!,
|
|
205
|
+
thinkingLevel: pi.getThinkingLevel() as ThinkingLevel,
|
|
206
|
+
modelRegistry: ctx.modelRegistry,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function runBtwRequest(
|
|
211
|
+
snapshot: BtwSnapshot,
|
|
212
|
+
onAbortReady?: (abort: () => Promise<void>) => void,
|
|
213
|
+
): Promise<BtwResult> {
|
|
214
|
+
const sessionManager = SessionManager.inMemory(snapshot.cwd);
|
|
215
|
+
seedSessionManager(sessionManager, snapshot.seedMessages);
|
|
216
|
+
|
|
217
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
218
|
+
cwd: snapshot.cwd,
|
|
219
|
+
agentDir: getAgentDir(),
|
|
220
|
+
noExtensions: true,
|
|
221
|
+
noSkills: true,
|
|
222
|
+
noPromptTemplates: true,
|
|
223
|
+
noThemes: true,
|
|
224
|
+
agentsFilesOverride: () => ({ agentsFiles: [] }),
|
|
225
|
+
systemPromptOverride: () => snapshot.systemPrompt,
|
|
226
|
+
appendSystemPromptOverride: () => [BTW_APPEND_SYSTEM_PROMPT],
|
|
227
|
+
});
|
|
228
|
+
await resourceLoader.reload();
|
|
229
|
+
|
|
230
|
+
const { session } = await createAgentSession({
|
|
231
|
+
cwd: snapshot.cwd,
|
|
232
|
+
model: snapshot.model,
|
|
233
|
+
thinkingLevel: snapshot.thinkingLevel,
|
|
234
|
+
modelRegistry: snapshot.modelRegistry,
|
|
235
|
+
resourceLoader,
|
|
236
|
+
tools: BTW_TOOL_NAMES,
|
|
237
|
+
sessionManager,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
onAbortReady?.(() => session.abort());
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await session.prompt(snapshot.request);
|
|
244
|
+
|
|
245
|
+
const lastAssistantMessage = getLastAssistantMessage(session.state.messages);
|
|
246
|
+
if (!lastAssistantMessage) {
|
|
247
|
+
throw new Error("BTW request finished without a response.");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (lastAssistantMessage.stopReason === "aborted") {
|
|
251
|
+
throw new Error("BTW request was aborted.");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (lastAssistantMessage.stopReason === "error") {
|
|
255
|
+
throw new Error(lastAssistantMessage.errorMessage || "BTW request failed.");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
question: snapshot.request,
|
|
260
|
+
answer: extractText(lastAssistantMessage.content) || "(No text response)",
|
|
261
|
+
};
|
|
262
|
+
} finally {
|
|
263
|
+
try {
|
|
264
|
+
await session.abort();
|
|
265
|
+
} catch {
|
|
266
|
+
// Ignore abort errors during cleanup.
|
|
267
|
+
}
|
|
268
|
+
session.dispose();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function seedSessionManager(sessionManager: SessionManager, messages: AgentMessage[]): void {
|
|
273
|
+
type SessionMessageInput = Parameters<SessionManager["appendMessage"]>[0];
|
|
274
|
+
|
|
275
|
+
for (const message of messages) {
|
|
276
|
+
sessionManager.appendMessage(message as SessionMessageInput);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function showResultDialog(ctx: ExtensionContext, result: BtwResult): Promise<void> {
|
|
281
|
+
await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
|
|
282
|
+
return new BtwResultComponent(result, tui, ctx.ui.theme, done);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getLastAssistantMessage(messages: AgentMessage[]): AssistantMessage | null {
|
|
287
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
288
|
+
const message = messages[i];
|
|
289
|
+
if (isAssistantMessage(message)) {
|
|
290
|
+
return message;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isAssistantMessage(message: AgentMessage): message is AssistantMessage {
|
|
298
|
+
return message.role === "assistant" && Array.isArray(message.content);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function extractText(content: AssistantMessage["content"]): string {
|
|
302
|
+
return content
|
|
303
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
304
|
+
.map((part) => part.text)
|
|
305
|
+
.join("\n")
|
|
306
|
+
.trim();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
class BtwResultComponent implements Component {
|
|
310
|
+
private readonly markdown: Markdown;
|
|
311
|
+
private scrollOffset = 0;
|
|
312
|
+
private cachedBodyWidth?: number;
|
|
313
|
+
private cachedBodyLines?: string[];
|
|
314
|
+
|
|
315
|
+
constructor(
|
|
316
|
+
private readonly result: BtwResult,
|
|
317
|
+
private readonly tui: TUI,
|
|
318
|
+
private readonly theme: Theme,
|
|
319
|
+
private readonly onDone: () => void,
|
|
320
|
+
) {
|
|
321
|
+
this.markdown = new Markdown(result.answer, 0, 0, RESULT_MARKDOWN_THEME);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
invalidate(): void {
|
|
325
|
+
this.cachedBodyWidth = undefined;
|
|
326
|
+
this.cachedBodyLines = undefined;
|
|
327
|
+
this.markdown.invalidate();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
handleInput(data: string): void {
|
|
331
|
+
if (
|
|
332
|
+
matchesKey(data, Key.enter) ||
|
|
333
|
+
matchesKey(data, Key.escape) ||
|
|
334
|
+
matchesKey(data, Key.ctrl("c")) ||
|
|
335
|
+
data.toLowerCase() === "q"
|
|
336
|
+
) {
|
|
337
|
+
this.onDone();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const bodyHeight = this.getBodyHeight();
|
|
342
|
+
const boxWidth = this.getBoxWidth(this.tui.terminal.columns);
|
|
343
|
+
const bodyLines = this.getBodyLines(this.getContentWidth(boxWidth));
|
|
344
|
+
const maxScroll = Math.max(0, bodyLines.length - bodyHeight);
|
|
345
|
+
|
|
346
|
+
if (matchesKey(data, Key.up) || data.toLowerCase() === "k") {
|
|
347
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
348
|
+
} else if (matchesKey(data, Key.down) || data.toLowerCase() === "j") {
|
|
349
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
350
|
+
} else if (matchesKey(data, Key.pageUp)) {
|
|
351
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - Math.max(4, bodyHeight - 2));
|
|
352
|
+
} else if (matchesKey(data, Key.pageDown)) {
|
|
353
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + Math.max(4, bodyHeight - 2));
|
|
354
|
+
} else if (matchesKey(data, Key.home)) {
|
|
355
|
+
this.scrollOffset = 0;
|
|
356
|
+
} else if (matchesKey(data, Key.end)) {
|
|
357
|
+
this.scrollOffset = maxScroll;
|
|
358
|
+
} else {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.tui.requestRender();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
render(width: number): string[] {
|
|
366
|
+
const boxWidth = this.getBoxWidth(width);
|
|
367
|
+
const contentWidth = this.getContentWidth(boxWidth);
|
|
368
|
+
const questionLines = wrapTextWithAnsi(
|
|
369
|
+
this.theme.fg("muted", this.result.question),
|
|
370
|
+
contentWidth,
|
|
371
|
+
);
|
|
372
|
+
const bodyLines = this.getBodyLines(contentWidth);
|
|
373
|
+
const bodyHeight = this.getBodyHeight();
|
|
374
|
+
const maxScroll = Math.max(0, bodyLines.length - bodyHeight);
|
|
375
|
+
this.scrollOffset = clamp(this.scrollOffset, 0, maxScroll);
|
|
376
|
+
|
|
377
|
+
const lines: string[] = [];
|
|
378
|
+
lines.push(this.borderLine("╭", "╮", boxWidth));
|
|
379
|
+
lines.push(this.boxLine(this.theme.bold("Request"), boxWidth));
|
|
380
|
+
for (const line of questionLines) {
|
|
381
|
+
lines.push(this.boxLine(line, boxWidth));
|
|
382
|
+
}
|
|
383
|
+
lines.push(this.separatorLine(boxWidth));
|
|
384
|
+
|
|
385
|
+
const visibleBody = bodyLines.slice(this.scrollOffset, this.scrollOffset + bodyHeight);
|
|
386
|
+
for (const line of visibleBody) {
|
|
387
|
+
lines.push(this.boxLine(line, boxWidth));
|
|
388
|
+
}
|
|
389
|
+
for (let i = visibleBody.length; i < bodyHeight; i += 1) {
|
|
390
|
+
lines.push(this.boxLine("", boxWidth));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
lines.push(this.separatorLine(boxWidth));
|
|
394
|
+
const scrollText = `${Math.min(bodyLines.length, this.scrollOffset + 1)}-${Math.min(bodyLines.length, this.scrollOffset + visibleBody.length)}/${bodyLines.length}`;
|
|
395
|
+
const controls = `${this.theme.fg("dim", "↑↓ scroll · PgUp/PgDn jump · Home/End · Enter/Esc close")} ${this.theme.fg("muted", scrollText)}`;
|
|
396
|
+
lines.push(this.boxLine(truncateToWidth(controls, contentWidth), boxWidth));
|
|
397
|
+
lines.push(this.borderLine("╰", "╯", boxWidth));
|
|
398
|
+
|
|
399
|
+
return lines;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private getBodyHeight(): number {
|
|
403
|
+
return Math.max(8, this.tui.terminal.rows - 14);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private getBodyLines(contentWidth: number): string[] {
|
|
407
|
+
if (this.cachedBodyWidth === contentWidth && this.cachedBodyLines) {
|
|
408
|
+
return this.cachedBodyLines;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const lines = this.markdown.render(contentWidth);
|
|
412
|
+
this.cachedBodyWidth = contentWidth;
|
|
413
|
+
this.cachedBodyLines = lines;
|
|
414
|
+
return lines;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private getBoxWidth(width: number): number {
|
|
418
|
+
return Math.max(50, Math.min(width - 2, 140));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private getContentWidth(boxWidth: number): number {
|
|
422
|
+
return Math.max(10, boxWidth - 4);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private borderLine(left: string, right: string, width: number): string {
|
|
426
|
+
return this.theme.fg("borderMuted", `${left}${"─".repeat(width - 2)}${right}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private separatorLine(width: number): string {
|
|
430
|
+
return this.theme.fg("borderMuted", `├${"─".repeat(width - 2)}┤`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private boxLine(content: string, width: number): string {
|
|
434
|
+
const truncated = truncateToWidth(content, Math.max(1, width - 4), "");
|
|
435
|
+
const padded = ` ${truncated}`;
|
|
436
|
+
const visible = visibleWidth(padded);
|
|
437
|
+
const rightPad = Math.max(0, width - 2 - visible);
|
|
438
|
+
return `${this.theme.fg("borderMuted", "│")}${padded}${" ".repeat(rightPad)}${this.theme.fg("borderMuted", "│")}`;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function clamp(value: number, min: number, max: number): number {
|
|
443
|
+
return Math.max(min, Math.min(max, value));
|
|
444
|
+
}
|