@oh-my-pi/pi-coding-agent 15.9.5 → 15.9.67
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/CHANGELOG.md +35 -0
- package/dist/types/config/keybindings.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/tools/eval-render.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +25 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/package.json +9 -9
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/config/keybindings.ts +58 -1
- package/src/config/settings-schema.ts +11 -1
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +7 -7
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +28 -27
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/__tests__/shared-executors.test.ts +2 -2
- package/src/eval/agent-bridge.ts +4 -5
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/lsp/client.ts +80 -2
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/main.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +22 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/tool-execution.ts +27 -13
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/controllers/command-controller.ts +0 -116
- package/src/modes/controllers/event-controller.ts +26 -10
- package/src/modes/controllers/selector-controller.ts +38 -1
- package/src/modes/interactive-mode.ts +4 -4
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/session/agent-session.ts +6 -2
- package/src/slash-commands/builtin-registry.ts +3 -11
- package/src/task/render.ts +38 -11
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-render.ts +24 -9
- package/src/tools/eval.ts +14 -19
- package/src/tools/fetch.ts +5 -5
- package/src/tools/read.ts +7 -7
- package/src/tools/render-utils.ts +46 -0
- package/src/tools/ssh.ts +21 -8
- package/src/tools/write.ts +17 -8
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/output-block.ts +14 -0
- package/src/web/search/render.ts +3 -3
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { ToolCall } from "@oh-my-pi/pi-ai";
|
|
3
|
+
|
|
4
|
+
/** A fenced code block extracted from assistant markdown. */
|
|
5
|
+
export interface CodeBlock {
|
|
6
|
+
/** Info string after the opening fence (language id), trimmed. */
|
|
7
|
+
lang: string;
|
|
8
|
+
/** Block body with the trailing newline stripped. */
|
|
9
|
+
code: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A runnable command found in the transcript. */
|
|
13
|
+
export interface LastCommand {
|
|
14
|
+
kind: "bash" | "eval";
|
|
15
|
+
code: string;
|
|
16
|
+
/** Highlight language: "bash" for bash, "python"/"javascript" for eval. */
|
|
17
|
+
language: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A node in the `/copy` picker tree. Leaves carry `content` (placed on the
|
|
22
|
+
* clipboard) plus `copyMessage` (the status shown afterwards); groups carry
|
|
23
|
+
* `children` to drill into.
|
|
24
|
+
*/
|
|
25
|
+
export interface CopyTarget {
|
|
26
|
+
/** Stable identifier (e.g. "msg:1", "msg:1:code:0", "msg:1:all", "cmd:1"). */
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
/** Dim annotation: line/block counts, language, or tool name. */
|
|
30
|
+
hint?: string;
|
|
31
|
+
/** Full text rendered in the preview pane. */
|
|
32
|
+
preview: string;
|
|
33
|
+
/** Highlight language for code/command previews (undefined = plain/markdown). */
|
|
34
|
+
language?: string;
|
|
35
|
+
/** Leaf: text copied to the clipboard. */
|
|
36
|
+
content?: string;
|
|
37
|
+
/** Leaf: status message shown after copying. */
|
|
38
|
+
copyMessage?: string;
|
|
39
|
+
/** Group: nested targets to drill into. */
|
|
40
|
+
children?: CopyTarget[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Minimal session surface needed to assemble copy targets (eases testing). */
|
|
44
|
+
export interface CopySource {
|
|
45
|
+
readonly messages: readonly AgentMessage[];
|
|
46
|
+
getLastVisibleHandoffText(): string | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Cap on how many recent assistant messages the picker lists. */
|
|
50
|
+
const MAX_MESSAGES = 50;
|
|
51
|
+
|
|
52
|
+
const CODE_BLOCK_RE = /^```([^\n]*)\n([\s\S]*?)^```/gm;
|
|
53
|
+
|
|
54
|
+
/** Extract fenced code blocks from assistant markdown, in document order. */
|
|
55
|
+
export function extractCodeBlocks(text: string): CodeBlock[] {
|
|
56
|
+
const blocks: CodeBlock[] = [];
|
|
57
|
+
for (const match of text.matchAll(CODE_BLOCK_RE)) {
|
|
58
|
+
blocks.push({ lang: match[1].trim(), code: match[2].replace(/\n$/, "") });
|
|
59
|
+
}
|
|
60
|
+
return blocks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractEvalCode(args: unknown): { code: string; language: string } | undefined {
|
|
64
|
+
if (!args || typeof args !== "object") return undefined;
|
|
65
|
+
const cells = (args as { cells?: unknown }).cells;
|
|
66
|
+
if (!Array.isArray(cells)) return undefined;
|
|
67
|
+
|
|
68
|
+
const codeBlocks: string[] = [];
|
|
69
|
+
let language = "python";
|
|
70
|
+
let languageResolved = false;
|
|
71
|
+
for (const cell of cells) {
|
|
72
|
+
if (!cell || typeof cell !== "object") continue;
|
|
73
|
+
const code = (cell as { code?: unknown }).code;
|
|
74
|
+
if (typeof code !== "string" || code.length === 0) continue;
|
|
75
|
+
codeBlocks.push(code);
|
|
76
|
+
if (!languageResolved) {
|
|
77
|
+
language = (cell as { language?: unknown }).language === "js" ? "javascript" : "python";
|
|
78
|
+
languageResolved = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return codeBlocks.length > 0 ? { code: codeBlocks.join("\n\n"), language } : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function commandFromToolCall(tc: ToolCall): LastCommand | undefined {
|
|
86
|
+
if (tc.name === "bash" && typeof tc.arguments.command === "string") {
|
|
87
|
+
return { kind: "bash", code: tc.arguments.command, language: "bash" };
|
|
88
|
+
}
|
|
89
|
+
if (tc.name === "eval") {
|
|
90
|
+
const evalResult = extractEvalCode(tc.arguments);
|
|
91
|
+
if (evalResult) return { kind: "eval", code: evalResult.code, language: evalResult.language };
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Walk the transcript backwards for the most recent bash command or eval code. */
|
|
97
|
+
export function extractLastCommand(messages: readonly AgentMessage[]): LastCommand | undefined {
|
|
98
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
99
|
+
const msg = messages[i];
|
|
100
|
+
if (msg.role !== "assistant") continue;
|
|
101
|
+
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
102
|
+
for (let j = toolCalls.length - 1; j >= 0; j--) {
|
|
103
|
+
const command = commandFromToolCall(toolCalls[j]!);
|
|
104
|
+
if (command) return command;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Concatenated visible text of an assistant message, or undefined when empty. */
|
|
111
|
+
function assistantText(msg: AgentMessage): string | undefined {
|
|
112
|
+
if (msg.role !== "assistant") return undefined;
|
|
113
|
+
let text = "";
|
|
114
|
+
for (const content of msg.content) {
|
|
115
|
+
if (content.type === "text") text += content.text;
|
|
116
|
+
}
|
|
117
|
+
return text.trim() || undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pluralLines(text: string): string {
|
|
121
|
+
const count = text.length === 0 ? 0 : text.split("\n").length;
|
|
122
|
+
return `${count} line${count === 1 ? "" : "s"}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function blockHint(block: CodeBlock): string {
|
|
126
|
+
const lines = pluralLines(block.code);
|
|
127
|
+
return block.lang ? `${block.lang} · ${lines}` : lines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** First non-empty line, whitespace-collapsed, used as a message label. */
|
|
131
|
+
function firstLine(text: string): string {
|
|
132
|
+
for (const line of text.split("\n")) {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (trimmed) return trimmed.replace(/\s+/g, " ");
|
|
135
|
+
}
|
|
136
|
+
return text.trim().replace(/\s+/g, " ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Build the target node for one assistant message: a leaf when it has no code
|
|
140
|
+
* blocks, otherwise a group exposing the full message, each block, and "all". */
|
|
141
|
+
function messageTarget(text: string, rank: number): CopyTarget {
|
|
142
|
+
const id = `msg:${rank}`;
|
|
143
|
+
const label = firstLine(text);
|
|
144
|
+
const blocks = extractCodeBlocks(text);
|
|
145
|
+
const hint = blocks.length > 0 ? `${pluralLines(text)} · ${blocks.length} code` : pluralLines(text);
|
|
146
|
+
const messageCopy = rank === 1 ? "Copied last message to clipboard" : "Copied message to clipboard";
|
|
147
|
+
|
|
148
|
+
if (blocks.length === 0) {
|
|
149
|
+
return { id, label, hint, preview: text, content: text, copyMessage: messageCopy };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The message node itself copies the full message; its code blocks are
|
|
153
|
+
// child copy targets you can expand into.
|
|
154
|
+
const children: CopyTarget[] = blocks.map((block, j) => ({
|
|
155
|
+
id: `${id}:code:${j}`,
|
|
156
|
+
label: `Block ${j + 1}`,
|
|
157
|
+
hint: blockHint(block),
|
|
158
|
+
preview: block.code,
|
|
159
|
+
language: block.lang || undefined,
|
|
160
|
+
content: block.code,
|
|
161
|
+
copyMessage: `Copied code block ${j + 1} to clipboard`,
|
|
162
|
+
}));
|
|
163
|
+
if (blocks.length > 1) {
|
|
164
|
+
const combined = blocks.map(b => b.code).join("\n\n");
|
|
165
|
+
children.push({
|
|
166
|
+
id: `${id}:all`,
|
|
167
|
+
label: `All ${blocks.length} blocks`,
|
|
168
|
+
hint: pluralLines(combined),
|
|
169
|
+
preview: combined,
|
|
170
|
+
content: combined,
|
|
171
|
+
copyMessage: `Copied ${blocks.length} code blocks to clipboard`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { id, label, hint, preview: text, content: text, copyMessage: messageCopy, children };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function commandTitle(command: LastCommand): string {
|
|
179
|
+
return command.kind === "bash" ? "Bash command" : "Eval code";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function commandTarget(command: LastCommand, rank: number): CopyTarget {
|
|
183
|
+
const title = commandTitle(command);
|
|
184
|
+
return {
|
|
185
|
+
id: `cmd:${rank}`,
|
|
186
|
+
label: firstLine(command.code) || title,
|
|
187
|
+
hint: `${command.kind} · ${pluralLines(command.code)}`,
|
|
188
|
+
preview: command.code,
|
|
189
|
+
language: command.language,
|
|
190
|
+
content: command.code,
|
|
191
|
+
copyMessage: `Copied ${command.kind === "bash" ? "bash command" : "eval code"} to clipboard`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Assemble the unified `/copy` target tree: recent assistant messages
|
|
197
|
+
* (most recent first, each drillable into its code blocks), runnable command
|
|
198
|
+
* targets interleaved after the assistant message that issued them, and a
|
|
199
|
+
* fresh-handoff fallback when no assistant message exists yet.
|
|
200
|
+
*/
|
|
201
|
+
export function buildCopyTargets(source: CopySource): CopyTarget[] {
|
|
202
|
+
const targets: CopyTarget[] = [];
|
|
203
|
+
const pendingCommands: LastCommand[] = [];
|
|
204
|
+
let messageRank = 0;
|
|
205
|
+
let commandRank = 0;
|
|
206
|
+
|
|
207
|
+
const appendCommands = (commands: readonly LastCommand[]) => {
|
|
208
|
+
for (const command of commands) {
|
|
209
|
+
commandRank += 1;
|
|
210
|
+
targets.push(commandTarget(command, commandRank));
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
for (let i = source.messages.length - 1; i >= 0 && messageRank < MAX_MESSAGES; i--) {
|
|
215
|
+
const msg = source.messages[i];
|
|
216
|
+
if (msg.role !== "assistant") continue;
|
|
217
|
+
|
|
218
|
+
const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall");
|
|
219
|
+
const commands: LastCommand[] = [];
|
|
220
|
+
for (let j = toolCalls.length - 1; j >= 0; j--) {
|
|
221
|
+
const command = commandFromToolCall(toolCalls[j]!);
|
|
222
|
+
if (command) commands.push(command);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const text = assistantText(msg);
|
|
226
|
+
if (!text) {
|
|
227
|
+
pendingCommands.push(...commands);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
messageRank += 1;
|
|
232
|
+
targets.push(messageTarget(text, messageRank));
|
|
233
|
+
appendCommands(pendingCommands);
|
|
234
|
+
appendCommands(commands);
|
|
235
|
+
pendingCommands.length = 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (messageRank === 0) {
|
|
239
|
+
const handoff = source.getLastVisibleHandoffText();
|
|
240
|
+
if (handoff) {
|
|
241
|
+
targets.unshift({
|
|
242
|
+
id: "handoff",
|
|
243
|
+
label: "Handoff context",
|
|
244
|
+
hint: pluralLines(handoff),
|
|
245
|
+
preview: handoff,
|
|
246
|
+
content: handoff,
|
|
247
|
+
copyMessage: "Copied handoff context to clipboard",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
appendCommands(pendingCommands);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return targets;
|
|
254
|
+
}
|
|
@@ -14,7 +14,7 @@ Performs structural AST-aware rewrites via native ast-grep.
|
|
|
14
14
|
</instruction>
|
|
15
15
|
|
|
16
16
|
<output>
|
|
17
|
-
- Replacement summary, per-file replacement counts, and change diffs as
|
|
17
|
+
- Replacement summary, per-file replacement counts, and change diffs as `[src/foo.ts#1A2B]`, `-12:before`, `+12:after` lines in hashline mode
|
|
18
18
|
- Parse issues when files cannot be processed
|
|
19
19
|
</output>
|
|
20
20
|
|
|
@@ -18,7 +18,7 @@ Performs structural code search using AST matching via native ast-grep.
|
|
|
18
18
|
|
|
19
19
|
<output>
|
|
20
20
|
- Grouped matches with file path, byte range, line/column ranges, metavariable captures
|
|
21
|
-
- Match lines are numbered under a file snapshot tag header in hashline mode:
|
|
21
|
+
- Match lines are numbered under a file snapshot tag header in hashline mode: `[src/foo.ts#1A2B]`, `*42:content` for the matched line, ` 43:content` for context
|
|
22
22
|
- Summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
|
|
23
23
|
</output>
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ Append `:<sel>` to `path`. The bare path falls back to the default mode.
|
|
|
28
28
|
|
|
29
29
|
- Reading a directory path returns a depth-limited dirent listing.
|
|
30
30
|
{{#if IS_HL_MODE}}
|
|
31
|
-
- Reading a file with an explicit selector emits a file snapshot tag header and numbered lines:
|
|
31
|
+
- Reading a file with an explicit selector emits a file snapshot tag header and numbered lines: `[src/foo.ts#1A2B]` then `41:def alpha():`. Copy the `[PATH#TAG]` header for anchored edits; ops use bare line numbers. NEVER fabricate the tag.
|
|
32
32
|
{{else}}
|
|
33
33
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
34
34
|
- Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`.
|
|
@@ -9,7 +9,7 @@ Searches files using powerful regex matching.
|
|
|
9
9
|
|
|
10
10
|
<output>
|
|
11
11
|
{{#if IS_HL_MODE}}
|
|
12
|
-
- Text output emits a file snapshot tag header per matched file plus numbered lines:
|
|
12
|
+
- Text output emits a file snapshot tag header per matched file plus numbered lines: `[src/login.ts#1A2B]`, `*42:if (user.id) {` (match), ` 43:return user;` (context). Copy the header for anchored edits; ops use bare line numbers.
|
|
13
13
|
{{else}}
|
|
14
14
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
15
15
|
- Text output is line-number-prefixed
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
extractRetryHint,
|
|
92
92
|
getAgentDbPath,
|
|
93
93
|
getInstallId,
|
|
94
|
+
isBunTestRuntime,
|
|
94
95
|
isEnoent,
|
|
95
96
|
isUnexpectedSocketCloseMessage,
|
|
96
97
|
logger,
|
|
@@ -1036,6 +1037,7 @@ export class AgentSession {
|
|
|
1036
1037
|
|
|
1037
1038
|
#acquirePowerAssertion(): void {
|
|
1038
1039
|
if (process.platform !== "darwin") return;
|
|
1040
|
+
if (isBunTestRuntime()) return;
|
|
1039
1041
|
if (this.#powerAssertion) return;
|
|
1040
1042
|
const idle = this.settings.get("power.preventIdleSleep");
|
|
1041
1043
|
const system = this.settings.get("power.preventSystemSleep");
|
|
@@ -8136,8 +8138,10 @@ export class AgentSession {
|
|
|
8136
8138
|
|
|
8137
8139
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
8138
8140
|
if (!switchedCredential && currentSelector) {
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
+
if (retrySettings.modelFallback) {
|
|
8142
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
8143
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector);
|
|
8144
|
+
}
|
|
8141
8145
|
if (switchedModel) {
|
|
8142
8146
|
delayMs = 0;
|
|
8143
8147
|
} else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
|
@@ -392,17 +392,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
392
392
|
},
|
|
393
393
|
{
|
|
394
394
|
name: "copy",
|
|
395
|
-
description: "
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
{ name: "code", description: "Copy last code block" },
|
|
399
|
-
{ name: "all", description: "Copy all code blocks from last message" },
|
|
400
|
-
{ name: "cmd", description: "Copy last bash/python command" },
|
|
401
|
-
],
|
|
402
|
-
allowArgs: true,
|
|
403
|
-
handleTui: async (command, runtime) => {
|
|
404
|
-
const sub = command.args.trim().toLowerCase() || undefined;
|
|
405
|
-
await runtime.ctx.handleCopyCommand(sub);
|
|
395
|
+
description: "Pick text or code from the conversation to copy",
|
|
396
|
+
handleTui: (_command, runtime) => {
|
|
397
|
+
runtime.ctx.showCopySelector();
|
|
406
398
|
runtime.ctx.editor.setText("");
|
|
407
399
|
},
|
|
408
400
|
},
|
package/src/task/render.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
13
13
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
14
14
|
import type { Theme } from "../modes/theme/theme";
|
|
15
15
|
import {
|
|
16
|
+
capPreviewLines,
|
|
16
17
|
formatBadge,
|
|
17
18
|
formatDuration,
|
|
18
19
|
formatMoreItems,
|
|
@@ -117,6 +118,26 @@ function normalizeReportFindings(value: unknown): ReportFindingDetails[] {
|
|
|
117
118
|
return findings;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Normalize the `yield` slot of `extractedToolData` into an array of
|
|
123
|
+
* yield-detail records. The subprocess executor always populates this slot as
|
|
124
|
+
* `unknown[]` (see `executor.ts` `extractData` handler), but the renderer
|
|
125
|
+
* MUST also tolerate a stray single object — optional chaining short-circuits
|
|
126
|
+
* on `null`/`undefined` only, so calling `.map` on a plain object would throw
|
|
127
|
+
* `TypeError: completeData?.map is not a function` and crash the TUI.
|
|
128
|
+
* A single object is wrapped as a 1-element array so the review verdict still
|
|
129
|
+
* renders; non-object primitives drop out.
|
|
130
|
+
*/
|
|
131
|
+
function normalizeYieldData(value: unknown): Array<{ data: unknown }> {
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.filter((item): item is { data: unknown } => item !== null && typeof item === "object");
|
|
134
|
+
}
|
|
135
|
+
if (value !== null && typeof value === "object") {
|
|
136
|
+
return [value as { data: unknown }];
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
121
142
|
if (value === null) return "null";
|
|
122
143
|
if (typeof value === "string") {
|
|
@@ -541,10 +562,11 @@ export function renderCall(
|
|
|
541
562
|
|
|
542
563
|
if (hasContext) {
|
|
543
564
|
lines.push(` ${branch} ${theme.fg("dim", "Context")}`);
|
|
544
|
-
|
|
565
|
+
const contextLines = context.split("\n").map(line => {
|
|
545
566
|
const content = line ? theme.fg("muted", replaceTabs(line)) : "";
|
|
546
|
-
|
|
547
|
-
}
|
|
567
|
+
return ` ${vertical} ${content}`;
|
|
568
|
+
});
|
|
569
|
+
lines.push(...capPreviewLines(contextLines, theme, { expanded: options.expanded, prefix: ` ${vertical} ` }));
|
|
548
570
|
}
|
|
549
571
|
|
|
550
572
|
// `Tasks` is the last child unless the isolation flag follows it.
|
|
@@ -671,12 +693,12 @@ function renderAgentProgress(
|
|
|
671
693
|
if (progress.extractedToolData) {
|
|
672
694
|
// For completed tasks, check for review verdict from yield tool
|
|
673
695
|
if (progress.status === "completed") {
|
|
674
|
-
const completeData = progress.extractedToolData.yield
|
|
696
|
+
const completeData = normalizeYieldData(progress.extractedToolData.yield);
|
|
675
697
|
const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
|
|
676
698
|
const reviewData = completeData
|
|
677
|
-
|
|
699
|
+
.map(c => c.data as SubmitReviewDetails)
|
|
678
700
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
679
|
-
if (reviewData
|
|
701
|
+
if (reviewData.length > 0) {
|
|
680
702
|
const summary = reviewData[reviewData.length - 1];
|
|
681
703
|
const findings = reportFindingData;
|
|
682
704
|
lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
|
|
@@ -912,16 +934,21 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
912
934
|
);
|
|
913
935
|
}
|
|
914
936
|
// Check for review result (yield with review schema + report_finding)
|
|
915
|
-
|
|
937
|
+
// Check for review result (yield with review schema + report_finding).
|
|
938
|
+
// `normalizeYieldData` guards against a stray non-array `yield` slot —
|
|
939
|
+
// optional chaining on `.map` only short-circuits on null/undefined and
|
|
940
|
+
// would otherwise crash the renderer with `TypeError: completeData?.map
|
|
941
|
+
// is not a function` when the slot is a plain object (see issue #1987).
|
|
942
|
+
const completeData = normalizeYieldData(result.extractedToolData?.yield);
|
|
916
943
|
const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
|
|
917
944
|
|
|
918
945
|
// Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
|
|
919
946
|
const reviewData = completeData
|
|
920
|
-
|
|
947
|
+
.map(c => c.data as SubmitReviewDetails)
|
|
921
948
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
922
|
-
const submitReviewData = reviewData
|
|
949
|
+
const submitReviewData = reviewData.length > 0 ? reviewData : undefined;
|
|
923
950
|
|
|
924
|
-
if (submitReviewData
|
|
951
|
+
if (submitReviewData) {
|
|
925
952
|
// Use combined review renderer
|
|
926
953
|
const summary = submitReviewData[submitReviewData.length - 1];
|
|
927
954
|
const findings = reportFindingData;
|
|
@@ -929,7 +956,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
929
956
|
return lines;
|
|
930
957
|
}
|
|
931
958
|
if (reportFindingData.length > 0) {
|
|
932
|
-
const hasCompleteData = completeData
|
|
959
|
+
const hasCompleteData = completeData.length > 0;
|
|
933
960
|
const message = hasCompleteData
|
|
934
961
|
? "Review verdict missing expected fields"
|
|
935
962
|
: "Review incomplete (yield not called)";
|
package/src/tools/bash.ts
CHANGED
|
@@ -20,7 +20,7 @@ import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
|
20
20
|
import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
|
|
21
21
|
import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
|
|
22
22
|
import { renderStatusLine } from "../tui";
|
|
23
|
-
import { CachedOutputBlock } from "../tui/output-block";
|
|
23
|
+
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
24
24
|
import { getSixelLineMask } from "../utils/sixel";
|
|
25
25
|
import type { ToolSession } from ".";
|
|
26
26
|
import { truncateForPrompt } from "./approval";
|
|
@@ -31,7 +31,7 @@ import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
|
31
31
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
32
32
|
import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
|
|
33
33
|
import { resolveToCwd } from "./path-utils";
|
|
34
|
-
import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
|
|
34
|
+
import { capPreviewLines, formatToolWorkingDirectory, replaceTabs } from "./render-utils";
|
|
35
35
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
36
36
|
import { toolResult } from "./tool-result";
|
|
37
37
|
import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
|
@@ -1083,16 +1083,22 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1083
1083
|
const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
|
|
1084
1084
|
const header = renderStatusLine({ icon: "pending", title }, uiTheme);
|
|
1085
1085
|
const outputBlock = new CachedOutputBlock();
|
|
1086
|
-
return {
|
|
1086
|
+
return markFramedBlockComponent({
|
|
1087
1087
|
render: (width: number): string[] =>
|
|
1088
1088
|
outputBlock.render(
|
|
1089
|
-
{
|
|
1089
|
+
{
|
|
1090
|
+
header,
|
|
1091
|
+
state: "pending",
|
|
1092
|
+
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
|
|
1093
|
+
width,
|
|
1094
|
+
animate: true,
|
|
1095
|
+
},
|
|
1090
1096
|
uiTheme,
|
|
1091
1097
|
),
|
|
1092
1098
|
invalidate: () => {
|
|
1093
1099
|
outputBlock.invalidate();
|
|
1094
1100
|
},
|
|
1095
|
-
};
|
|
1101
|
+
});
|
|
1096
1102
|
},
|
|
1097
1103
|
|
|
1098
1104
|
renderResult(
|
|
@@ -1114,7 +1120,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1114
1120
|
const details = result.details;
|
|
1115
1121
|
const outputBlock = new CachedOutputBlock();
|
|
1116
1122
|
|
|
1117
|
-
return {
|
|
1123
|
+
return markFramedBlockComponent({
|
|
1118
1124
|
render: (width: number): string[] => {
|
|
1119
1125
|
// REACTIVE: read mutable options at render time
|
|
1120
1126
|
const { renderContext } = options;
|
|
@@ -1201,7 +1207,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1201
1207
|
header,
|
|
1202
1208
|
state: options.isPartial ? "pending" : isError ? "error" : "success",
|
|
1203
1209
|
sections: [
|
|
1204
|
-
{
|
|
1210
|
+
{
|
|
1211
|
+
lines: options.isPartial
|
|
1212
|
+
? capPreviewLines(cmdLines ?? [], uiTheme, { expanded })
|
|
1213
|
+
: (cmdLines ?? []),
|
|
1214
|
+
},
|
|
1205
1215
|
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
1206
1216
|
],
|
|
1207
1217
|
width,
|
|
@@ -1213,7 +1223,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1213
1223
|
invalidate: () => {
|
|
1214
1224
|
outputBlock.invalidate();
|
|
1215
1225
|
},
|
|
1216
|
-
};
|
|
1226
|
+
});
|
|
1217
1227
|
},
|
|
1218
1228
|
mergeCallAndResult: true,
|
|
1219
1229
|
inline: true,
|
|
@@ -9,7 +9,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
11
11
|
import type { Theme } from "../../modes/theme/theme";
|
|
12
|
-
import { Hasher, renderCodeCell, renderStatusLine } from "../../tui";
|
|
12
|
+
import { Hasher, isFramedBlockComponent, markFramedBlockComponent, renderCodeCell, renderStatusLine } from "../../tui";
|
|
13
13
|
import type { BrowserToolDetails } from "../browser";
|
|
14
14
|
import { formatStyledTruncationWarning, stripOutputNotice } from "../output-meta";
|
|
15
15
|
import { replaceTabs, shortenPath } from "../render-utils";
|
|
@@ -65,13 +65,14 @@ function dropTrailingBlankLines(text: string): string {
|
|
|
65
65
|
|
|
66
66
|
function appendLine(component: Component, line: string | undefined): Component {
|
|
67
67
|
if (!line) return component;
|
|
68
|
-
|
|
68
|
+
const wrapped = {
|
|
69
69
|
render: (width: number): string[] => {
|
|
70
70
|
const base = component.render(width);
|
|
71
71
|
return [...base, line];
|
|
72
72
|
},
|
|
73
73
|
invalidate: () => component.invalidate?.(),
|
|
74
74
|
};
|
|
75
|
+
return isFramedBlockComponent(component) ? markFramedBlockComponent(wrapped) : wrapped;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function renderRunCell(
|
|
@@ -93,7 +94,7 @@ function renderRunCell(
|
|
|
93
94
|
const title = titleParts.join(" · ");
|
|
94
95
|
|
|
95
96
|
let cached: { key: bigint; width: number; lines: string[] } | undefined;
|
|
96
|
-
return {
|
|
97
|
+
return markFramedBlockComponent({
|
|
97
98
|
render: (width: number): string[] => {
|
|
98
99
|
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
99
100
|
const previewLines = options.renderContext?.previewLines ?? BROWSER_DEFAULT_PREVIEW_LINES;
|
|
@@ -131,7 +132,7 @@ function renderRunCell(
|
|
|
131
132
|
invalidate: () => {
|
|
132
133
|
cached = undefined;
|
|
133
134
|
},
|
|
134
|
-
};
|
|
135
|
+
});
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
function renderOpenOrCloseLine(
|
package/src/tools/debug.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import type { Theme } from "../modes/theme/theme";
|
|
37
37
|
import debugDescription from "../prompts/tools/debug.md" with { type: "text" };
|
|
38
38
|
import { renderStatusLine } from "../tui";
|
|
39
|
-
import { CachedOutputBlock } from "../tui/output-block";
|
|
39
|
+
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
40
40
|
import type { ToolSession } from ".";
|
|
41
41
|
import { truncateForPrompt } from "./approval";
|
|
42
42
|
import type { OutputMeta } from "./output-meta";
|
|
@@ -581,7 +581,7 @@ export const debugToolRenderer = {
|
|
|
581
581
|
args?: DebugRenderArgs,
|
|
582
582
|
): Component {
|
|
583
583
|
const outputBlock = new CachedOutputBlock();
|
|
584
|
-
return {
|
|
584
|
+
return markFramedBlockComponent({
|
|
585
585
|
render(width: number): string[] {
|
|
586
586
|
const action = (args?.action ?? result.details?.action ?? "debug").replaceAll("_", " ");
|
|
587
587
|
const status = options.isPartial ? "running" : result.isError ? "error" : "success";
|
|
@@ -620,7 +620,7 @@ export const debugToolRenderer = {
|
|
|
620
620
|
invalidate() {
|
|
621
621
|
outputBlock.invalidate();
|
|
622
622
|
},
|
|
623
|
-
};
|
|
623
|
+
});
|
|
624
624
|
},
|
|
625
625
|
mergeCallAndResult: true,
|
|
626
626
|
inline: true,
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { formatContextUsage } from "../modes/components/status-line/context-thre
|
|
|
18
18
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
19
19
|
import { shimmerEnabled } from "../modes/theme/shimmer";
|
|
20
20
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
21
|
-
import { borderShimmerTick, renderCodeCell } from "../tui";
|
|
21
|
+
import { borderShimmerTick, markFramedBlockComponent, renderCodeCell } from "../tui";
|
|
22
22
|
import {
|
|
23
23
|
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
24
24
|
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
@@ -39,8 +39,15 @@ import {
|
|
|
39
39
|
truncateToWidth,
|
|
40
40
|
wrapBrackets,
|
|
41
41
|
} from "./render-utils";
|
|
42
|
-
|
|
43
42
|
export const EVAL_DEFAULT_PREVIEW_LINES = 10;
|
|
43
|
+
/**
|
|
44
|
+
* Rows of source kept in the *pending* eval preview. The window follows the
|
|
45
|
+
* streaming edge (newest lines pinned to the bottom) so you can watch the code
|
|
46
|
+
* being written, while staying bounded — a volatile tool block taller than the
|
|
47
|
+
* viewport would otherwise strand its scrolled-off head out of native scrollback
|
|
48
|
+
* on ED3-risk terminals. Matches the streaming windows used by edit/write.
|
|
49
|
+
*/
|
|
50
|
+
export const EVAL_STREAMING_PREVIEW_LINES = 12;
|
|
44
51
|
|
|
45
52
|
function languageForHighlighter(language: EvalLanguage | undefined): "python" | "javascript" {
|
|
46
53
|
return language === "js" ? "javascript" : "python";
|
|
@@ -490,10 +497,10 @@ export const evalToolRenderer = {
|
|
|
490
497
|
|
|
491
498
|
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
492
499
|
|
|
493
|
-
return {
|
|
500
|
+
return markFramedBlockComponent({
|
|
494
501
|
render: (width: number): string[] => {
|
|
495
502
|
const animate = options.isPartial && shimmerEnabled();
|
|
496
|
-
const key = `${animate ? borderShimmerTick() : 0}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
|
|
503
|
+
const key = `${animate ? borderShimmerTick() : 0}|${options.expanded ? 1 : 0}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
|
|
497
504
|
if (cached && cached.key === key && cached.width === width) {
|
|
498
505
|
return cached.result;
|
|
499
506
|
}
|
|
@@ -510,8 +517,16 @@ export const evalToolRenderer = {
|
|
|
510
517
|
title: cell.title,
|
|
511
518
|
status: "pending",
|
|
512
519
|
width,
|
|
513
|
-
codeMaxLines:
|
|
514
|
-
|
|
520
|
+
codeMaxLines: EVAL_STREAMING_PREVIEW_LINES,
|
|
521
|
+
// Follow the streaming edge with a bounded tail window so the
|
|
522
|
+
// newest source stays visible as it is written, instead of
|
|
523
|
+
// rendering every line of a >100-line `code` — which would
|
|
524
|
+
// overflow the viewport and, because a tool block is volatile
|
|
525
|
+
// (it collapses to a capped result), strand its scrolled-off head
|
|
526
|
+
// out of native scrollback, cutting the box top. `Ctrl+O` lifts
|
|
527
|
+
// the window via `expanded` for a deliberate full view.
|
|
528
|
+
codeTail: true,
|
|
529
|
+
expanded: options.expanded,
|
|
515
530
|
animate,
|
|
516
531
|
},
|
|
517
532
|
uiTheme,
|
|
@@ -527,7 +542,7 @@ export const evalToolRenderer = {
|
|
|
527
542
|
invalidate: () => {
|
|
528
543
|
cached = undefined;
|
|
529
544
|
},
|
|
530
|
-
};
|
|
545
|
+
});
|
|
531
546
|
},
|
|
532
547
|
|
|
533
548
|
renderResult(
|
|
@@ -571,7 +586,7 @@ export const evalToolRenderer = {
|
|
|
571
586
|
if (cellResults && cellResults.length > 0) {
|
|
572
587
|
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
573
588
|
|
|
574
|
-
return {
|
|
589
|
+
return markFramedBlockComponent({
|
|
575
590
|
render: (width: number): string[] => {
|
|
576
591
|
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
577
592
|
const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
|
|
@@ -649,7 +664,7 @@ export const evalToolRenderer = {
|
|
|
649
664
|
invalidate: () => {
|
|
650
665
|
cached = undefined;
|
|
651
666
|
},
|
|
652
|
-
};
|
|
667
|
+
});
|
|
653
668
|
}
|
|
654
669
|
|
|
655
670
|
const displayOutput = output;
|