@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2
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 +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commands/commit.ts +10 -0
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +93 -8
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/ssh.ts +3 -2
- package/src/tools/write.ts +64 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared rendering for extension/hook custom message frames.
|
|
3
|
+
*
|
|
4
|
+
* Both `CustomMessageComponent` and `HookMessageComponent` wrap a
|
|
5
|
+
* `Spacer(1) + Box` layout, try a user-supplied renderer first, and fall
|
|
6
|
+
* back to a label + markdown body when the renderer returns nothing or
|
|
7
|
+
* throws. The only meaningful difference is that hook messages collapse to
|
|
8
|
+
* the first N lines when not expanded; extension messages render in full.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
12
|
+
import type { Box, Component } from "@oh-my-pi/pi-tui";
|
|
13
|
+
import { Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
14
|
+
import { getMarkdownTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
15
|
+
|
|
16
|
+
/** Message shape consumed by the shared frame. */
|
|
17
|
+
export interface FramedMessage {
|
|
18
|
+
customType: string;
|
|
19
|
+
content: string | (TextContent | { type: string })[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Callable signature shared by `MessageRenderer` (extensions) and
|
|
24
|
+
* `HookMessageRenderer` (hooks). Both narrow `message` to their own type;
|
|
25
|
+
* this signature is the structural intersection callers can hand off here.
|
|
26
|
+
*/
|
|
27
|
+
export type FramedRenderer<M extends FramedMessage> = (
|
|
28
|
+
message: M,
|
|
29
|
+
options: { expanded: boolean },
|
|
30
|
+
theme: Theme,
|
|
31
|
+
) => Component | undefined;
|
|
32
|
+
|
|
33
|
+
export interface RebuildFrameOptions<M extends FramedMessage> {
|
|
34
|
+
message: M;
|
|
35
|
+
box: Box;
|
|
36
|
+
expanded: boolean;
|
|
37
|
+
/** Collapse the markdown body to this many lines when `expanded` is false. Omit to never collapse. */
|
|
38
|
+
collapseAfterLines?: number;
|
|
39
|
+
customRenderer?: FramedRenderer<M>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Attempt the custom renderer; on failure or undefined return, populate
|
|
44
|
+
* `box` with the default `[customType]` label + markdown body and return
|
|
45
|
+
* undefined. When the custom renderer succeeds, return its Component so the
|
|
46
|
+
* caller can mount it and skip the default box.
|
|
47
|
+
*/
|
|
48
|
+
export function renderFramedMessage<M extends FramedMessage>(opts: RebuildFrameOptions<M>): Component | undefined {
|
|
49
|
+
if (opts.customRenderer) {
|
|
50
|
+
try {
|
|
51
|
+
const component = opts.customRenderer(opts.message, { expanded: opts.expanded }, theme);
|
|
52
|
+
if (component) return component;
|
|
53
|
+
} catch {
|
|
54
|
+
// Fall through to default rendering
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
opts.box.clear();
|
|
59
|
+
|
|
60
|
+
const label = theme.fg("customMessageLabel", theme.bold(`[${opts.message.customType}]`));
|
|
61
|
+
opts.box.addChild(new Text(label, 0, 0));
|
|
62
|
+
opts.box.addChild(new Spacer(1));
|
|
63
|
+
|
|
64
|
+
let text: string;
|
|
65
|
+
if (typeof opts.message.content === "string") {
|
|
66
|
+
text = opts.message.content;
|
|
67
|
+
} else {
|
|
68
|
+
text = opts.message.content
|
|
69
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
70
|
+
.map(c => c.text)
|
|
71
|
+
.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!opts.expanded && opts.collapseAfterLines !== undefined) {
|
|
75
|
+
const lines = text.split("\n");
|
|
76
|
+
if (lines.length > opts.collapseAfterLines) {
|
|
77
|
+
text = `${lines.slice(0, opts.collapseAfterLines).join("\n")}\n…`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
opts.box.addChild(
|
|
82
|
+
new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
83
|
+
color: (value: string) => theme.fg("customMessageText", value),
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import { getSupportedEfforts, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
5
|
+
fuzzyFilter,
|
|
5
6
|
getKeybindings,
|
|
6
7
|
Input,
|
|
7
8
|
matchesKey,
|
|
@@ -18,7 +19,6 @@ import { resolveModelRoleValue } from "../../config/model-resolver";
|
|
|
18
19
|
import type { Settings } from "../../config/settings";
|
|
19
20
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
20
21
|
import { getThinkingLevelMetadata } from "../../thinking";
|
|
21
|
-
import { fuzzyFilter } from "../../utils/fuzzy";
|
|
22
22
|
import { getTabBarTheme } from "../shared";
|
|
23
23
|
import { DynamicBorder } from "./dynamic-border";
|
|
24
24
|
|
|
@@ -18,6 +18,7 @@ import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
|
18
18
|
import { Container, Markdown, type MarkdownTheme, matchesKey } from "@oh-my-pi/pi-tui";
|
|
19
19
|
import { formatDuration, formatNumber, logger } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import type { KeyId } from "../../config/keybindings";
|
|
21
|
+
import { isSilentAbort } from "../../session/messages";
|
|
21
22
|
import type { SessionMessageEntry } from "../../session/session-manager";
|
|
22
23
|
import { parseSessionEntries } from "../../session/session-manager";
|
|
23
24
|
import { PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
@@ -267,7 +268,10 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
267
268
|
if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
|
|
268
269
|
if (progress.tokens > 0) stats.push(`${formatNumber(progress.tokens)} tokens`);
|
|
269
270
|
if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
|
|
270
|
-
|
|
271
|
+
const parts: string[] = [];
|
|
272
|
+
if (stats.length > 0) parts.push(theme.fg("dim", stats.join(theme.sep.dot)));
|
|
273
|
+
if (progress.cost > 0) parts.push(theme.fg("statusLineCost", `$${progress.cost.toFixed(2)}`));
|
|
274
|
+
return parts.join(theme.sep.dot);
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
#buildTranscriptLines(messageEntries: SessionMessageEntry[], lines: string[]): void {
|
|
@@ -285,7 +289,7 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
285
289
|
|
|
286
290
|
if (msg.role === "assistant") {
|
|
287
291
|
// Handle error messages with empty content
|
|
288
|
-
if (msg.content.length === 0 && msg.errorMessage) {
|
|
292
|
+
if (msg.content.length === 0 && msg.errorMessage && !isSilentAbort(msg.errorMessage)) {
|
|
289
293
|
const startLine = lines.length;
|
|
290
294
|
const isSelected = entryIndex === this.#selectedEntryIndex;
|
|
291
295
|
const cursor = isSelected ? theme.fg("accent", "▶") : " ";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Component,
|
|
3
3
|
Container,
|
|
4
|
+
fuzzyFilter,
|
|
4
5
|
Input,
|
|
5
6
|
matchesKey,
|
|
6
7
|
padding,
|
|
@@ -14,7 +15,6 @@ import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
|
14
15
|
import { theme } from "../../modes/theme/theme";
|
|
15
16
|
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
16
17
|
import type { SessionInfo } from "../../session/session-manager";
|
|
17
|
-
import { fuzzyFilter } from "../../utils/fuzzy";
|
|
18
18
|
import { DynamicBorder } from "./dynamic-border";
|
|
19
19
|
import { HookSelectorComponent } from "./hook-selector";
|
|
20
20
|
|
|
@@ -2,8 +2,8 @@ import * as os from "node:os";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { formatDuration, formatNumber, getProjectDir, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import { theme } from "../../../modes/theme/theme";
|
|
5
|
+
import { formatDuration, formatNumber, getProjectDir, pathIsWithin, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { type ThemeColor, theme } from "../../../modes/theme/theme";
|
|
7
7
|
import { shortenPath } from "../../../tools/render-utils";
|
|
8
8
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
|
|
9
9
|
import { sanitizeStatusText } from "../../shared";
|
|
@@ -32,6 +32,33 @@ function normalizePremiumRequests(value: number): number {
|
|
|
32
32
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const SCRATCH_ROOTS: readonly string[] = (() => {
|
|
36
|
+
const roots = new Set<string>([os.tmpdir(), path.join(os.homedir(), "tmp")]);
|
|
37
|
+
if (process.platform === "win32") {
|
|
38
|
+
const { TEMP, TMP, SystemRoot } = process.env;
|
|
39
|
+
if (TEMP) roots.add(TEMP);
|
|
40
|
+
if (TMP) roots.add(TMP);
|
|
41
|
+
if (SystemRoot) roots.add(path.join(SystemRoot, "Temp"));
|
|
42
|
+
} else {
|
|
43
|
+
roots.add("/tmp");
|
|
44
|
+
roots.add("/var/tmp");
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
roots.add("/private/tmp");
|
|
47
|
+
roots.add("/private/var/tmp");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...roots];
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
function classifyProjectDir(pwd: string): { scratch: boolean; relative: string | null } {
|
|
54
|
+
for (const root of SCRATCH_ROOTS) {
|
|
55
|
+
if (pathIsWithin(root, pwd)) {
|
|
56
|
+
return { scratch: true, relative: relativePathWithinRoot(root, pwd) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { scratch: false, relative: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
63
|
// Segment Implementations
|
|
37
64
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -76,17 +103,65 @@ const modelSegment: StatusLineSegment = {
|
|
|
76
103
|
},
|
|
77
104
|
};
|
|
78
105
|
|
|
106
|
+
function formatGoalBudget(current: number, budget?: number): string {
|
|
107
|
+
const used = formatNumber(current);
|
|
108
|
+
if (budget === undefined) return used;
|
|
109
|
+
return `${used}/${formatNumber(budget)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: boolean }): RenderedSegment {
|
|
113
|
+
const goal = ctx.session.getGoalModeState()?.goal;
|
|
114
|
+
const status = goal?.status ?? (mode.paused ? "paused" : "active");
|
|
115
|
+
|
|
116
|
+
let icon: string = theme.icon.goal;
|
|
117
|
+
let color: ThemeColor = "accent";
|
|
118
|
+
switch (status) {
|
|
119
|
+
case "paused":
|
|
120
|
+
icon = theme.icon.pause || theme.symbol("status.pending");
|
|
121
|
+
color = "warning";
|
|
122
|
+
break;
|
|
123
|
+
case "complete":
|
|
124
|
+
icon = theme.symbol("status.success");
|
|
125
|
+
color = "success";
|
|
126
|
+
break;
|
|
127
|
+
case "budget-limited":
|
|
128
|
+
icon = theme.symbol("status.warning");
|
|
129
|
+
color = "warning";
|
|
130
|
+
break;
|
|
131
|
+
case "dropped":
|
|
132
|
+
icon = theme.symbol("status.aborted");
|
|
133
|
+
color = "dim";
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const parts: string[] = [withIcon(icon, "Goal")];
|
|
140
|
+
const showBudget = ctx.session.settings.get("goal.statusInFooter") === true;
|
|
141
|
+
if (showBudget && goal) {
|
|
142
|
+
parts.push(formatGoalBudget(goal.tokensUsed, goal.tokenBudget));
|
|
143
|
+
}
|
|
144
|
+
return { content: theme.fg(color, parts.join(" ")), visible: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
79
147
|
const modeSegment: StatusLineSegment = {
|
|
80
148
|
id: "mode",
|
|
81
149
|
render(ctx) {
|
|
150
|
+
const pauseSuffix = theme.icon.pause ? ` ${theme.icon.pause}` : " (paused)";
|
|
151
|
+
|
|
82
152
|
const plan = ctx.planMode;
|
|
83
153
|
if (plan && (plan.enabled || plan.paused)) {
|
|
84
|
-
const label = plan.paused ?
|
|
154
|
+
const label = plan.paused ? `Plan${pauseSuffix}` : "Plan";
|
|
85
155
|
const content = withIcon(theme.icon.plan, label);
|
|
86
156
|
const color = plan.paused ? "warning" : "accent";
|
|
87
157
|
return { content: theme.fg(color, content), visible: true };
|
|
88
158
|
}
|
|
89
159
|
|
|
160
|
+
const goal = ctx.goalMode;
|
|
161
|
+
if (goal && (goal.enabled || goal.paused)) {
|
|
162
|
+
return renderGoalMode(ctx, goal);
|
|
163
|
+
}
|
|
164
|
+
|
|
90
165
|
const loop = ctx.loopMode;
|
|
91
166
|
if (loop?.enabled) {
|
|
92
167
|
const content = withIcon(theme.icon.loop, "Loop");
|
|
@@ -102,10 +177,16 @@ const pathSegment: StatusLineSegment = {
|
|
|
102
177
|
render(ctx) {
|
|
103
178
|
const opts = ctx.options.path ?? {};
|
|
104
179
|
|
|
105
|
-
|
|
180
|
+
const projectDir = getProjectDir();
|
|
181
|
+
const { scratch, relative } = classifyProjectDir(projectDir);
|
|
182
|
+
let pwd = projectDir;
|
|
106
183
|
|
|
107
184
|
if (opts.stripWorkPrefix !== false) {
|
|
108
|
-
|
|
185
|
+
if (scratch) {
|
|
186
|
+
if (relative) pwd = relative;
|
|
187
|
+
} else {
|
|
188
|
+
pwd = stripDisplayRoot(pwd);
|
|
189
|
+
}
|
|
109
190
|
}
|
|
110
191
|
if (opts.abbreviate !== false) {
|
|
111
192
|
pwd = shortenPath(pwd);
|
|
@@ -118,7 +199,8 @@ const pathSegment: StatusLineSegment = {
|
|
|
118
199
|
pwd = `${ellipsis}${pwd.slice(-sliceLen)}`;
|
|
119
200
|
}
|
|
120
201
|
|
|
121
|
-
const
|
|
202
|
+
const icon = scratch ? theme.icon.scratchFolder : theme.icon.folder;
|
|
203
|
+
const content = withIcon(icon, pwd);
|
|
122
204
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
123
205
|
},
|
|
124
206
|
};
|
|
@@ -216,8 +298,11 @@ const tokenOutSegment: StatusLineSegment = {
|
|
|
216
298
|
const tokenTotalSegment: StatusLineSegment = {
|
|
217
299
|
id: "token_total",
|
|
218
300
|
render(ctx) {
|
|
219
|
-
|
|
220
|
-
|
|
301
|
+
// Excludes cacheRead: that field re-reads the full cached context every
|
|
302
|
+
// turn, making the cumulative sum N×context_size. The dedicated cache_read
|
|
303
|
+
// segment handles cache monitoring; the cost segment handles billing.
|
|
304
|
+
const { input, output, cacheWrite } = ctx.usageStats;
|
|
305
|
+
const total = input + output + cacheWrite;
|
|
221
306
|
if (!total) return { content: "", visible: false };
|
|
222
307
|
|
|
223
308
|
const content = withIcon(theme.icon.tokens, formatNumber(total));
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
3
2
|
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
4
3
|
import { formatCount, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
5
4
|
import { $ } from "bun";
|
|
@@ -7,10 +6,10 @@ import { settings } from "../../config/settings";
|
|
|
7
6
|
import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
|
|
8
7
|
import { theme } from "../../modes/theme/theme";
|
|
9
8
|
import type { AgentSession } from "../../session/agent-session";
|
|
10
|
-
import { calculatePromptTokens } from "../../session/compaction/compaction";
|
|
11
9
|
import * as git from "../../utils/git";
|
|
12
10
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../utils/session-color";
|
|
13
11
|
import { sanitizeStatusText } from "../shared";
|
|
12
|
+
import { computeContextBreakdown } from "../utils/context-usage";
|
|
14
13
|
import {
|
|
15
14
|
canReuseCachedPr,
|
|
16
15
|
createPrCacheContext,
|
|
@@ -59,6 +58,7 @@ export class StatusLineComponent implements Component {
|
|
|
59
58
|
#sessionStartTime: number = Date.now();
|
|
60
59
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
61
60
|
#loopModeStatus: { enabled: boolean } | null = null;
|
|
61
|
+
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
62
62
|
|
|
63
63
|
// Git status caching (1s TTL)
|
|
64
64
|
#cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
@@ -73,6 +73,10 @@ export class StatusLineComponent implements Component {
|
|
|
73
73
|
#lastTokensPerSecond: number | null = null;
|
|
74
74
|
#lastTokensPerSecondTimestamp: number | null = null;
|
|
75
75
|
|
|
76
|
+
// Context breakdown caching (2s TTL — aligns with /context command output)
|
|
77
|
+
#cachedBreakdown: { usedTokens: number; contextWindow: number } | null = null;
|
|
78
|
+
#breakdownFetchedAt = 0;
|
|
79
|
+
|
|
76
80
|
constructor(private readonly session: AgentSession) {
|
|
77
81
|
this.#settings = {
|
|
78
82
|
preset: settings.get("statusLine.preset"),
|
|
@@ -109,6 +113,10 @@ export class StatusLineComponent implements Component {
|
|
|
109
113
|
this.#loopModeStatus = status ?? null;
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
setGoalModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
|
|
117
|
+
this.#goalModeStatus = status ?? null;
|
|
118
|
+
}
|
|
119
|
+
|
|
112
120
|
setHookStatus(key: string, text: string | undefined): void {
|
|
113
121
|
if (text === undefined) {
|
|
114
122
|
this.#hookStatuses.delete(key);
|
|
@@ -301,6 +309,19 @@ export class StatusLineComponent implements Component {
|
|
|
301
309
|
return null;
|
|
302
310
|
}
|
|
303
311
|
|
|
312
|
+
#getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
if (!this.#cachedBreakdown || now - this.#breakdownFetchedAt > 2_000) {
|
|
315
|
+
const breakdown = computeContextBreakdown(this.session);
|
|
316
|
+
this.#cachedBreakdown = {
|
|
317
|
+
usedTokens: breakdown.usedTokens,
|
|
318
|
+
contextWindow: breakdown.contextWindow,
|
|
319
|
+
};
|
|
320
|
+
this.#breakdownFetchedAt = now;
|
|
321
|
+
}
|
|
322
|
+
return this.#cachedBreakdown;
|
|
323
|
+
}
|
|
324
|
+
|
|
304
325
|
#buildSegmentContext(width: number): SegmentContext {
|
|
305
326
|
const state = this.session.state;
|
|
306
327
|
|
|
@@ -318,14 +339,10 @@ export class StatusLineComponent implements Component {
|
|
|
318
339
|
tokensPerSecond: this.#getTokensPerSecond(),
|
|
319
340
|
};
|
|
320
341
|
|
|
321
|
-
//
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
.find(m => m.role === "assistant" && m.stopReason !== "aborted") as AssistantMessage | undefined;
|
|
326
|
-
|
|
327
|
-
const contextTokens = lastAssistantMessage ? calculatePromptTokens(lastAssistantMessage.usage) : 0;
|
|
328
|
-
const contextWindow = state.model?.contextWindow || 0;
|
|
342
|
+
// Context usage — aligned with /context command so both surfaces report the same value
|
|
343
|
+
const breakdown = this.#getCachedContextBreakdown();
|
|
344
|
+
const contextTokens = breakdown.usedTokens;
|
|
345
|
+
const contextWindow = breakdown.contextWindow || state.model?.contextWindow || 0;
|
|
329
346
|
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
330
347
|
|
|
331
348
|
return {
|
|
@@ -334,6 +351,7 @@ export class StatusLineComponent implements Component {
|
|
|
334
351
|
options: this.#resolveSettings().segmentOptions ?? {},
|
|
335
352
|
planMode: this.#planModeStatus,
|
|
336
353
|
loopMode: this.#loopModeStatus,
|
|
354
|
+
goalMode: this.#goalModeStatus,
|
|
337
355
|
usageStats,
|
|
338
356
|
contextPercent,
|
|
339
357
|
contextWindow,
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
33
33
|
import { toolRenderers } from "../../tools/renderers";
|
|
34
34
|
import { renderStatusLine } from "../../tui";
|
|
35
|
-
import { convertToPng } from "../../utils/image-convert";
|
|
36
35
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
37
36
|
import { renderDiff } from "./diff";
|
|
38
37
|
|
|
@@ -295,13 +294,13 @@ export class ToolExecutionComponent extends Container {
|
|
|
295
294
|
|
|
296
295
|
// Convert async - catch errors from processing
|
|
297
296
|
const index = i;
|
|
298
|
-
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
297
|
+
new Bun.Image(Buffer.from(img.data, "base64"))
|
|
298
|
+
.png()
|
|
299
|
+
.toBase64()
|
|
300
|
+
.then(data => {
|
|
301
|
+
this.#convertedImages.set(index, { data, mimeType: "image/png" });
|
|
302
|
+
this.#updateDisplay();
|
|
303
|
+
this.#ui.requestRender();
|
|
305
304
|
})
|
|
306
305
|
.catch(() => {
|
|
307
306
|
// Ignore conversion failures - display will use original image format
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for /mcp and /ssh command controllers.
|
|
3
|
+
*
|
|
4
|
+
* Captures argument parsing, source grouping, and chat-message rendering that
|
|
5
|
+
* was duplicated between mcp-command-controller and ssh-command-controller.
|
|
6
|
+
* Intentionally kept narrow: subcommand routing, help text, success/error
|
|
7
|
+
* wording, and add-flow logic stay in the per-controller files because they
|
|
8
|
+
* diverge in workflow.
|
|
9
|
+
*/
|
|
10
|
+
import { Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
11
|
+
import type { SourceMeta } from "../../capability/types";
|
|
12
|
+
import { shortenPath } from "../../tools/render-utils";
|
|
13
|
+
import { DynamicBorder } from "../components/dynamic-border";
|
|
14
|
+
import { parseCommandArgs } from "../shared";
|
|
15
|
+
import type { InteractiveModeContext } from "../types";
|
|
16
|
+
|
|
17
|
+
export type ScopeValue = "project" | "user";
|
|
18
|
+
|
|
19
|
+
export type ScopeFlagResult = { ok: true; scope: ScopeValue } | { ok: false; error: string };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate the value following a `--scope` flag.
|
|
23
|
+
*/
|
|
24
|
+
export function readScopeFlag(value: string | undefined): ScopeFlagResult {
|
|
25
|
+
if (!value || (value !== "project" && value !== "user")) {
|
|
26
|
+
return { ok: false, error: "Invalid --scope value. Use project or user." };
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, scope: value };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type RemoveArgs = { name: string | undefined; scope: ScopeValue };
|
|
32
|
+
|
|
33
|
+
export type ParseRemoveResult = { ok: true; value: RemoveArgs } | { ok: false; error: string };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse the argument tail of `/<cmd> remove <name> [--scope project|user]`.
|
|
37
|
+
*
|
|
38
|
+
* `rest` is the text after the subcommand keyword. The caller is responsible
|
|
39
|
+
* for emitting the command-specific "<entity> name required" usage hint when
|
|
40
|
+
* `value.name` is undefined.
|
|
41
|
+
*/
|
|
42
|
+
export function parseRemoveArgs(rest: string): ParseRemoveResult {
|
|
43
|
+
const tokens = parseCommandArgs(rest);
|
|
44
|
+
|
|
45
|
+
let name: string | undefined;
|
|
46
|
+
let scope: ScopeValue = "project";
|
|
47
|
+
let i = 0;
|
|
48
|
+
|
|
49
|
+
if (tokens.length > 0 && !tokens[0].startsWith("-")) {
|
|
50
|
+
name = tokens[0];
|
|
51
|
+
i = 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
while (i < tokens.length) {
|
|
55
|
+
const token = tokens[i];
|
|
56
|
+
if (token === "--scope") {
|
|
57
|
+
const r = readScopeFlag(tokens[i + 1]);
|
|
58
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
59
|
+
scope = r.scope;
|
|
60
|
+
i += 2;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
return { ok: false, error: `Unknown option: ${token}` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { ok: true, value: { name, scope } };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Group capability-loaded items by their source provider+path, yielding each
|
|
71
|
+
* group with a display-ready `shortPath`.
|
|
72
|
+
*/
|
|
73
|
+
export function* groupBySource<T>(
|
|
74
|
+
items: Iterable<T>,
|
|
75
|
+
getSource: (item: T) => SourceMeta,
|
|
76
|
+
): Iterable<{ providerName: string; shortPath: string; items: T[] }> {
|
|
77
|
+
const groups = new Map<string, T[]>();
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
const src = getSource(item);
|
|
80
|
+
const key = `${src.providerName}|${src.path}`;
|
|
81
|
+
let group = groups.get(key);
|
|
82
|
+
if (!group) {
|
|
83
|
+
group = [];
|
|
84
|
+
groups.set(key, group);
|
|
85
|
+
}
|
|
86
|
+
group.push(item);
|
|
87
|
+
}
|
|
88
|
+
for (const [key, grouped] of groups) {
|
|
89
|
+
const sepIdx = key.indexOf("|");
|
|
90
|
+
yield {
|
|
91
|
+
providerName: key.slice(0, sepIdx),
|
|
92
|
+
shortPath: shortenPath(key.slice(sepIdx + 1)),
|
|
93
|
+
items: grouped,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Render a message block (DynamicBorder / Text / DynamicBorder) into the chat
|
|
100
|
+
* container and request a render.
|
|
101
|
+
*/
|
|
102
|
+
export function showCommandMessage(ctx: InteractiveModeContext, text: string): void {
|
|
103
|
+
ctx.chatContainer.addChild(new Spacer(1));
|
|
104
|
+
ctx.chatContainer.addChild(new DynamicBorder());
|
|
105
|
+
ctx.chatContainer.addChild(new Text(text, 1, 1));
|
|
106
|
+
ctx.chatContainer.addChild(new DynamicBorder());
|
|
107
|
+
ctx.ui.requestRender();
|
|
108
|
+
}
|
|
@@ -256,12 +256,21 @@ export class CommandController {
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
#copyLastMessage() {
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
this
|
|
259
|
+
const assistantText = this.ctx.session.getLastAssistantText();
|
|
260
|
+
if (assistantText) {
|
|
261
|
+
this.#doCopy(assistantText, "Copied last agent message to clipboard");
|
|
262
262
|
return;
|
|
263
263
|
}
|
|
264
|
-
|
|
264
|
+
|
|
265
|
+
if (!this.ctx.session.hasCopyCandidateAssistantMessage()) {
|
|
266
|
+
const handoffText = this.ctx.session.getLastVisibleHandoffText();
|
|
267
|
+
if (handoffText) {
|
|
268
|
+
this.#doCopy(handoffText, "Copied handoff context to clipboard");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.ctx.showError("No agent messages to copy yet.");
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
#copyCode() {
|
|
@@ -13,9 +13,11 @@ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
|
13
13
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
14
14
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
15
15
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
16
|
+
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
|
16
17
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
17
18
|
import { calculatePromptTokens } from "../../session/compaction/compaction";
|
|
18
|
-
import
|
|
19
|
+
import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
|
|
20
|
+
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
19
21
|
|
|
20
22
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
21
23
|
|
|
@@ -61,6 +63,8 @@ export class EventController {
|
|
|
61
63
|
todo_auto_clear: e => this.#handleTodoAutoClear(e),
|
|
62
64
|
irc_message: e => this.#handleIrcMessage(e),
|
|
63
65
|
notice: e => this.#handleNotice(e),
|
|
66
|
+
thinking_level_changed: async () => {},
|
|
67
|
+
goal_updated: async () => {},
|
|
64
68
|
} satisfies AgentSessionEventHandlers;
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -178,6 +182,17 @@ export class EventController {
|
|
|
178
182
|
this.#renderedCustomMessages.add(signature);
|
|
179
183
|
this.#resetReadGroup();
|
|
180
184
|
this.ctx.addMessageToChat(event.message);
|
|
185
|
+
// Tag-keyed pending-bar refresh: when AgentSession.#handleAgentEvent
|
|
186
|
+
// spliced this dequeued custom message out of #steeringMessages /
|
|
187
|
+
// #followUpMessages (it ran before this emit), the array state is
|
|
188
|
+
// already correct — pendingMessagesContainer just needs to be
|
|
189
|
+
// re-rendered to match. Gated on tag presence so non-queued customs
|
|
190
|
+
// (ttsr-injection, irc:*, async-result, hookMessage) skip the
|
|
191
|
+
// rebuild; their dispatch path never registered a pending chip.
|
|
192
|
+
// Mirrors the user-role refresh at the bottom of this function.
|
|
193
|
+
if (event.message.role === "custom" && readPendingDisplayTag(event.message.details)) {
|
|
194
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
195
|
+
}
|
|
181
196
|
this.ctx.ui.requestRender();
|
|
182
197
|
} else if (event.message.role === "user") {
|
|
183
198
|
const textContent = this.ctx.getUserMessageText(event.message);
|
|
@@ -364,7 +379,15 @@ export class EventController {
|
|
|
364
379
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
365
380
|
this.ctx.streamingMessage = event.message;
|
|
366
381
|
let errorMessage: string | undefined;
|
|
367
|
-
|
|
382
|
+
const aborted = this.ctx.streamingMessage.stopReason === "aborted";
|
|
383
|
+
const silentlyAborted = aborted && isSilentAbort(this.ctx.streamingMessage.errorMessage);
|
|
384
|
+
const ttsrSilenced = aborted && this.ctx.session.isTtsrAbortPending;
|
|
385
|
+
if (aborted && !silentlyAborted && !ttsrSilenced) {
|
|
386
|
+
// Real user-cancel / network / provider abort: surface the standard
|
|
387
|
+
// operator-facing label. AgentSession.#handleAgentEvent already stamped
|
|
388
|
+
// SILENT_ABORT_MARKER for the plan-compact transition before this
|
|
389
|
+
// controller ran, so reaching this branch implies the abort was NOT a
|
|
390
|
+
// silent internal transition.
|
|
368
391
|
const retryAttempt = this.ctx.session.retryAttempt;
|
|
369
392
|
errorMessage =
|
|
370
393
|
retryAttempt > 0
|
|
@@ -372,7 +395,10 @@ export class EventController {
|
|
|
372
395
|
: "Operation aborted";
|
|
373
396
|
this.ctx.streamingMessage.errorMessage = errorMessage;
|
|
374
397
|
}
|
|
375
|
-
if (
|
|
398
|
+
if (silentlyAborted || ttsrSilenced) {
|
|
399
|
+
// Silence the streaming render by downgrading stopReason to "stop" for
|
|
400
|
+
// display only — does NOT mutate the persisted message's stopReason
|
|
401
|
+
// (the marker on errorMessage drives replay-side suppression).
|
|
376
402
|
const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
|
|
377
403
|
this.ctx.streamingComponent.updateContent(msgWithoutAbort);
|
|
378
404
|
} else {
|
|
@@ -522,10 +548,13 @@ export class EventController {
|
|
|
522
548
|
`Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo_write succeeds."}`,
|
|
523
549
|
);
|
|
524
550
|
}
|
|
525
|
-
if (event.toolName === "
|
|
526
|
-
const details = event.result.details as
|
|
527
|
-
if (details) {
|
|
528
|
-
|
|
551
|
+
if (event.toolName === "resolve" && !event.isError) {
|
|
552
|
+
const details = event.result.details as ResolveToolDetails | undefined;
|
|
553
|
+
if (details?.sourceToolName === "plan_approval" && details.action === "apply") {
|
|
554
|
+
const planDetails = details.sourceResultDetails as PlanApprovalDetails | undefined;
|
|
555
|
+
if (planDetails) {
|
|
556
|
+
await this.ctx.handlePlanApproval(planDetails);
|
|
557
|
+
}
|
|
529
558
|
}
|
|
530
559
|
}
|
|
531
560
|
}
|