@oh-my-pi/pi-coding-agent 15.13.2 → 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 +22 -0
- package/dist/cli.js +147 -122
- package/dist/types/config/settings-schema.d.ts +31 -0
- package/dist/types/eval/js/context-manager.d.ts +15 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/session/unexpected-stop-classifier.d.ts +13 -0
- package/dist/types/stt/asr-client.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +1 -1
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tts/tts-client.d.ts +1 -1
- 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 +6 -2
- package/src/config/settings-schema.ts +25 -0
- package/src/eval/__tests__/agent-bridge.test.ts +106 -46
- package/src/eval/__tests__/js-context-manager.test.ts +12 -2
- package/src/eval/js/context-manager.ts +40 -3
- 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 +5 -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/controllers/event-controller.ts +3 -3
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/streaming-reveal.ts +4 -4
- package/src/modes/interactive-mode.ts +2 -0
- 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/unexpected-stop-classifier.md +17 -0
- package/src/prompts/system/unexpected-stop-retry.md +4 -0
- package/src/session/agent-session.ts +164 -10
- package/src/session/session-dump-format.ts +8 -19
- package/src/session/unexpected-stop-classifier.ts +129 -0
- package/src/stt/asr-client.ts +1 -1
- package/src/tiny/title-client.ts +1 -1
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser/tab-worker-entry.ts +12 -4
- package/src/tools/job.ts +1 -0
- package/src/tts/tts-client.ts +1 -1
- 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
|
}
|
|
@@ -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();
|
|
@@ -689,11 +689,17 @@ export class InputController {
|
|
|
689
689
|
this.ctx.pendingImages = [];
|
|
690
690
|
this.ctx.pendingImageLinks = [];
|
|
691
691
|
|
|
692
|
-
// 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.
|
|
693
698
|
const submission = this.ctx.startPendingSubmission({
|
|
694
699
|
text,
|
|
695
700
|
images,
|
|
696
701
|
imageLinks: inputImageLinks,
|
|
702
|
+
streamingBehavior: "steer",
|
|
697
703
|
});
|
|
698
704
|
|
|
699
705
|
this.ctx.onInputCallback(submission);
|
|
@@ -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
|
};
|
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.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
You are checking whether an assistant message is an unexpected stop. A message is an unexpected stop if the assistant says it will take an action, continue working, or call a tool, but then ends without actually doing so.
|
|
2
|
+
|
|
3
|
+
Examples of unexpected stops:
|
|
4
|
+
- "I should do the same for the JS eval worker. Doing that now."
|
|
5
|
+
- "Let me run the tests next."
|
|
6
|
+
- "I'll fix that now."
|
|
7
|
+
- "Should I do that for you?"
|
|
8
|
+
|
|
9
|
+
Not an unexpected stop:
|
|
10
|
+
- "I've completed the task."
|
|
11
|
+
- "Is there anything else I can help with?"
|
|
12
|
+
- "The fix is done and tests pass."
|
|
13
|
+
|
|
14
|
+
Message:
|
|
15
|
+
{{message}}
|
|
16
|
+
|
|
17
|
+
Answer with a single word: YES if this is an unexpected stop, NO otherwise.
|