@oh-my-pi/pi-coding-agent 14.9.9 → 15.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 +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- 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/args.ts +2 -2
- 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 +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- 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 +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -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/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- 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/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 +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- 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/read-tool-group.ts +29 -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 +55 -4
- 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 +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- 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 +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- 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/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- 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/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- 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-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- 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/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- 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/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- 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/gemini.ts +35 -95
- 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
|
@@ -3,7 +3,7 @@ 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
5
|
import { formatDuration, formatNumber, getProjectDir, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import { theme } from "../../../modes/theme/theme";
|
|
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";
|
|
@@ -76,17 +76,65 @@ const modelSegment: StatusLineSegment = {
|
|
|
76
76
|
},
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
function formatGoalBudget(current: number, budget?: number): string {
|
|
80
|
+
const used = formatNumber(current);
|
|
81
|
+
if (budget === undefined) return used;
|
|
82
|
+
return `${used}/${formatNumber(budget)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: boolean }): RenderedSegment {
|
|
86
|
+
const goal = ctx.session.getGoalModeState()?.goal;
|
|
87
|
+
const status = goal?.status ?? (mode.paused ? "paused" : "active");
|
|
88
|
+
|
|
89
|
+
let icon: string = theme.icon.goal;
|
|
90
|
+
let color: ThemeColor = "accent";
|
|
91
|
+
switch (status) {
|
|
92
|
+
case "paused":
|
|
93
|
+
icon = theme.icon.pause || theme.symbol("status.pending");
|
|
94
|
+
color = "warning";
|
|
95
|
+
break;
|
|
96
|
+
case "complete":
|
|
97
|
+
icon = theme.symbol("status.success");
|
|
98
|
+
color = "success";
|
|
99
|
+
break;
|
|
100
|
+
case "budget-limited":
|
|
101
|
+
icon = theme.symbol("status.warning");
|
|
102
|
+
color = "warning";
|
|
103
|
+
break;
|
|
104
|
+
case "dropped":
|
|
105
|
+
icon = theme.symbol("status.aborted");
|
|
106
|
+
color = "dim";
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const parts: string[] = [withIcon(icon, "Goal")];
|
|
113
|
+
const showBudget = ctx.session.settings.get("goal.statusInFooter") === true;
|
|
114
|
+
if (showBudget && goal) {
|
|
115
|
+
parts.push(formatGoalBudget(goal.tokensUsed, goal.tokenBudget));
|
|
116
|
+
}
|
|
117
|
+
return { content: theme.fg(color, parts.join(" ")), visible: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
79
120
|
const modeSegment: StatusLineSegment = {
|
|
80
121
|
id: "mode",
|
|
81
122
|
render(ctx) {
|
|
123
|
+
const pauseSuffix = theme.icon.pause ? ` ${theme.icon.pause}` : " (paused)";
|
|
124
|
+
|
|
82
125
|
const plan = ctx.planMode;
|
|
83
126
|
if (plan && (plan.enabled || plan.paused)) {
|
|
84
|
-
const label = plan.paused ?
|
|
127
|
+
const label = plan.paused ? `Plan${pauseSuffix}` : "Plan";
|
|
85
128
|
const content = withIcon(theme.icon.plan, label);
|
|
86
129
|
const color = plan.paused ? "warning" : "accent";
|
|
87
130
|
return { content: theme.fg(color, content), visible: true };
|
|
88
131
|
}
|
|
89
132
|
|
|
133
|
+
const goal = ctx.goalMode;
|
|
134
|
+
if (goal && (goal.enabled || goal.paused)) {
|
|
135
|
+
return renderGoalMode(ctx, goal);
|
|
136
|
+
}
|
|
137
|
+
|
|
90
138
|
const loop = ctx.loopMode;
|
|
91
139
|
if (loop?.enabled) {
|
|
92
140
|
const content = withIcon(theme.icon.loop, "Loop");
|
|
@@ -216,8 +264,11 @@ const tokenOutSegment: StatusLineSegment = {
|
|
|
216
264
|
const tokenTotalSegment: StatusLineSegment = {
|
|
217
265
|
id: "token_total",
|
|
218
266
|
render(ctx) {
|
|
219
|
-
|
|
220
|
-
|
|
267
|
+
// Excludes cacheRead: that field re-reads the full cached context every
|
|
268
|
+
// turn, making the cumulative sum N×context_size. The dedicated cache_read
|
|
269
|
+
// segment handles cache monitoring; the cost segment handles billing.
|
|
270
|
+
const { input, output, cacheWrite } = ctx.usageStats;
|
|
271
|
+
const total = input + output + cacheWrite;
|
|
221
272
|
if (!total) return { content: "", visible: false };
|
|
222
273
|
|
|
223
274
|
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
|
+
}
|
|
@@ -37,6 +37,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
|
37
37
|
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
|
38
38
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
39
39
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
40
|
+
import { CompactionCancelledError, type CompactionOutcome } from "../../session/compaction";
|
|
40
41
|
import type { NewSessionOptions } from "../../session/session-manager";
|
|
41
42
|
import { outputMeta } from "../../tools/output-meta";
|
|
42
43
|
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
|
@@ -255,12 +256,21 @@ export class CommandController {
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
#copyLastMessage() {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
this
|
|
259
|
+
const assistantText = this.ctx.session.getLastAssistantText();
|
|
260
|
+
if (assistantText) {
|
|
261
|
+
this.#doCopy(assistantText, "Copied last agent message to clipboard");
|
|
261
262
|
return;
|
|
262
263
|
}
|
|
263
|
-
|
|
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.");
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
#copyCode() {
|
|
@@ -1071,16 +1081,16 @@ export class CommandController {
|
|
|
1071
1081
|
this.ctx.ui.requestRender();
|
|
1072
1082
|
}
|
|
1073
1083
|
|
|
1074
|
-
async handleCompactCommand(customInstructions?: string): Promise<
|
|
1084
|
+
async handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
|
|
1075
1085
|
const entries = this.ctx.sessionManager.getEntries();
|
|
1076
1086
|
const messageCount = entries.filter(e => e.type === "message").length;
|
|
1077
1087
|
|
|
1078
1088
|
if (messageCount < 2) {
|
|
1079
1089
|
this.ctx.showWarning("Nothing to compact (no messages yet)");
|
|
1080
|
-
return;
|
|
1090
|
+
return "ok";
|
|
1081
1091
|
}
|
|
1082
1092
|
|
|
1083
|
-
|
|
1093
|
+
return this.executeCompaction(customInstructions, false);
|
|
1084
1094
|
}
|
|
1085
1095
|
|
|
1086
1096
|
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
|
@@ -1098,7 +1108,10 @@ export class CommandController {
|
|
|
1098
1108
|
}
|
|
1099
1109
|
}
|
|
1100
1110
|
|
|
1101
|
-
async executeCompaction(
|
|
1111
|
+
async executeCompaction(
|
|
1112
|
+
customInstructionsOrOptions?: string | CompactOptions,
|
|
1113
|
+
isAuto = false,
|
|
1114
|
+
): Promise<CompactionOutcome> {
|
|
1102
1115
|
if (this.ctx.loadingAnimation) {
|
|
1103
1116
|
this.ctx.loadingAnimation.stop();
|
|
1104
1117
|
this.ctx.loadingAnimation = undefined;
|
|
@@ -1122,6 +1135,7 @@ export class CommandController {
|
|
|
1122
1135
|
this.ctx.statusContainer.addChild(compactingLoader);
|
|
1123
1136
|
this.ctx.ui.requestRender();
|
|
1124
1137
|
|
|
1138
|
+
let outcome: CompactionOutcome = "ok";
|
|
1125
1139
|
try {
|
|
1126
1140
|
const instructions = typeof customInstructionsOrOptions === "string" ? customInstructionsOrOptions : undefined;
|
|
1127
1141
|
const options =
|
|
@@ -1135,10 +1149,12 @@ export class CommandController {
|
|
|
1135
1149
|
this.ctx.statusLine.invalidate();
|
|
1136
1150
|
this.ctx.updateEditorTopBorder();
|
|
1137
1151
|
} catch (error) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1152
|
+
if (error instanceof CompactionCancelledError) {
|
|
1153
|
+
outcome = "cancelled";
|
|
1140
1154
|
this.ctx.showError("Compaction cancelled");
|
|
1141
1155
|
} else {
|
|
1156
|
+
outcome = "failed";
|
|
1157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1142
1158
|
this.ctx.showError(`Compaction failed: ${message}`);
|
|
1143
1159
|
}
|
|
1144
1160
|
} finally {
|
|
@@ -1147,6 +1163,7 @@ export class CommandController {
|
|
|
1147
1163
|
this.ctx.editor.onEscape = originalOnEscape;
|
|
1148
1164
|
}
|
|
1149
1165
|
await this.ctx.flushCompactionQueue({ willRetry: false });
|
|
1166
|
+
return outcome;
|
|
1150
1167
|
}
|
|
1151
1168
|
|
|
1152
1169
|
async handleHandoffCommand(customInstructions?: string): Promise<void> {
|
|
@@ -3,15 +3,21 @@ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ReadToolGroupComponent,
|
|
8
|
+
readArgsHaveTarget,
|
|
9
|
+
readArgsTargetInternalUrl,
|
|
10
|
+
} from "../../modes/components/read-tool-group";
|
|
7
11
|
import { TodoReminderComponent } from "../../modes/components/todo-reminder";
|
|
8
12
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
9
13
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
10
14
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
11
15
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
16
|
+
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
|
12
17
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
13
18
|
import { calculatePromptTokens } from "../../session/compaction/compaction";
|
|
14
|
-
import
|
|
19
|
+
import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
|
|
20
|
+
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
15
21
|
|
|
16
22
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
17
23
|
|
|
@@ -57,6 +63,8 @@ export class EventController {
|
|
|
57
63
|
todo_auto_clear: e => this.#handleTodoAutoClear(e),
|
|
58
64
|
irc_message: e => this.#handleIrcMessage(e),
|
|
59
65
|
notice: e => this.#handleNotice(e),
|
|
66
|
+
thinking_level_changed: async () => {},
|
|
67
|
+
goal_updated: async () => {},
|
|
60
68
|
} satisfies AgentSessionEventHandlers;
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -174,6 +182,17 @@ export class EventController {
|
|
|
174
182
|
this.#renderedCustomMessages.add(signature);
|
|
175
183
|
this.#resetReadGroup();
|
|
176
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
|
+
}
|
|
177
196
|
this.ctx.ui.requestRender();
|
|
178
197
|
} else if (event.message.role === "user") {
|
|
179
198
|
const textContent = this.ctx.getUserMessageText(event.message);
|
|
@@ -274,16 +293,25 @@ export class EventController {
|
|
|
274
293
|
for (const content of this.ctx.streamingMessage.content) {
|
|
275
294
|
if (content.type !== "toolCall") continue;
|
|
276
295
|
if (content.name === "read") {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
component
|
|
281
|
-
|
|
282
|
-
const group = this.#getReadGroup();
|
|
283
|
-
group.updateArgs(content.arguments, content.id);
|
|
284
|
-
this.ctx.pendingTools.set(content.id, group);
|
|
296
|
+
if (!readArgsHaveTarget(content.arguments)) {
|
|
297
|
+
// Args still streaming — defer until path is parseable so we can route to the
|
|
298
|
+
// read group (regular files) vs ToolExecutionComponent (internal URLs).
|
|
299
|
+
// Creating either component now would lock the read into the wrong shape.
|
|
300
|
+
continue;
|
|
285
301
|
}
|
|
286
|
-
|
|
302
|
+
if (!readArgsTargetInternalUrl(content.arguments)) {
|
|
303
|
+
this.#trackReadToolCall(content.id, content.arguments);
|
|
304
|
+
const component = this.ctx.pendingTools.get(content.id);
|
|
305
|
+
if (component) {
|
|
306
|
+
component.updateArgs(content.arguments, content.id);
|
|
307
|
+
} else {
|
|
308
|
+
const group = this.#getReadGroup();
|
|
309
|
+
group.updateArgs(content.arguments, content.id);
|
|
310
|
+
this.ctx.pendingTools.set(content.id, group);
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// Internal URL read falls through to ToolExecutionComponent below.
|
|
287
315
|
}
|
|
288
316
|
|
|
289
317
|
// Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
|
|
@@ -351,7 +379,15 @@ export class EventController {
|
|
|
351
379
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
352
380
|
this.ctx.streamingMessage = event.message;
|
|
353
381
|
let errorMessage: string | undefined;
|
|
354
|
-
|
|
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.
|
|
355
391
|
const retryAttempt = this.ctx.session.retryAttempt;
|
|
356
392
|
errorMessage =
|
|
357
393
|
retryAttempt > 0
|
|
@@ -359,7 +395,10 @@ export class EventController {
|
|
|
359
395
|
: "Operation aborted";
|
|
360
396
|
this.ctx.streamingMessage.errorMessage = errorMessage;
|
|
361
397
|
}
|
|
362
|
-
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).
|
|
363
402
|
const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
|
|
364
403
|
this.ctx.streamingComponent.updateContent(msgWithoutAbort);
|
|
365
404
|
} else {
|
|
@@ -384,7 +423,7 @@ export class EventController {
|
|
|
384
423
|
async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
|
|
385
424
|
this.#updateWorkingMessageFromIntent(event.intent);
|
|
386
425
|
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
|
387
|
-
if (event.toolName === "read") {
|
|
426
|
+
if (event.toolName === "read" && readArgsHaveTarget(event.args) && !readArgsTargetInternalUrl(event.args)) {
|
|
388
427
|
this.#trackReadToolCall(event.toolCallId, event.args);
|
|
389
428
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
390
429
|
if (component) {
|
|
@@ -509,10 +548,13 @@ export class EventController {
|
|
|
509
548
|
`Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo_write succeeds."}`,
|
|
510
549
|
);
|
|
511
550
|
}
|
|
512
|
-
if (event.toolName === "
|
|
513
|
-
const details = event.result.details as
|
|
514
|
-
if (details) {
|
|
515
|
-
|
|
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
|
+
}
|
|
516
558
|
}
|
|
517
559
|
}
|
|
518
560
|
}
|
|
@@ -119,7 +119,10 @@ export class ExtensionUiController {
|
|
|
119
119
|
abort: () => this.ctx.session.abort(),
|
|
120
120
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
121
121
|
shutdown: () => {
|
|
122
|
-
//
|
|
122
|
+
// Defer the actual teardown to the main loop, which calls
|
|
123
|
+
// `checkShutdownRequested()` at idle boundaries so any queued
|
|
124
|
+
// steering / follow-up messages drain first (see issue #1020).
|
|
125
|
+
this.ctx.shutdownRequested = true;
|
|
123
126
|
},
|
|
124
127
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
125
128
|
compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
|
|
@@ -356,7 +359,10 @@ export class ExtensionUiController {
|
|
|
356
359
|
abort: () => this.ctx.session.abort(),
|
|
357
360
|
hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
|
|
358
361
|
shutdown: () => {
|
|
359
|
-
//
|
|
362
|
+
// Defer the actual teardown to the main loop, which calls
|
|
363
|
+
// `checkShutdownRequested()` at idle boundaries so any queued
|
|
364
|
+
// steering / follow-up messages drain first (see issue #1020).
|
|
365
|
+
this.ctx.shutdownRequested = true;
|
|
360
366
|
},
|
|
361
367
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
362
368
|
compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
|