@oh-my-pi/pi-coding-agent 15.13.1 → 15.13.3
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 +47 -0
- package/dist/cli.js +1057 -289
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/models-config.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +97 -0
- package/dist/types/edit/hashline/block-resolver.d.ts +1 -1
- package/dist/types/edit/index.d.ts +2 -0
- package/dist/types/eval/js/context-manager.d.ts +15 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +4 -4
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +2 -1
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/session-dump-format.d.ts +2 -1
- package/dist/types/session/unexpected-stop-classifier.d.ts +13 -0
- package/dist/types/stt/asr-client.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +11 -0
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +2 -0
- package/dist/types/tools/ast-edit.d.ts +2 -0
- package/dist/types/tools/ast-grep.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +2 -0
- package/dist/types/tools/find.d.ts +2 -0
- package/dist/types/tools/inspect-image.d.ts +2 -1
- package/dist/types/tools/irc.d.ts +2 -0
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tts/tts-client.d.ts +1 -1
- package/dist/types/tui/tree-list.d.ts +1 -0
- package/dist/types/utils/thinking-display.d.ts +1 -17
- package/package.json +12 -12
- package/src/cli.ts +25 -12
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/models-config.ts +1 -0
- package/src/config/settings-schema.ts +78 -0
- package/src/edit/hashline/block-resolver.ts +1 -1
- package/src/edit/hashline/execute.ts +1 -6
- package/src/edit/index.ts +48 -0
- package/src/eval/__tests__/agent-bridge.test.ts +106 -46
- package/src/eval/__tests__/js-context-manager.test.ts +53 -3
- package/src/eval/js/context-manager.ts +132 -29
- package/src/eval/js/worker-core.ts +1 -1
- package/src/eval/js/worker-entry.ts +7 -0
- package/src/export/html/template.js +18 -22
- package/src/internal-urls/docs-index.generated.ts +12 -3
- package/src/main.ts +15 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/agent-hub.ts +31 -7
- package/src/modes/components/assistant-message.ts +24 -15
- package/src/modes/components/snapcompact-shape-preview-doc.md +2 -2
- package/src/modes/components/snapcompact-shape-preview.ts +2 -2
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/components/welcome.ts +14 -4
- package/src/modes/controllers/event-controller.ts +3 -3
- package/src/modes/controllers/input-controller.ts +28 -39
- package/src/modes/controllers/streaming-reveal.ts +4 -4
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/prompts/agents/oracle.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/system-prompt.md +17 -21
- package/src/prompts/system/unexpected-stop-classifier.md +17 -0
- package/src/prompts/system/unexpected-stop-retry.md +4 -0
- package/src/prompts/tools/ask.md +0 -8
- package/src/prompts/tools/ast-edit.md +0 -15
- package/src/prompts/tools/ast-grep.md +0 -13
- package/src/prompts/tools/browser.md +0 -21
- package/src/prompts/tools/debug.md +0 -13
- package/src/prompts/tools/eval.md +0 -9
- package/src/prompts/tools/find.md +0 -13
- package/src/prompts/tools/inspect-image.md +0 -9
- package/src/prompts/tools/irc.md +0 -15
- package/src/prompts/tools/patch.md +0 -13
- package/src/prompts/tools/ssh.md +0 -9
- package/src/prompts/tools/todo.md +1 -19
- package/src/sdk.ts +19 -0
- package/src/session/agent-session.ts +289 -29
- package/src/session/session-dump-format.ts +17 -49
- package/src/session/unexpected-stop-classifier.ts +129 -0
- package/src/stt/asr-client.ts +1 -1
- package/src/system-prompt.ts +31 -0
- package/src/tiny/title-client.ts +1 -1
- package/src/tools/ask.ts +41 -0
- package/src/tools/ast-edit.ts +46 -0
- package/src/tools/ast-grep.ts +24 -0
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser/tab-worker-entry.ts +12 -4
- package/src/tools/browser.ts +52 -0
- package/src/tools/debug.ts +17 -0
- package/src/tools/eval.ts +20 -1
- package/src/tools/find.ts +24 -0
- package/src/tools/inspect-image.ts +27 -1
- package/src/tools/irc.ts +41 -0
- package/src/tools/job.ts +1 -0
- package/src/tools/ssh.ts +16 -0
- package/src/tools/todo.ts +82 -3
- package/src/tts/tts-client.ts +1 -1
- package/src/tui/tree-list.ts +68 -19
- package/src/utils/thinking-display.ts +8 -34
package/src/main.ts
CHANGED
|
@@ -274,7 +274,19 @@ export async function submitInteractiveInput(
|
|
|
274
274
|
|
|
275
275
|
try {
|
|
276
276
|
using _keepalive = new EventLoopKeepalive();
|
|
277
|
-
|
|
277
|
+
// Honor the submission's queue intent, defaulting to followUp. Reading
|
|
278
|
+
// `session.isStreaming` to decide queue-vs-fresh is NOT atomic with the
|
|
279
|
+
// eventual `agent.prompt()` call inside `session.prompt()`: a background turn
|
|
280
|
+
// (queued-message drain, idle compaction, goal/loop continuation timer) can
|
|
281
|
+
// flip the agent busy in the gap, and a bare prompt() would then throw
|
|
282
|
+
// AgentBusyError straight to an error toast even though the UI shows no
|
|
283
|
+
// "Working…". Passing a behavior unconditionally is a no-op when the session
|
|
284
|
+
// is genuinely idle (a fresh turn runs and the option is ignored) and queues
|
|
285
|
+
// the message instead of erroring when a turn is already underway. Normal
|
|
286
|
+
// user Enter carries "steer" (interrupt, matching the streaming-branch Enter);
|
|
287
|
+
// background/continuation submits omit it and fall back to "followUp". The
|
|
288
|
+
// synthetic branch below opts out by design.
|
|
289
|
+
const streamingBehavior = input.streamingBehavior ?? ("followUp" as const);
|
|
278
290
|
// Continue shortcuts submit an already-started synthetic developer prompt with
|
|
279
291
|
// no optimistic user message.
|
|
280
292
|
if (!input.started && !mode.markPendingSubmissionStarted(input)) {
|
|
@@ -287,9 +299,7 @@ export async function submitInteractiveInput(
|
|
|
287
299
|
display: input.display ?? false,
|
|
288
300
|
attribution: "agent" as const,
|
|
289
301
|
};
|
|
290
|
-
await (streamingBehavior
|
|
291
|
-
? session.promptCustomMessage(message, { streamingBehavior })
|
|
292
|
-
: session.promptCustomMessage(message));
|
|
302
|
+
await session.promptCustomMessage(message, { streamingBehavior });
|
|
293
303
|
} else if (input.synthetic) {
|
|
294
304
|
// Synthetic continue shortcuts are hidden developer prompts. The streaming
|
|
295
305
|
// queue (#queueUserMessage) only carries user-attributed messages, so we do
|
|
@@ -299,7 +309,7 @@ export async function submitInteractiveInput(
|
|
|
299
309
|
// its role.
|
|
300
310
|
await session.prompt(input.text, { synthetic: true, expandPromptTemplates: false });
|
|
301
311
|
} else {
|
|
302
|
-
await session.prompt(input.text, { images: input.images,
|
|
312
|
+
await session.prompt(input.text, { images: input.images, streamingBehavior });
|
|
303
313
|
}
|
|
304
314
|
} catch (error: unknown) {
|
|
305
315
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
@@ -72,7 +72,7 @@ import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
|
|
|
72
72
|
import { normalizeLocalScheme } from "../../tools/path-utils";
|
|
73
73
|
import { runResolveInvocation } from "../../tools/resolve";
|
|
74
74
|
import { ToolError } from "../../tools/tool-errors";
|
|
75
|
-
import {
|
|
75
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
76
76
|
import { createAcpClientBridge } from "./acp-client-bridge";
|
|
77
77
|
import {
|
|
78
78
|
buildToolCallStartUpdate,
|
|
@@ -1907,7 +1907,7 @@ export class AcpAgent implements Agent {
|
|
|
1907
1907
|
continue;
|
|
1908
1908
|
}
|
|
1909
1909
|
if (item.type === "thinking" && "thinking" in item && typeof item.thinking === "string") {
|
|
1910
|
-
const thinking =
|
|
1910
|
+
const thinking = canonicalizeMessage(item.thinking);
|
|
1911
1911
|
if (thinking.length === 0) continue;
|
|
1912
1912
|
notifications.push({
|
|
1913
1913
|
sessionId,
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
10
10
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
11
11
|
import type { TodoStatus } from "../../tools/todo";
|
|
12
|
-
import {
|
|
12
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
13
13
|
|
|
14
14
|
interface MessageProgress {
|
|
15
15
|
textEmitted: boolean;
|
|
@@ -259,7 +259,7 @@ function mapAssistantMessageUpdate(
|
|
|
259
259
|
break;
|
|
260
260
|
case "thinking_delta": {
|
|
261
261
|
const block = event.assistantMessageEvent.partial?.content?.[event.assistantMessageEvent.contentIndex];
|
|
262
|
-
if (block?.type === "thinking" && !
|
|
262
|
+
if (block?.type === "thinking" && !canonicalizeMessage(block.thinking)) return [];
|
|
263
263
|
sessionUpdate = "agent_thought_chunk";
|
|
264
264
|
text = event.assistantMessageEvent.delta;
|
|
265
265
|
if (text.length > 0 && progress) {
|
|
@@ -41,7 +41,7 @@ import type { SessionMessageEntry } from "../../session/session-entries";
|
|
|
41
41
|
import { parseSessionEntries } from "../../session/session-loader";
|
|
42
42
|
import { createIrcMessageCard } from "../../tools/irc";
|
|
43
43
|
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
44
|
-
import {
|
|
44
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
45
45
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
46
46
|
import { getEditorTheme, theme } from "../theme/theme";
|
|
47
47
|
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
@@ -193,6 +193,8 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
193
193
|
#rows: AgentRef[] = [];
|
|
194
194
|
#selectedRow = 0;
|
|
195
195
|
#notice: string | undefined;
|
|
196
|
+
/** Captured row order from the first refresh; keeps the hub stable while open. */
|
|
197
|
+
#rowOrder: Map<string, number> | undefined;
|
|
196
198
|
|
|
197
199
|
// Chat state
|
|
198
200
|
#chatAgentId: string | undefined;
|
|
@@ -349,10 +351,32 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
349
351
|
|
|
350
352
|
#refreshRows(): void {
|
|
351
353
|
const selectedId = this.#rows[this.#selectedRow]?.id;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
const refs = this.#registry.list().filter(ref => ref.id !== MAIN_AGENT_ID);
|
|
355
|
+
|
|
356
|
+
if (!this.#rowOrder) {
|
|
357
|
+
// First refresh (usually the constructor): order by status, then recency.
|
|
358
|
+
this.#rows = refs.sort(
|
|
359
|
+
(a, b) => STATUS_ORDER[a.status] - STATUS_ORDER[b.status] || b.lastActivity - a.lastActivity,
|
|
360
|
+
);
|
|
361
|
+
this.#rowOrder = new Map(this.#rows.map((ref, i) => [ref.id, i]));
|
|
362
|
+
} else {
|
|
363
|
+
// After the hub is open, freeze the relative order so keyboard selection
|
|
364
|
+
// does not jump around as agents heartbeat or update activity. New agents
|
|
365
|
+
// are appended at the end and then stay put.
|
|
366
|
+
this.#rows = refs.sort((a, b) => {
|
|
367
|
+
const statusDiff = STATUS_ORDER[a.status] - STATUS_ORDER[b.status];
|
|
368
|
+
if (statusDiff !== 0) return statusDiff;
|
|
369
|
+
const aOrder = this.#rowOrder!.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
370
|
+
const bOrder = this.#rowOrder!.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
371
|
+
return aOrder - bOrder;
|
|
372
|
+
});
|
|
373
|
+
for (const ref of this.#rows) {
|
|
374
|
+
if (!this.#rowOrder.has(ref.id)) {
|
|
375
|
+
this.#rowOrder.set(ref.id, this.#rowOrder.size);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
356
380
|
const keptIndex = selectedId ? this.#rows.findIndex(ref => ref.id === selectedId) : -1;
|
|
357
381
|
this.#selectedRow = keptIndex >= 0 ? keptIndex : Math.min(this.#selectedRow, Math.max(0, this.#rows.length - 1));
|
|
358
382
|
}
|
|
@@ -1028,8 +1052,8 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
1028
1052
|
|
|
1029
1053
|
const hasVisibleAssistantContent = message.content.some(
|
|
1030
1054
|
content =>
|
|
1031
|
-
(content.type === "text" && content.text
|
|
1032
|
-
(content.type === "thinking" &&
|
|
1055
|
+
(content.type === "text" && canonicalizeMessage(content.text)) ||
|
|
1056
|
+
(content.type === "thinking" && canonicalizeMessage(content.thinking)),
|
|
1033
1057
|
);
|
|
1034
1058
|
if (hasVisibleAssistantContent) {
|
|
1035
1059
|
// New visible turn content closes the current read run (mirrors rebuild).
|
|
@@ -4,7 +4,7 @@ import type { AssistantThinkingRenderer } from "../../extensibility/extensions/t
|
|
|
4
4
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
5
5
|
import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
|
|
6
6
|
import { getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
7
|
-
import {
|
|
7
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Max lines of a turn-ending provider error rendered inline in the transcript.
|
|
@@ -232,9 +232,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
232
232
|
const parts: string[] = [`htb:${this.hideThinkingBlock ? 1 : 0}`];
|
|
233
233
|
for (const content of message.content) {
|
|
234
234
|
if (content.type === "text") {
|
|
235
|
-
parts.push(content.text
|
|
235
|
+
parts.push(canonicalizeMessage(content.text) ? "T1" : "T0");
|
|
236
236
|
} else if (content.type === "thinking") {
|
|
237
|
-
|
|
237
|
+
const canon = canonicalizeMessage(content.thinking);
|
|
238
|
+
if (!canon) parts.push("K0");
|
|
238
239
|
else if (this.hideThinkingBlock) parts.push("KH");
|
|
239
240
|
else parts.push("KV");
|
|
240
241
|
} else {
|
|
@@ -267,7 +268,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
267
268
|
for (const item of this.#fastPathItems) {
|
|
268
269
|
if (item.blockType === "thinking") {
|
|
269
270
|
const content = message.content[item.contentIndex];
|
|
270
|
-
if (content?.type === "thinking" &&
|
|
271
|
+
if (content?.type === "thinking" && canonicalizeMessage(content.thinking) !== item.lastText)
|
|
272
|
+
return false;
|
|
271
273
|
}
|
|
272
274
|
}
|
|
273
275
|
}
|
|
@@ -291,13 +293,17 @@ export class AssistantMessageComponent extends Container {
|
|
|
291
293
|
for (const item of this.#fastPathItems) {
|
|
292
294
|
item.md.transientRenderCache = transient;
|
|
293
295
|
const content = message.content[item.contentIndex];
|
|
296
|
+
if (!content) {
|
|
297
|
+
this.#fastPathKey = undefined;
|
|
298
|
+
this.#fastPathItems = undefined;
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
294
301
|
let newText: string;
|
|
295
|
-
if (item.blockType === "text" && content
|
|
302
|
+
if (item.blockType === "text" && content.type === "text") {
|
|
296
303
|
newText = content.text.trim();
|
|
297
|
-
} else if (item.blockType === "thinking" && content
|
|
298
|
-
newText =
|
|
304
|
+
} else if (item.blockType === "thinking" && content.type === "thinking") {
|
|
305
|
+
newText = canonicalizeMessage(content.thinking);
|
|
299
306
|
} else {
|
|
300
|
-
// Block at this index is gone or changed type (index shift) — fail closed.
|
|
301
307
|
this.#fastPathKey = undefined;
|
|
302
308
|
this.#fastPathItems = undefined;
|
|
303
309
|
return false;
|
|
@@ -329,24 +335,23 @@ export class AssistantMessageComponent extends Container {
|
|
|
329
335
|
|
|
330
336
|
const hasVisibleContent = message.content.some(
|
|
331
337
|
c =>
|
|
332
|
-
(c.type === "text" && c.text
|
|
333
|
-
(!this.hideThinkingBlock && c.type === "thinking" &&
|
|
338
|
+
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
339
|
+
(!this.hideThinkingBlock && c.type === "thinking" && canonicalizeMessage(c.thinking)),
|
|
334
340
|
);
|
|
335
341
|
|
|
336
342
|
// Render content in order
|
|
337
343
|
let thinkingIndex = 0;
|
|
338
344
|
for (let i = 0; i < message.content.length; i++) {
|
|
339
345
|
const content = message.content[i];
|
|
340
|
-
if (content.type === "text" && content.text
|
|
341
|
-
// Assistant text messages with no background - trim the text
|
|
346
|
+
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
342
347
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
343
348
|
const trimmed = content.text.trim();
|
|
344
349
|
const md = new Markdown(trimmed, 1, 0, getMarkdownTheme());
|
|
345
350
|
md.transientRenderCache = this.#lastUpdateTransient;
|
|
346
351
|
this.#contentContainer.addChild(md);
|
|
347
352
|
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
348
|
-
} else if (content.type === "thinking" &&
|
|
349
|
-
const thinkingText =
|
|
353
|
+
} else if (content.type === "thinking" && canonicalizeMessage(content.thinking)) {
|
|
354
|
+
const thinkingText = canonicalizeMessage(content.thinking);
|
|
350
355
|
if (this.hideThinkingBlock) {
|
|
351
356
|
thinkingIndex += 1;
|
|
352
357
|
continue;
|
|
@@ -355,7 +360,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
355
360
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
356
361
|
const hasVisibleContentAfter = message.content
|
|
357
362
|
.slice(i + 1)
|
|
358
|
-
.some(
|
|
363
|
+
.some(
|
|
364
|
+
c =>
|
|
365
|
+
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
366
|
+
(c.type === "thinking" && canonicalizeMessage(c.thinking)),
|
|
367
|
+
);
|
|
359
368
|
|
|
360
369
|
// Thinking traces in thinkingText color, italic
|
|
361
370
|
const md = new Markdown(thinkingText, 1, 0, getMarkdownTheme(), {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[User]: Fix the settings overlay crash. Wheeling past the last row throws.
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[Tool Call]: read(path="src/select-list.ts:140-180")
|
|
4
4
|
|
|
5
|
-
[Tool
|
|
5
|
+
[Tool Result]: 162: const index = Math.floor(line / rowHeight); index is never checked against bounds.
|
|
6
6
|
|
|
7
7
|
[Assistant]: Found it. The hit test indexes past the filtered list; clamping to the last row fixes the crash.
|
|
8
8
|
|
|
@@ -38,10 +38,10 @@ const ZOOM_SCALE = 4;
|
|
|
38
38
|
const MAX_IMAGE_COLS = 28;
|
|
39
39
|
const MAX_IMAGE_ROWS = 14;
|
|
40
40
|
|
|
41
|
-
/** Sample transcript with `[Tool
|
|
41
|
+
/** Sample transcript with `[Tool Result]:` bodies wrapped in dim-ink toggles. */
|
|
42
42
|
const PREVIEW_TEXT = sampleDoc
|
|
43
43
|
.trim()
|
|
44
|
-
.replace(/\[Tool
|
|
44
|
+
.replace(/\[Tool Result\]: ([^[]*)/g, (_match, body: string) => `[Tool Result]: ${DIM_ON}${body}${DIM_OFF}`);
|
|
45
45
|
|
|
46
46
|
type PreviewEntry =
|
|
47
47
|
| { state: "rendering" }
|
|
@@ -18,6 +18,7 @@ import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../m
|
|
|
18
18
|
import type { SessionTreeNode } from "../../session/session-entries";
|
|
19
19
|
import { shortenPath } from "../../tools/render-utils";
|
|
20
20
|
import { toPathList } from "../../tools/search";
|
|
21
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
21
22
|
import { DynamicBorder } from "./dynamic-border";
|
|
22
23
|
|
|
23
24
|
/** Gutter info: position (displayIndent where connector was) and whether to show │ */
|
|
@@ -702,12 +703,12 @@ class TreeList implements Component {
|
|
|
702
703
|
}
|
|
703
704
|
|
|
704
705
|
#hasTextContent(content: unknown): boolean {
|
|
705
|
-
if (typeof content === "string") return content
|
|
706
|
+
if (typeof content === "string") return Boolean(canonicalizeMessage(content));
|
|
706
707
|
if (Array.isArray(content)) {
|
|
707
708
|
for (const c of content) {
|
|
708
709
|
if (typeof c === "object" && c !== null && "type" in c && c.type === "text") {
|
|
709
710
|
const text = (c as { text?: string }).text;
|
|
710
|
-
if (text && text
|
|
711
|
+
if (text && canonicalizeMessage(text)) return true;
|
|
711
712
|
}
|
|
712
713
|
}
|
|
713
714
|
}
|
|
@@ -70,8 +70,7 @@ export interface LspServerInfo {
|
|
|
70
70
|
export class WelcomeComponent implements Component {
|
|
71
71
|
#animStart: number | null = null;
|
|
72
72
|
#animTimer: ReturnType<typeof setInterval> | null = null;
|
|
73
|
-
|
|
74
|
-
readonly #tip: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
|
|
73
|
+
#selectedTip: string | undefined;
|
|
75
74
|
// Render cache: the welcome box is the first transcript-area component, so
|
|
76
75
|
// returning a stable array reference keeps the whole frame prefix stable.
|
|
77
76
|
// Bypassed while the intro animation runs (every frame differs).
|
|
@@ -85,6 +84,16 @@ export class WelcomeComponent implements Component {
|
|
|
85
84
|
private recentSessions: RecentSession[] = [],
|
|
86
85
|
private lspServers: LspServerInfo[] = [],
|
|
87
86
|
) {}
|
|
87
|
+
get tip(): string | undefined {
|
|
88
|
+
if (this.#selectedTip === undefined) {
|
|
89
|
+
if (theme.getSymbolPreset() === "unicode" && Math.random() < 0.1) {
|
|
90
|
+
this.#selectedTip = "Please use nerdfont 😭.";
|
|
91
|
+
} else {
|
|
92
|
+
this.#selectedTip = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : "";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return this.#selectedTip || undefined;
|
|
96
|
+
}
|
|
88
97
|
|
|
89
98
|
invalidate(): void {
|
|
90
99
|
this.#cachedWidth = -1;
|
|
@@ -316,8 +325,9 @@ export class WelcomeComponent implements Component {
|
|
|
316
325
|
* when no tip is available or the box is too narrow to be useful.
|
|
317
326
|
*/
|
|
318
327
|
#renderTip(boxWidth: number): string[] {
|
|
319
|
-
|
|
320
|
-
|
|
328
|
+
const tip = this.tip;
|
|
329
|
+
if (!tip) return [];
|
|
330
|
+
return renderWelcomeTip(tip, boxWidth);
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
/** Center text within a given width */
|
|
@@ -22,7 +22,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
|
|
|
22
22
|
import { isSilentAbort, readQueueChipText, resolveAbortLabel } from "../../session/messages";
|
|
23
23
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
24
24
|
import { vocalizer } from "../../tts/vocalizer";
|
|
25
|
-
import {
|
|
25
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
26
26
|
import { interruptHint } from "../shared";
|
|
27
27
|
import { StreamingRevealController } from "./streaming-reveal";
|
|
28
28
|
import { ToolArgsRevealController } from "./tool-args-reveal";
|
|
@@ -480,8 +480,8 @@ export class EventController {
|
|
|
480
480
|
|
|
481
481
|
const visibleBlockCount = this.ctx.streamingMessage.content.filter(
|
|
482
482
|
content =>
|
|
483
|
-
(content.type === "text" && content.text
|
|
484
|
-
(content.type === "thinking" &&
|
|
483
|
+
(content.type === "text" && canonicalizeMessage(content.text)) ||
|
|
484
|
+
(content.type === "thinking" && canonicalizeMessage(content.thinking)),
|
|
485
485
|
).length;
|
|
486
486
|
if (visibleBlockCount > this.#lastVisibleBlockCount) {
|
|
487
487
|
this.#resetReadGroup();
|
|
@@ -44,26 +44,9 @@ function hasPasteText(value: unknown): value is PasteTarget {
|
|
|
44
44
|
return typeof value === "object" && value !== null && typeof (value as PasteTarget).pasteText === "function";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Wrap pasted text in
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let longestRun = 0;
|
|
51
|
-
let run = 0;
|
|
52
|
-
for (let i = 0; i < content.length; i++) {
|
|
53
|
-
if (content.charCodeAt(i) === 96 /* backtick */) {
|
|
54
|
-
run++;
|
|
55
|
-
if (run > longestRun) longestRun = run;
|
|
56
|
-
} else {
|
|
57
|
-
run = 0;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const fence = "`".repeat(Math.max(3, longestRun + 1));
|
|
61
|
-
return `${fence}\n${content}\n${fence}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Wrap pasted text in `<pasted_text>` tags so the model treats it as one quoted block. */
|
|
65
|
-
function wrapPasteInXml(content: string): string {
|
|
66
|
-
return `<pasted_text>\n${content}\n</pasted_text>`;
|
|
47
|
+
/** Wrap pasted text in `<attachment>` tags so the model treats it as one quoted block. */
|
|
48
|
+
function wrapPasteInAttachmentBlock(content: string): string {
|
|
49
|
+
return `<attachment>\n${content}\n</attachment>`;
|
|
67
50
|
}
|
|
68
51
|
|
|
69
52
|
const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
|
|
@@ -99,8 +82,8 @@ export class InputController {
|
|
|
99
82
|
// (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
|
|
100
83
|
// #detectLeftDoubleTap.
|
|
101
84
|
#leftTapCount = 0;
|
|
102
|
-
// Sequential index for `local://attachment-N` references created by the large-paste
|
|
103
|
-
//
|
|
85
|
+
// Sequential index for `local://attachment-N` references created by the large-paste local-file
|
|
86
|
+
// action. Seeded from 0 and bumped past any existing attachment files in #attachPasteAsFile.
|
|
104
87
|
#attachmentCounter = 0;
|
|
105
88
|
|
|
106
89
|
#showTinyTitleDownloadProgress(modelKey: string): void {
|
|
@@ -706,11 +689,17 @@ export class InputController {
|
|
|
706
689
|
this.ctx.pendingImages = [];
|
|
707
690
|
this.ctx.pendingImageLinks = [];
|
|
708
691
|
|
|
709
|
-
// Render user message immediately, then let session events catch up
|
|
692
|
+
// Render user message immediately, then let session events catch up.
|
|
693
|
+
// Tag the submission as "steer": this is a normal Enter the controller
|
|
694
|
+
// believed was idle, but a background turn can start in the gap before
|
|
695
|
+
// `submitInteractiveInput` dispatches it. Steering matches the
|
|
696
|
+
// streaming-branch Enter (above) and keeps the message from throwing
|
|
697
|
+
// AgentBusyError on that race.
|
|
710
698
|
const submission = this.ctx.startPendingSubmission({
|
|
711
699
|
text,
|
|
712
700
|
images,
|
|
713
701
|
imageLinks: inputImageLinks,
|
|
702
|
+
streamingBehavior: "steer",
|
|
714
703
|
});
|
|
715
704
|
|
|
716
705
|
this.ctx.onInputCallback(submission);
|
|
@@ -1282,24 +1271,24 @@ export class InputController {
|
|
|
1282
1271
|
}
|
|
1283
1272
|
|
|
1284
1273
|
/**
|
|
1285
|
-
* Present the large-paste menu and apply the chosen action: wrap in
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1274
|
+
* Present the large-paste menu and apply the chosen action: wrap in `<attachment>` tags (collapsed
|
|
1275
|
+
* to a `[Paste]` marker that expands on submit), save the text to a file and reference its path so
|
|
1276
|
+
* the agent can `read` it on demand, or paste inline. Cancelling (Esc) falls back to the default
|
|
1277
|
+
* inline paste marker, so the pasted content is never lost.
|
|
1289
1278
|
*/
|
|
1290
1279
|
async presentLargePasteMenu(text: string, lineCount: number): Promise<void> {
|
|
1291
|
-
const
|
|
1292
|
-
const
|
|
1293
|
-
const
|
|
1280
|
+
const WRAPPED_BLOCK = "Attach as a wrapped block";
|
|
1281
|
+
const LOCAL_FILE = "Attach as local file";
|
|
1282
|
+
const INLINE = "Paste inline";
|
|
1294
1283
|
|
|
1295
1284
|
let choice: string | undefined;
|
|
1296
1285
|
try {
|
|
1297
1286
|
choice = await this.ctx.showHookSelector(
|
|
1298
1287
|
`Pasted ${lineCount} lines`,
|
|
1299
1288
|
[
|
|
1300
|
-
{ label:
|
|
1301
|
-
{ label:
|
|
1302
|
-
{ label:
|
|
1289
|
+
{ label: WRAPPED_BLOCK, description: "Wrap the text in <attachment> tags, collapsed to a marker" },
|
|
1290
|
+
{ label: LOCAL_FILE, description: "Save the text to a local://attachment file" },
|
|
1291
|
+
{ label: INLINE, description: "Collapse the text to an inline paste marker" },
|
|
1303
1292
|
],
|
|
1304
1293
|
{ helpText: "Esc to paste inline" },
|
|
1305
1294
|
);
|
|
@@ -1309,15 +1298,15 @@ export class InputController {
|
|
|
1309
1298
|
}
|
|
1310
1299
|
|
|
1311
1300
|
switch (choice) {
|
|
1312
|
-
case
|
|
1313
|
-
this.ctx.editor.insertPaste(
|
|
1314
|
-
break;
|
|
1315
|
-
case XML:
|
|
1316
|
-
this.ctx.editor.insertPaste(wrapPasteInXml(text));
|
|
1301
|
+
case WRAPPED_BLOCK:
|
|
1302
|
+
this.ctx.editor.insertPaste(wrapPasteInAttachmentBlock(text));
|
|
1317
1303
|
break;
|
|
1318
|
-
case
|
|
1304
|
+
case LOCAL_FILE:
|
|
1319
1305
|
await this.#attachPasteAsFile(text, lineCount);
|
|
1320
1306
|
break;
|
|
1307
|
+
case INLINE:
|
|
1308
|
+
this.ctx.editor.insertPaste(text);
|
|
1309
|
+
break;
|
|
1321
1310
|
default:
|
|
1322
1311
|
// Esc / cancel: keep the original behavior — collapse to an inline paste marker.
|
|
1323
1312
|
this.ctx.editor.insertPaste(text);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { getSegmenter } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { LRUCache } from "lru-cache/raw";
|
|
4
|
-
import {
|
|
4
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
5
5
|
import type { AssistantMessageComponent } from "../components/assistant-message";
|
|
6
6
|
|
|
7
7
|
export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
|
|
@@ -88,7 +88,7 @@ export function visibleUnits(message: AssistantMessage, hideThinking: boolean):
|
|
|
88
88
|
for (const block of message.content) {
|
|
89
89
|
if (block.type === "text") {
|
|
90
90
|
total += countGraphemes(block.text);
|
|
91
|
-
} else if (block.type === "thinking" && !hideThinking &&
|
|
91
|
+
} else if (block.type === "thinking" && !hideThinking && canonicalizeMessage(block.thinking)) {
|
|
92
92
|
total += countGraphemes(block.thinking);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -129,7 +129,7 @@ export function buildDisplayMessage(
|
|
|
129
129
|
const units = countOf(i, block.text);
|
|
130
130
|
content.push(revealTextBlock(block, remaining, units));
|
|
131
131
|
remaining = Math.max(0, remaining - units);
|
|
132
|
-
} else if (block.type === "thinking" && !hideThinking &&
|
|
132
|
+
} else if (block.type === "thinking" && !hideThinking && canonicalizeMessage(block.thinking)) {
|
|
133
133
|
const units = countOf(i, block.thinking);
|
|
134
134
|
content.push(revealThinkingBlock(block, remaining, units));
|
|
135
135
|
remaining = Math.max(0, remaining - units);
|
|
@@ -231,7 +231,7 @@ export class StreamingRevealController {
|
|
|
231
231
|
const block = message.content[i]!;
|
|
232
232
|
if (block.type === "text") {
|
|
233
233
|
total += this.#unitCounter.count(i, block.text);
|
|
234
|
-
} else if (block.type === "thinking" && !this.#hideThinkingBlock &&
|
|
234
|
+
} else if (block.type === "thinking" && !this.#hideThinkingBlock && canonicalizeMessage(block.thinking)) {
|
|
235
235
|
total += this.#unitCounter.count(i, block.thinking);
|
|
236
236
|
}
|
|
237
237
|
}
|
|
@@ -1123,6 +1123,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1123
1123
|
imageLinks?: (string | undefined)[];
|
|
1124
1124
|
customType?: string;
|
|
1125
1125
|
display?: boolean;
|
|
1126
|
+
streamingBehavior?: "steer" | "followUp";
|
|
1126
1127
|
}): SubmittedUserInput {
|
|
1127
1128
|
const submission: SubmittedUserInput = {
|
|
1128
1129
|
text: input.text,
|
|
@@ -1130,6 +1131,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1130
1131
|
imageLinks: input.imageLinks,
|
|
1131
1132
|
customType: input.customType,
|
|
1132
1133
|
display: input.display,
|
|
1134
|
+
streamingBehavior: input.streamingBehavior,
|
|
1133
1135
|
cancelled: false,
|
|
1134
1136
|
started: false,
|
|
1135
1137
|
};
|
|
@@ -796,6 +796,7 @@ export async function runRpcMode(
|
|
|
796
796
|
name: tool.name,
|
|
797
797
|
description: tool.description,
|
|
798
798
|
parameters: isZodSchema(tool.parameters) ? zodToWireSchema(tool.parameters) : tool.parameters,
|
|
799
|
+
examples: tool.examples,
|
|
799
800
|
})),
|
|
800
801
|
contextUsage: session.getContextUsage(),
|
|
801
802
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
9
|
-
import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { Effort, ImageContent, Model, ToolExample } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import type { BashResult } from "../../exec/bash-executor";
|
|
11
11
|
import type { ContextUsage } from "../../extensibility/extensions/types";
|
|
12
12
|
import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
|
|
@@ -107,7 +107,7 @@ export interface RpcSessionState {
|
|
|
107
107
|
todoPhases: TodoPhase[];
|
|
108
108
|
/** For session dump / export (plain-text parity with /dump). */
|
|
109
109
|
systemPrompt?: string[];
|
|
110
|
-
dumpTools?: Array<{ name: string; description: string; parameters: unknown }>;
|
|
110
|
+
dumpTools?: Array<{ name: string; description: string; parameters: unknown; examples?: readonly ToolExample[] }>;
|
|
111
111
|
/** Current context window usage. Null tokens/percent when unknown (e.g. right after compaction). */
|
|
112
112
|
contextUsage?: ContextUsage;
|
|
113
113
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -54,6 +54,11 @@ export type SubmittedUserInput = {
|
|
|
54
54
|
* turn. Used by the `c`/`.` continue shortcut. */
|
|
55
55
|
synthetic?: boolean;
|
|
56
56
|
display?: boolean;
|
|
57
|
+
/** Queue intent if the session is (or becomes) busy when this submission is
|
|
58
|
+
* dispatched: "steer" (interrupt the active turn) or "followUp" (process after
|
|
59
|
+
* it). Normal user Enter carries "steer" to match the streaming-branch Enter;
|
|
60
|
+
* background/continuation submits omit it and default to "followUp". */
|
|
61
|
+
streamingBehavior?: "steer" | "followUp";
|
|
57
62
|
cancelled: boolean;
|
|
58
63
|
started: boolean;
|
|
59
64
|
};
|
|
@@ -222,6 +227,7 @@ export interface InteractiveModeContext {
|
|
|
222
227
|
imageLinks?: (string | undefined)[];
|
|
223
228
|
customType?: string;
|
|
224
229
|
display?: boolean;
|
|
230
|
+
streamingBehavior?: "steer" | "followUp";
|
|
225
231
|
}): SubmittedUserInput;
|
|
226
232
|
cancelPendingSubmission(): boolean;
|
|
227
233
|
markPendingSubmissionStarted(input: SubmittedUserInput): boolean;
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
import type { SessionContext } from "../../session/session-context";
|
|
46
46
|
import { createIrcMessageCard } from "../../tools/irc";
|
|
47
47
|
import { formatBytes, formatDuration } from "../../tools/render-utils";
|
|
48
|
-
import {
|
|
48
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
49
49
|
|
|
50
50
|
type TextBlock = { type: "text"; text: string };
|
|
51
51
|
interface RenderInitialMessagesOptions {
|
|
@@ -399,8 +399,8 @@ export class UiHelpers {
|
|
|
399
399
|
const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
|
|
400
400
|
const hasVisibleAssistantContent = message.content.some(
|
|
401
401
|
content =>
|
|
402
|
-
(content.type === "text" && content.text
|
|
403
|
-
(content.type === "thinking" &&
|
|
402
|
+
(content.type === "text" && canonicalizeMessage(content.text)) ||
|
|
403
|
+
(content.type === "thinking" && canonicalizeMessage(content.thinking)),
|
|
404
404
|
);
|
|
405
405
|
if (hasVisibleAssistantContent) {
|
|
406
406
|
// Rebuild reconstructs immutable history; seal (not finalize) so the
|
|
@@ -4,7 +4,6 @@ description: Wise senior engineer to consult or delegate work to — debugging,
|
|
|
4
4
|
spawns: explore
|
|
5
5
|
model: pi/slow
|
|
6
6
|
thinking-level: xhigh
|
|
7
|
-
blocking: true
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
You are the wise guy on the team — a senior engineer with deep judgment that other agents consult when they are stuck, uncertain, or need a second opinion. You also take direct delegation: if the caller hands you work, you do it, including reads, writes, edits, and running commands.
|