@oh-my-pi/pi-coding-agent 15.13.3 → 16.0.1
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 +155 -133
- package/dist/cli.js +621 -530
- package/dist/types/advisor/__tests__/advisor.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +58 -0
- package/dist/types/advisor/index.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +52 -0
- package/dist/types/advisor/watchdog.d.ts +5 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +66 -5
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/components/advisor-message.d.ts +9 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/types.d.ts +8 -1
- package/dist/types/sdk.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +81 -2
- package/dist/types/session/session-history-format.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/session/yield-queue.d.ts +2 -0
- package/dist/types/task/index.d.ts +21 -0
- package/dist/types/tools/github-cache.d.ts +5 -4
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/report-tool-issue.d.ts +0 -1
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/__tests__/advisor.test.ts +586 -0
- package/src/advisor/advise-tool.ts +87 -0
- package/src/advisor/index.ts +3 -0
- package/src/advisor/runtime.ts +248 -0
- package/src/advisor/watchdog.ts +83 -0
- package/src/cli/args.ts +1 -0
- package/src/collab/host.ts +1 -1
- package/src/config/model-roles.ts +13 -1
- package/src/config/settings-schema.ts +65 -6
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +101 -6
- package/src/discovery/helpers.ts +11 -0
- package/src/eval/__tests__/prelude-agent.test.ts +73 -0
- package/src/eval/js/shared/prelude.txt +12 -3
- package/src/eval/py/prelude.py +26 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/plugins/loader.ts +3 -2
- package/src/extensibility/plugins/manager.ts +4 -3
- package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +9 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/advisor-message.ts +99 -0
- package/src/modes/components/agent-hub.ts +7 -0
- package/src/modes/components/assistant-message.ts +86 -0
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/segments.ts +20 -7
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/command-controller.ts +69 -2
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +7 -0
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/ui-helpers.ts +9 -0
- package/src/prompts/advisor/advise-tool.md +1 -0
- package/src/prompts/advisor/system.md +31 -0
- package/src/prompts/agents/designer.md +8 -0
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +4 -1
- package/src/prompts/tools/eval.md +13 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/sdk.ts +61 -14
- package/src/session/agent-session.ts +667 -13
- package/src/session/session-dump-format.ts +15 -131
- package/src/session/session-history-format.ts +30 -11
- package/src/session/session-manager.ts +3 -1
- package/src/session/yield-queue.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +105 -4
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +5 -4
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/tools/path-utils.ts +33 -2
- package/src/tools/report-tool-issue.ts +2 -7
- package/src/web/scrapers/docs-rs.ts +2 -3
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
parsePositiveDecimalInt,
|
|
29
29
|
resolveDefaultRepoMemoized,
|
|
30
30
|
} from "../tools/gh";
|
|
31
|
-
import { formatFreshnessNote } from "../tools/github-cache";
|
|
31
|
+
import { type CacheStatus, formatFreshnessNote } from "../tools/github-cache";
|
|
32
32
|
import * as git from "../utils/git";
|
|
33
33
|
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext } from "./types";
|
|
34
34
|
|
|
@@ -355,7 +355,7 @@ interface BuildSingleArgs {
|
|
|
355
355
|
scheme: Scheme;
|
|
356
356
|
parsed: ParsedSingle;
|
|
357
357
|
rendered: string;
|
|
358
|
-
status:
|
|
358
|
+
status: CacheStatus;
|
|
359
359
|
fetchedAt: number;
|
|
360
360
|
/** Resolved repo (post short-form expansion) — used for the PR-only diff hint. */
|
|
361
361
|
repo?: string;
|
|
@@ -377,11 +377,15 @@ function buildSingleResource({
|
|
|
377
377
|
const diffUrl = repoSegment ? `pr://${repoSegment}/${parsed.number}/diff` : `pr://${parsed.number}/diff`;
|
|
378
378
|
notes.push(`Diff: ${diffUrl}`);
|
|
379
379
|
}
|
|
380
|
+
const content =
|
|
381
|
+
status === "stale"
|
|
382
|
+
? `> WARNING: Live GitHub refresh failed; this ${scheme} content is cached and may be stale.\n\n${rendered}`
|
|
383
|
+
: rendered;
|
|
380
384
|
return {
|
|
381
385
|
url: url.href,
|
|
382
|
-
content
|
|
386
|
+
content,
|
|
383
387
|
contentType: "text/markdown",
|
|
384
|
-
size: Buffer.byteLength(
|
|
388
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
385
389
|
notes,
|
|
386
390
|
};
|
|
387
391
|
}
|
package/src/main.ts
CHANGED
|
@@ -140,6 +140,10 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
|
|
|
140
140
|
// memory should opt in explicitly through their own settings layer.
|
|
141
141
|
"memory.backend",
|
|
142
142
|
"memories.enabled",
|
|
143
|
+
// Advisor is interactive-session assistance. Protocol hosts opt in explicitly
|
|
144
|
+
// instead of inheriting a user's globally-enabled local preference.
|
|
145
|
+
"advisor.enabled",
|
|
146
|
+
"advisor.subagents",
|
|
143
147
|
];
|
|
144
148
|
|
|
145
149
|
const RPC_BACKGROUND_DEFAULTED_SETTING_PATHS: SettingPath[] = [
|
|
@@ -307,7 +311,11 @@ export async function submitInteractiveInput(
|
|
|
307
311
|
// developer directive to a visible user message. A synthetic submit while
|
|
308
312
|
// streaming keeps its prior behavior (rejected as busy) rather than changing
|
|
309
313
|
// its role.
|
|
310
|
-
await session.prompt(input.text, {
|
|
314
|
+
await session.prompt(input.text, {
|
|
315
|
+
synthetic: true,
|
|
316
|
+
expandPromptTemplates: false,
|
|
317
|
+
userInitiated: input.userInitiated,
|
|
318
|
+
});
|
|
311
319
|
} else {
|
|
312
320
|
await session.prompt(input.text, { images: input.images, streamingBehavior });
|
|
313
321
|
}
|
|
@@ -62,7 +62,7 @@ import { loadAllExtensions } from "../../modes/components/extensions/state-manag
|
|
|
62
62
|
import { theme } from "../../modes/theme/theme";
|
|
63
63
|
import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
|
|
64
64
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
65
|
-
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
|
|
65
|
+
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
66
66
|
import type { UsageStatistics } from "../../session/session-entries";
|
|
67
67
|
import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
|
|
68
68
|
import { SessionManager } from "../../session/session-manager";
|
|
@@ -836,7 +836,7 @@ export class AcpAgent implements Agent {
|
|
|
836
836
|
timer = setTimeout(() => reject(new Error("ACP cancel cleanup timed out")), this.#cancelCleanupTimeoutMs);
|
|
837
837
|
});
|
|
838
838
|
try {
|
|
839
|
-
await Promise.race([record.session.abort(), timeout]);
|
|
839
|
+
await Promise.race([record.session.abort({ reason: USER_INTERRUPT_LABEL }), timeout]);
|
|
840
840
|
} finally {
|
|
841
841
|
if (timer) clearTimeout(timer);
|
|
842
842
|
// Order matters: clear `cleanup` before evicting the slot so the slot-eviction
|
|
@@ -2098,7 +2098,7 @@ export class AcpAgent implements Agent {
|
|
|
2098
2098
|
getModel: () => record.session.model,
|
|
2099
2099
|
isIdle: () => !record.session.isStreaming,
|
|
2100
2100
|
abort: () => {
|
|
2101
|
-
void record.session.abort();
|
|
2101
|
+
void record.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
2102
2102
|
},
|
|
2103
2103
|
hasPendingMessages: () => record.session.queuedMessageCount > 0,
|
|
2104
2104
|
shutdown: () => {},
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type Component, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { AdvisorMessageDetails, AdvisorSeverity } from "../../advisor";
|
|
3
|
+
import {
|
|
4
|
+
createCachedComponent,
|
|
5
|
+
formatBadge,
|
|
6
|
+
replaceTabs,
|
|
7
|
+
type ToolUIColor,
|
|
8
|
+
wrapTextWithAnsi,
|
|
9
|
+
} from "../../tools/render-utils";
|
|
10
|
+
import { Ellipsis, renderStatusLine, truncateToWidth } from "../../tui";
|
|
11
|
+
import type { Theme } from "../theme/theme";
|
|
12
|
+
|
|
13
|
+
const COLLAPSED_NOTES = 3;
|
|
14
|
+
const NOTE_LINE_WIDTH = 110;
|
|
15
|
+
|
|
16
|
+
function wrapVarying(text: string, w1: number, w2: number): string[] {
|
|
17
|
+
if (text.length === 0) return [];
|
|
18
|
+
const firstWrap = wrapTextWithAnsi(text, w1);
|
|
19
|
+
if (firstWrap.length <= 1) {
|
|
20
|
+
return firstWrap;
|
|
21
|
+
}
|
|
22
|
+
const firstLine = firstWrap[0];
|
|
23
|
+
const idx = text.indexOf(firstLine);
|
|
24
|
+
if (idx === -1) {
|
|
25
|
+
return wrapTextWithAnsi(text, w2);
|
|
26
|
+
}
|
|
27
|
+
const remainder = text.slice(idx + firstLine.length).trimStart();
|
|
28
|
+
const restWrap = wrapTextWithAnsi(remainder, w2);
|
|
29
|
+
return [firstLine, ...restWrap];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function severityColor(severity: AdvisorSeverity | undefined): ToolUIColor {
|
|
33
|
+
switch (severity) {
|
|
34
|
+
case "blocker":
|
|
35
|
+
return "error";
|
|
36
|
+
case "concern":
|
|
37
|
+
return "warning";
|
|
38
|
+
default:
|
|
39
|
+
return "muted";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Display-only transcript card for advisor notes injected into the primary
|
|
45
|
+
* session. Mirrors the IRC card's glyph + quote-border conventions so passive
|
|
46
|
+
* advice reads as a distinct, non-interrupting aside rather than a user turn.
|
|
47
|
+
*/
|
|
48
|
+
export function createAdvisorMessageCard(
|
|
49
|
+
details: AdvisorMessageDetails | undefined,
|
|
50
|
+
getExpanded: () => boolean,
|
|
51
|
+
uiTheme: Theme,
|
|
52
|
+
): Component {
|
|
53
|
+
const notes = details?.notes ?? [];
|
|
54
|
+
const blockers = notes.filter(note => note.severity === "blocker").length;
|
|
55
|
+
const meta: string[] = [`${notes.length} ${notes.length === 1 ? "note" : "notes"}`];
|
|
56
|
+
if (blockers > 0) meta.push(uiTheme.fg("error", `${blockers} blocker${blockers === 1 ? "" : "s"}`));
|
|
57
|
+
|
|
58
|
+
return createCachedComponent(
|
|
59
|
+
getExpanded,
|
|
60
|
+
(width, expanded) => {
|
|
61
|
+
const glyph = uiTheme.styledSymbol("status.info", "accent");
|
|
62
|
+
const lines = [renderStatusLine({ iconOverride: glyph, title: "Advisor", meta }, uiTheme)];
|
|
63
|
+
const quote = uiTheme.fg("dim", uiTheme.md.quoteBorder);
|
|
64
|
+
const shown = expanded ? notes : notes.slice(0, COLLAPSED_NOTES);
|
|
65
|
+
for (const entry of shown) {
|
|
66
|
+
const badge = entry.severity
|
|
67
|
+
? `${formatBadge(entry.severity, severityColor(entry.severity), uiTheme)} `
|
|
68
|
+
: "";
|
|
69
|
+
const quotePrefix = ` ${quote} `;
|
|
70
|
+
const quoteWidth = visibleWidth(quotePrefix);
|
|
71
|
+
const badgeWidth = visibleWidth(badge);
|
|
72
|
+
const w1 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth - badgeWidth);
|
|
73
|
+
const w2 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth);
|
|
74
|
+
|
|
75
|
+
const paragraphs = entry.note.split("\n").filter(p => p.trim());
|
|
76
|
+
const bodyLines: string[] = [];
|
|
77
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
78
|
+
const p = paragraphs[i];
|
|
79
|
+
if (i === 0) {
|
|
80
|
+
bodyLines.push(...wrapVarying(p, w1, w2));
|
|
81
|
+
} else {
|
|
82
|
+
bodyLines.push(...wrapTextWithAnsi(p, w2));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
bodyLines.forEach((line, index) => {
|
|
87
|
+
const prefix = index === 0 ? badge : "";
|
|
88
|
+
lines.push(` ${quote} ${prefix}${uiTheme.fg("toolOutput", replaceTabs(line))}`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const hidden = notes.length - shown.length;
|
|
92
|
+
if (hidden > 0) {
|
|
93
|
+
lines.push(` ${quote} ${uiTheme.fg("dim", `… +${hidden} more ${hidden === 1 ? "note" : "notes"}`)}`);
|
|
94
|
+
}
|
|
95
|
+
return lines.map(line => truncateToWidth(line, width, Ellipsis.Unicode));
|
|
96
|
+
},
|
|
97
|
+
{ paddingX: 1 },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -19,6 +19,7 @@ import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
|
19
19
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
20
20
|
import { Container, Editor, matchesKey, ScrollView, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
21
21
|
import { formatAge, formatBytes, formatDuration, formatNumber, getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
22
|
+
import type { AdvisorMessageDetails } from "../../advisor";
|
|
22
23
|
import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
|
|
23
24
|
import type { KeyId } from "../../config/keybindings";
|
|
24
25
|
import { settings } from "../../config/settings";
|
|
@@ -45,6 +46,7 @@ import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
|
45
46
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
46
47
|
import { getEditorTheme, theme } from "../theme/theme";
|
|
47
48
|
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
49
|
+
import { createAdvisorMessageCard } from "./advisor-message";
|
|
48
50
|
import { AssistantMessageComponent } from "./assistant-message";
|
|
49
51
|
import { createBackgroundTanDispatchBlock } from "./background-tan-message";
|
|
50
52
|
import { BashExecutionComponent } from "./bash-execution";
|
|
@@ -1241,6 +1243,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
1241
1243
|
this.#chatLog.addChild(card);
|
|
1242
1244
|
return;
|
|
1243
1245
|
}
|
|
1246
|
+
if (message.customType === "advisor") {
|
|
1247
|
+
const details = (message as CustomMessage<AdvisorMessageDetails>).details;
|
|
1248
|
+
this.#chatLog.addChild(createAdvisorMessageCard(details, () => this.#chatExpanded, theme));
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1244
1251
|
if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) {
|
|
1245
1252
|
this.#chatLog.addChild(createBackgroundTanDispatchBlock(message as CustomMessage<unknown>));
|
|
1246
1253
|
return;
|
|
@@ -15,6 +15,15 @@ import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
|
15
15
|
*/
|
|
16
16
|
const MAX_TRANSCRIPT_ERROR_LINES = 8;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Frames for the streaming "thinking" pulse rendered in place of a hidden
|
|
20
|
+
* thinking block while the model is still producing it. A single fixed-width
|
|
21
|
+
* glyph that rises ▁▃▄▃ so the indicator animates without shifting the line.
|
|
22
|
+
* Advanced every {@link THINKING_DOTS_FRAME_MS}.
|
|
23
|
+
*/
|
|
24
|
+
const THINKING_DOTS_FRAMES = ["▁", "▃", "▄", "▃"] as const;
|
|
25
|
+
const THINKING_DOTS_FRAME_MS = 320;
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* Component that renders a complete assistant message
|
|
20
29
|
*/
|
|
@@ -50,6 +59,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
50
59
|
#fastPathItems:
|
|
51
60
|
| Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
|
|
52
61
|
| undefined;
|
|
62
|
+
/** Live "thinking" pulse shown in place of a hidden thinking block while it
|
|
63
|
+
* streams; undefined when not animating. Driven by {@link #thinkingDotsTimer}. */
|
|
64
|
+
#thinkingDots: Text | undefined;
|
|
65
|
+
#thinkingDotsTimer: NodeJS.Timeout | undefined;
|
|
66
|
+
#thinkingDotsFrame = 0;
|
|
53
67
|
|
|
54
68
|
constructor(
|
|
55
69
|
message?: AssistantMessage,
|
|
@@ -87,6 +101,60 @@ export class AssistantMessageComponent extends Container {
|
|
|
87
101
|
this.hideThinkingBlock = hide;
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
override dispose(): void {
|
|
105
|
+
this.#stopThinkingAnimation();
|
|
106
|
+
super.dispose();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether to render the animated "thinking" pulse in place of the suppressed
|
|
111
|
+
* reasoning: only while this block is still streaming (not yet finalized — the
|
|
112
|
+
* in-flight message always carries `stopReason: "stop"`, so finalization is the
|
|
113
|
+
* only reliable live signal), thinking is hidden, no tool call has started, and
|
|
114
|
+
* the active tail block is a thinking block (the model is reasoning right now).
|
|
115
|
+
* Once text starts, a tool call streams, or the block is sealed, the pulse ends.
|
|
116
|
+
*/
|
|
117
|
+
#shouldAnimateThinking(message: AssistantMessage): boolean {
|
|
118
|
+
if (!this.hideThinkingBlock || this.#transcriptBlockFinalized) return false;
|
|
119
|
+
let tail: "text" | "thinking" | undefined;
|
|
120
|
+
for (const content of message.content) {
|
|
121
|
+
if (content.type === "toolCall") return false;
|
|
122
|
+
if (content.type === "text" && canonicalizeMessage(content.text)) tail = "text";
|
|
123
|
+
else if (content.type === "thinking" && canonicalizeMessage(content.thinking)) tail = "thinking";
|
|
124
|
+
}
|
|
125
|
+
return tail === "thinking";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#thinkingDotsLabel(): string {
|
|
129
|
+
const glyph = THINKING_DOTS_FRAMES[this.#thinkingDotsFrame % THINKING_DOTS_FRAMES.length] ?? "…";
|
|
130
|
+
return theme.fg("thinkingText", glyph);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#startThinkingAnimation(): void {
|
|
134
|
+
if (this.#thinkingDotsTimer) return;
|
|
135
|
+
this.#thinkingDotsTimer = setInterval(() => this.#advanceThinkingDots(), THINKING_DOTS_FRAME_MS);
|
|
136
|
+
this.#thinkingDotsTimer.unref?.();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#advanceThinkingDots(): void {
|
|
140
|
+
if (!this.#thinkingDots) {
|
|
141
|
+
this.#stopThinkingAnimation();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.#thinkingDotsFrame = (this.#thinkingDotsFrame + 1) % THINKING_DOTS_FRAMES.length;
|
|
145
|
+
if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
|
|
146
|
+
this.onImageUpdate?.();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#stopThinkingAnimation(): void {
|
|
151
|
+
if (this.#thinkingDotsTimer) {
|
|
152
|
+
clearInterval(this.#thinkingDotsTimer);
|
|
153
|
+
this.#thinkingDotsTimer = undefined;
|
|
154
|
+
}
|
|
155
|
+
this.#thinkingDotsFrame = 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
90
158
|
/**
|
|
91
159
|
* Toggle suppression of the inline `Error: …` line while the same error is
|
|
92
160
|
* pinned in the banner above the editor. Re-renders so the change is visible.
|
|
@@ -109,6 +177,14 @@ export class AssistantMessageComponent extends Container {
|
|
|
109
177
|
|
|
110
178
|
markTranscriptBlockFinalized(): void {
|
|
111
179
|
this.#transcriptBlockFinalized = true;
|
|
180
|
+
this.#stopThinkingAnimation();
|
|
181
|
+
// If the live pulse was on screen when the block sealed, drop the fast path
|
|
182
|
+
// and rebuild so the placeholder is removed — finalized blocks never animate.
|
|
183
|
+
if (this.#thinkingDots) {
|
|
184
|
+
this.#fastPathKey = undefined;
|
|
185
|
+
this.#fastPathItems = undefined;
|
|
186
|
+
if (this.#lastMessage) this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
187
|
+
}
|
|
112
188
|
}
|
|
113
189
|
|
|
114
190
|
/**
|
|
@@ -326,6 +402,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
326
402
|
|
|
327
403
|
// Clear content container
|
|
328
404
|
this.#contentContainer.clear();
|
|
405
|
+
this.#thinkingDots = undefined;
|
|
329
406
|
|
|
330
407
|
// Determine if we should capture Markdown instances for next fast path
|
|
331
408
|
const shouldCapture = this.#canFastPath(message);
|
|
@@ -382,6 +459,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
382
459
|
}
|
|
383
460
|
}
|
|
384
461
|
|
|
462
|
+
if (this.#shouldAnimateThinking(message)) {
|
|
463
|
+
if (hasVisibleContent) this.#contentContainer.addChild(new Spacer(1));
|
|
464
|
+
this.#thinkingDots = new Text(this.#thinkingDotsLabel(), 1, 0);
|
|
465
|
+
this.#contentContainer.addChild(this.#thinkingDots);
|
|
466
|
+
this.#startThinkingAnimation();
|
|
467
|
+
} else {
|
|
468
|
+
this.#stopThinkingAnimation();
|
|
469
|
+
}
|
|
470
|
+
|
|
385
471
|
this.#renderToolImages();
|
|
386
472
|
// Check if aborted - show after partial content
|
|
387
473
|
// But only if there are no tool calls (tool execution components will show the error)
|
|
@@ -104,6 +104,13 @@ const CONDITIONS: Record<string, () => boolean> = {
|
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
|
+
planModeEnabled: () => {
|
|
108
|
+
try {
|
|
109
|
+
return Settings.instance.get("plan.enabled");
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
107
114
|
};
|
|
108
115
|
|
|
109
116
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -86,32 +86,45 @@ const modelSegment: StatusLineSegment = {
|
|
|
86
86
|
modelName = modelName.slice(7);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Fast-mode icon and thinking-level suffix trail the model name and are
|
|
90
|
+
// colored together with it as `statusLineModel`. The advisor "++" badge
|
|
91
|
+
// sits between the name and that tail in `accent`, so it reads as a
|
|
92
|
+
// distinct marker. theme.fg resets only the fg, so the spans are
|
|
93
|
+
// concatenated (not nested) to keep each color intact.
|
|
94
|
+
let tail = "";
|
|
91
95
|
if (ctx.session.isFastModeActive() && theme.icon.fast) {
|
|
92
|
-
|
|
96
|
+
tail += ` ${theme.icon.fast}`;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
// Add thinking level with dot separator
|
|
96
99
|
if (opts.showThinkingLevel !== false && state.model?.thinking) {
|
|
97
100
|
if (ctx.session.isAutoThinking) {
|
|
98
101
|
// Pending (no turn classified yet / classifying) shows a symbol-theme
|
|
99
102
|
// question-box marker; once resolved it shows `<level>`.
|
|
100
103
|
const resolved = ctx.session.autoResolvedThinkingLevel();
|
|
101
104
|
const resolvedText = resolved ? (theme.thinking[resolved as keyof typeof theme.thinking] ?? resolved) : "";
|
|
102
|
-
|
|
105
|
+
tail += `${theme.sep.dot}${resolved ? resolvedText : `${theme.thinking.autoPending} auto`}`;
|
|
103
106
|
} else {
|
|
104
107
|
const level = state.thinkingLevel ?? ThinkingLevel.Off;
|
|
105
108
|
if (level !== ThinkingLevel.Off) {
|
|
106
109
|
const thinkingText = theme.thinking[level as keyof typeof theme.thinking];
|
|
107
110
|
if (thinkingText) {
|
|
108
|
-
|
|
111
|
+
tail += `${theme.sep.dot}${thinkingText}`;
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
|
|
117
|
+
// `statusLineModel` is aliased to `accent` in many themes, so the badge
|
|
118
|
+
// uses `success` to stay visibly distinct from the model name color.
|
|
119
|
+
let content = theme.fg("statusLineModel", withIcon(theme.icon.model, modelName));
|
|
120
|
+
if (ctx.session.isAdvisorActive()) {
|
|
121
|
+
content += theme.fg("success", "++");
|
|
122
|
+
}
|
|
123
|
+
if (tail) {
|
|
124
|
+
content += theme.fg("statusLineModel", tail);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { content, visible: true };
|
|
115
128
|
},
|
|
116
129
|
};
|
|
117
130
|
|
|
@@ -6,7 +6,7 @@ Find out which model you emotionally abuse the most with `omp stats`
|
|
|
6
6
|
Try task isolation to create CoW worktrees
|
|
7
7
|
Need a cheap nested model call? Use `completion(x...)`. Have a big batch of tasks? Ask clanker to use it!
|
|
8
8
|
Spaghetti code? Try complaining with /omfg
|
|
9
|
-
Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
|
|
9
|
+
Did you know? Each kitty/tmux/cmux/zellij/wezterm split keeps its own session — `omp -c` resumes the right one
|
|
10
10
|
Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
|
|
11
11
|
Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
|
|
12
12
|
Say `workflowz` in your message to drive the task with parallel subagents in eval — watch it glow as you type
|
|
@@ -84,9 +84,9 @@ export class CommandController {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
handleDumpCommand() {
|
|
87
|
+
handleDumpCommand(isRaw = false) {
|
|
88
88
|
try {
|
|
89
|
-
const formatted = this.ctx.session.formatSessionAsText();
|
|
89
|
+
const formatted = this.ctx.session.formatSessionAsText({ compact: !isRaw });
|
|
90
90
|
if (!formatted) {
|
|
91
91
|
this.ctx.showError("No messages to dump yet.");
|
|
92
92
|
return;
|
|
@@ -98,6 +98,26 @@ export class CommandController {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
handleAdvisorDumpCommand(isRaw = false) {
|
|
102
|
+
try {
|
|
103
|
+
const advisorHistory = this.ctx.session.formatAdvisorHistoryAsText({ compact: !isRaw });
|
|
104
|
+
if (advisorHistory === null) {
|
|
105
|
+
this.ctx.showError("Advisor is not active for this session.");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!advisorHistory) {
|
|
109
|
+
this.ctx.showError("Advisor has no history yet.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
copyToClipboard(advisorHistory);
|
|
113
|
+
this.ctx.showStatus("Advisor history copied to clipboard");
|
|
114
|
+
} catch (error: unknown) {
|
|
115
|
+
this.ctx.showError(
|
|
116
|
+
`Failed to copy advisor history: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
async handleDebugTranscriptCommand(): Promise<void> {
|
|
102
122
|
try {
|
|
103
123
|
const width = Math.max(1, this.ctx.ui.terminal.columns);
|
|
@@ -305,6 +325,53 @@ export class CommandController {
|
|
|
305
325
|
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
|
|
306
326
|
}
|
|
307
327
|
|
|
328
|
+
async handleAdvisorStatusCommand(): Promise<void> {
|
|
329
|
+
const stats = this.ctx.session.getAdvisorStats();
|
|
330
|
+
if (!stats.active) {
|
|
331
|
+
this.ctx.present([
|
|
332
|
+
new Spacer(1),
|
|
333
|
+
new Text(
|
|
334
|
+
stats.configured
|
|
335
|
+
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
336
|
+
: "Advisor is disabled.",
|
|
337
|
+
1,
|
|
338
|
+
0,
|
|
339
|
+
),
|
|
340
|
+
]);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const model = stats.model!;
|
|
344
|
+
let info = `${theme.bold("Advisor Status")}\n\n`;
|
|
345
|
+
info += `${theme.bold("Provider")}\n`;
|
|
346
|
+
info += `${theme.fg("dim", "Model:")} ${model.provider}/${model.id}\n`;
|
|
347
|
+
info += `\n${theme.bold("Messages")}\n`;
|
|
348
|
+
info += `${theme.fg("dim", "User:")} ${stats.messages.user.toLocaleString()}\n`;
|
|
349
|
+
info += `${theme.fg("dim", "Assistant:")} ${stats.messages.assistant.toLocaleString()}\n`;
|
|
350
|
+
info += `${theme.fg("dim", "Total:")} ${stats.messages.total.toLocaleString()}\n`;
|
|
351
|
+
info += `\n${theme.bold("Context")}\n`;
|
|
352
|
+
if (stats.contextWindow > 0) {
|
|
353
|
+
const percent = Math.round((stats.contextTokens / stats.contextWindow) * 100);
|
|
354
|
+
info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()} / ${stats.contextWindow.toLocaleString()} (${percent}%)\n`;
|
|
355
|
+
} else {
|
|
356
|
+
info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()}\n`;
|
|
357
|
+
}
|
|
358
|
+
info += `\n${theme.bold("Spend")}\n`;
|
|
359
|
+
info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
|
|
360
|
+
info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
|
|
361
|
+
if (stats.tokens.cacheRead > 0) {
|
|
362
|
+
info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
|
|
363
|
+
}
|
|
364
|
+
if (stats.tokens.cacheWrite > 0) {
|
|
365
|
+
info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
|
|
366
|
+
}
|
|
367
|
+
info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
368
|
+
if (stats.cost > 0) {
|
|
369
|
+
info += `\n${theme.bold("Cost")}\n`;
|
|
370
|
+
info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(4)}\n`;
|
|
371
|
+
}
|
|
372
|
+
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
|
|
373
|
+
}
|
|
374
|
+
|
|
308
375
|
async handleJobsCommand(): Promise<void> {
|
|
309
376
|
const snapshot = this.ctx.session.getAsyncJobSnapshot({ recentLimit: 5 });
|
|
310
377
|
if (!snapshot) {
|
|
@@ -23,6 +23,7 @@ import { HookInputComponent } from "../../modes/components/hook-input";
|
|
|
23
23
|
import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
|
|
24
24
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
25
25
|
import type { InteractiveModeContext, InteractiveSelectorDialogOptions } from "../../modes/types";
|
|
26
|
+
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
26
27
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
27
28
|
|
|
28
29
|
const MAX_WIDGET_LINES = 10;
|
|
@@ -123,7 +124,7 @@ export class ExtensionUiController {
|
|
|
123
124
|
const contextActions: ExtensionContextActions = {
|
|
124
125
|
getModel: () => this.ctx.session.model,
|
|
125
126
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
126
|
-
abort: () => this.ctx.session.abort(),
|
|
127
|
+
abort: () => this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL }),
|
|
127
128
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
128
129
|
shutdown: () => {
|
|
129
130
|
// Defer the actual teardown to the main loop, which calls
|
|
@@ -359,7 +360,7 @@ export class ExtensionUiController {
|
|
|
359
360
|
const contextActions: ExtensionContextActions = {
|
|
360
361
|
getModel: () => this.ctx.session.model,
|
|
361
362
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
362
|
-
abort: () => this.ctx.session.abort(),
|
|
363
|
+
abort: () => this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL }),
|
|
363
364
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
364
365
|
shutdown: () => {
|
|
365
366
|
// Defer the actual teardown to the main loop, which calls
|
|
@@ -500,7 +501,7 @@ export class ExtensionUiController {
|
|
|
500
501
|
isIdle: () => !this.ctx.session.isStreaming,
|
|
501
502
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
502
503
|
abort: () => {
|
|
503
|
-
this.ctx.session.abort();
|
|
504
|
+
this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
504
505
|
},
|
|
505
506
|
shutdown: () => {
|
|
506
507
|
// Signal shutdown request
|
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
|
|
41
41
|
import {
|
|
42
42
|
isImageProviderPreference,
|
|
43
|
+
isSearchProviderId,
|
|
43
44
|
isSearchProviderPreference,
|
|
45
|
+
setExcludedSearchProviders,
|
|
44
46
|
setPreferredImageProvider,
|
|
45
47
|
setPreferredSearchProvider,
|
|
46
48
|
} from "../../tools";
|
|
@@ -419,6 +421,11 @@ export class SelectorController {
|
|
|
419
421
|
setPreferredSearchProvider(value);
|
|
420
422
|
}
|
|
421
423
|
break;
|
|
424
|
+
case "providers.webSearchExclude":
|
|
425
|
+
if (Array.isArray(value)) {
|
|
426
|
+
setExcludedSearchProviders(value.filter(isSearchProviderId));
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
422
429
|
case "providers.image":
|
|
423
430
|
if (isImageProviderPreference(value)) {
|
|
424
431
|
setPreferredImageProvider(value);
|