@oh-my-pi/pi-coding-agent 15.0.0 → 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 +41 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- 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/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 +1 -4
- package/src/config/settings-schema.ts +71 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- 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/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 +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- 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/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 +5 -6
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +79 -45
- 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 +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 +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- 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/rpc-mode.ts +14 -87
- 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 +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/goal.md +13 -0
- package/src/prompts/tools/hashline.md +102 -114
- package/src/prompts/tools/read.md +1 -0
- 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/executor.ts +17 -7
- 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-interactive.ts +9 -1
- package/src/tools/bash.ts +27 -4
- 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 +15 -4
- package/src/tools/eval.ts +3 -1
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +7 -6
- package/src/tools/index.ts +22 -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 +516 -233
- 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 +44 -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/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
|
@@ -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
|
}
|
|
@@ -443,6 +443,15 @@ export class InputController {
|
|
|
443
443
|
args: args || undefined,
|
|
444
444
|
lineCount: body ? body.split("\n").length : 0,
|
|
445
445
|
};
|
|
446
|
+
// When the agent is streaming, register the compact slash-form text as
|
|
447
|
+
// the pending-display twin BEFORE dispatching the CustomMessage. The
|
|
448
|
+
// returned tag is embedded in details so AgentSession.#handleAgentEvent
|
|
449
|
+
// can remove the matching display entry when the agent consumes this
|
|
450
|
+
// message (mirrors the user-message dequeue path).
|
|
451
|
+
if (this.ctx.session.isStreaming) {
|
|
452
|
+
const tag = this.ctx.session.enqueueCustomMessageDisplay(text, streamingBehavior);
|
|
453
|
+
details.__pendingDisplayTag = tag;
|
|
454
|
+
}
|
|
446
455
|
await this.ctx.session.promptCustomMessage(
|
|
447
456
|
{
|
|
448
457
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
@@ -453,6 +462,10 @@ export class InputController {
|
|
|
453
462
|
},
|
|
454
463
|
{ streamingBehavior },
|
|
455
464
|
);
|
|
465
|
+
if (this.ctx.session.isStreaming) {
|
|
466
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
467
|
+
this.ctx.ui.requestRender();
|
|
468
|
+
}
|
|
456
469
|
} catch (err) {
|
|
457
470
|
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
458
471
|
}
|
|
@@ -37,11 +37,11 @@ import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../
|
|
|
37
37
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
38
38
|
import { shortenPath } from "../../tools/render-utils";
|
|
39
39
|
import { openPath } from "../../utils/open";
|
|
40
|
-
import { DynamicBorder } from "../components/dynamic-border";
|
|
41
40
|
import { MCPAddWizard } from "../components/mcp-add-wizard";
|
|
42
41
|
import { parseCommandArgs } from "../shared";
|
|
43
42
|
import { theme } from "../theme/theme";
|
|
44
43
|
import type { InteractiveModeContext } from "../types";
|
|
44
|
+
import { groupBySource, parseRemoveArgs, readScopeFlag, showCommandMessage } from "./command-controller-shared";
|
|
45
45
|
|
|
46
46
|
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
47
47
|
const { promise: timeoutPromise, reject } = Promise.withResolvers<T>();
|
|
@@ -49,6 +49,22 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string)
|
|
|
49
49
|
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Outcome of {@link MCPCommandController}'s OAuth handler.
|
|
54
|
+
*
|
|
55
|
+
* `clientId`/`clientSecret` are populated when the OAuth provider required (or
|
|
56
|
+
* accepted) dynamic client registration; callers MUST persist them alongside
|
|
57
|
+
* `credentialId` so subsequent token refreshes and reauthorizations can reuse
|
|
58
|
+
* the same registered client. Both are also set when the caller pre-supplied a
|
|
59
|
+
* client id via the wizard or `oauth.clientId` in `mcp.json`, in which case the
|
|
60
|
+
* write-back is a no-op.
|
|
61
|
+
*/
|
|
62
|
+
interface OAuthFlowResult {
|
|
63
|
+
credentialId: string;
|
|
64
|
+
clientId?: string;
|
|
65
|
+
clientSecret?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
type MCPAddScope = "user" | "project";
|
|
53
69
|
type MCPAddTransport = "http" | "sse";
|
|
54
70
|
|
|
@@ -207,11 +223,11 @@ export class MCPCommandController {
|
|
|
207
223
|
break;
|
|
208
224
|
}
|
|
209
225
|
if (argToken === "--scope") {
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
212
|
-
return { scope, error:
|
|
226
|
+
const r = readScopeFlag(tokens[i + 1]);
|
|
227
|
+
if (!r.ok) {
|
|
228
|
+
return { scope, error: r.error };
|
|
213
229
|
}
|
|
214
|
-
scope =
|
|
230
|
+
scope = r.scope;
|
|
215
231
|
i += 2;
|
|
216
232
|
continue;
|
|
217
233
|
}
|
|
@@ -406,7 +422,7 @@ export class MCPCommandController {
|
|
|
406
422
|
|
|
407
423
|
try {
|
|
408
424
|
const oauthClientSecret = finalConfig.oauth?.clientSecret ?? "";
|
|
409
|
-
const
|
|
425
|
+
const oauthResult = await this.#handleOAuthFlow(
|
|
410
426
|
oauth.authorizationUrl,
|
|
411
427
|
oauth.tokenUrl,
|
|
412
428
|
oauth.clientId ?? finalConfig.oauth?.clientId ?? "",
|
|
@@ -416,14 +432,21 @@ export class MCPCommandController {
|
|
|
416
432
|
finalConfig.oauth?.callbackPath,
|
|
417
433
|
finalConfig.oauth?.redirectUri,
|
|
418
434
|
);
|
|
435
|
+
const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? finalConfig.oauth?.clientId;
|
|
436
|
+
const persistedClientSecret = oauthResult.clientSecret ?? finalConfig.oauth?.clientSecret;
|
|
419
437
|
finalConfig = {
|
|
420
438
|
...finalConfig,
|
|
421
439
|
auth: {
|
|
422
440
|
type: "oauth",
|
|
423
|
-
credentialId,
|
|
441
|
+
credentialId: oauthResult.credentialId,
|
|
424
442
|
tokenUrl: oauth.tokenUrl,
|
|
425
|
-
clientId:
|
|
426
|
-
clientSecret:
|
|
443
|
+
clientId: persistedClientId,
|
|
444
|
+
clientSecret: persistedClientSecret,
|
|
445
|
+
},
|
|
446
|
+
oauth: {
|
|
447
|
+
...finalConfig.oauth,
|
|
448
|
+
clientId: persistedClientId ?? finalConfig.oauth?.clientId,
|
|
449
|
+
clientSecret: persistedClientSecret ?? finalConfig.oauth?.clientSecret,
|
|
427
450
|
},
|
|
428
451
|
};
|
|
429
452
|
} catch (oauthError) {
|
|
@@ -488,7 +511,7 @@ export class MCPCommandController {
|
|
|
488
511
|
callbackPort?: number,
|
|
489
512
|
callbackPath?: string,
|
|
490
513
|
redirectUri?: string,
|
|
491
|
-
): Promise<
|
|
514
|
+
): Promise<OAuthFlowResult> {
|
|
492
515
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
493
516
|
let parsedAuthUrl: URL;
|
|
494
517
|
|
|
@@ -600,7 +623,11 @@ export class MCPCommandController {
|
|
|
600
623
|
// Store under a synthetic provider name
|
|
601
624
|
await authStorage.set(credentialId, oauthCredential);
|
|
602
625
|
|
|
603
|
-
return
|
|
626
|
+
return {
|
|
627
|
+
credentialId,
|
|
628
|
+
clientId: flow.resolvedClientId,
|
|
629
|
+
clientSecret: flow.registeredClientSecret,
|
|
630
|
+
};
|
|
604
631
|
} catch (error) {
|
|
605
632
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
606
633
|
|
|
@@ -984,23 +1011,7 @@ export class MCPCommandController {
|
|
|
984
1011
|
|
|
985
1012
|
// Show discovered servers (from .claude.json, .cursor/mcp.json, .vscode/mcp.json, etc.)
|
|
986
1013
|
if (discoveredServers.length > 0) {
|
|
987
|
-
|
|
988
|
-
const bySource = new Map<string, typeof discoveredServers>();
|
|
989
|
-
for (const entry of discoveredServers) {
|
|
990
|
-
const key = `${entry.source.providerName}|${entry.source.path}`;
|
|
991
|
-
let group = bySource.get(key);
|
|
992
|
-
if (!group) {
|
|
993
|
-
group = [];
|
|
994
|
-
bySource.set(key, group);
|
|
995
|
-
}
|
|
996
|
-
group.push(entry);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
for (const [key, entries] of bySource) {
|
|
1000
|
-
const sepIdx = key.indexOf("|");
|
|
1001
|
-
const providerName = key.slice(0, sepIdx);
|
|
1002
|
-
const sourcePath = key.slice(sepIdx + 1);
|
|
1003
|
-
const shortPath = shortenPath(sourcePath);
|
|
1014
|
+
for (const { providerName, shortPath, items: entries } of groupBySource(discoveredServers, e => e.source)) {
|
|
1004
1015
|
lines.push(theme.fg("accent", providerName) + theme.fg("muted", ` (${shortPath}):`));
|
|
1005
1016
|
for (const { name } of entries) {
|
|
1006
1017
|
const state = this.ctx.mcpManager!.getConnectionStatus(name);
|
|
@@ -1037,32 +1048,12 @@ export class MCPCommandController {
|
|
|
1037
1048
|
async #handleRemove(text: string): Promise<void> {
|
|
1038
1049
|
const match = text.match(/^\/mcp\s+(?:remove|rm)\b\s*(.*)$/i);
|
|
1039
1050
|
const rest = match?.[1]?.trim() ?? "";
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
let scope: "project" | "user" = "project";
|
|
1044
|
-
let i = 0;
|
|
1045
|
-
|
|
1046
|
-
if (tokens.length > 0 && !tokens[0].startsWith("-")) {
|
|
1047
|
-
name = tokens[0];
|
|
1048
|
-
i = 1;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
while (i < tokens.length) {
|
|
1052
|
-
const token = tokens[i];
|
|
1053
|
-
if (token === "--scope") {
|
|
1054
|
-
const value = tokens[i + 1];
|
|
1055
|
-
if (!value || (value !== "project" && value !== "user")) {
|
|
1056
|
-
this.ctx.showError("Invalid --scope value. Use project or user.");
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
scope = value;
|
|
1060
|
-
i += 2;
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
this.ctx.showError(`Unknown option: ${token}`);
|
|
1051
|
+
const parsed = parseRemoveArgs(rest);
|
|
1052
|
+
if (!parsed.ok) {
|
|
1053
|
+
this.ctx.showError(parsed.error);
|
|
1064
1054
|
return;
|
|
1065
1055
|
}
|
|
1056
|
+
const { name, scope } = parsed.value;
|
|
1066
1057
|
|
|
1067
1058
|
if (!name) {
|
|
1068
1059
|
this.ctx.showError("Server name required. Usage: /mcp remove <name> [--scope project|user]");
|
|
@@ -1348,7 +1339,7 @@ export class MCPCommandController {
|
|
|
1348
1339
|
|
|
1349
1340
|
this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
|
|
1350
1341
|
|
|
1351
|
-
const
|
|
1342
|
+
const oauthResult = await this.#handleOAuthFlow(
|
|
1352
1343
|
oauth.authorizationUrl,
|
|
1353
1344
|
oauth.tokenUrl,
|
|
1354
1345
|
oauth.clientId ?? found.config.oauth?.clientId ?? "",
|
|
@@ -1359,14 +1350,22 @@ export class MCPCommandController {
|
|
|
1359
1350
|
found.config.oauth?.redirectUri,
|
|
1360
1351
|
);
|
|
1361
1352
|
|
|
1353
|
+
const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? found.config.oauth?.clientId;
|
|
1354
|
+
const persistedClientSecret = oauthResult.clientSecret ?? (oauthClientSecret || undefined);
|
|
1355
|
+
|
|
1362
1356
|
const updated: MCPServerConfig = {
|
|
1363
1357
|
...baseConfig,
|
|
1364
1358
|
auth: {
|
|
1365
1359
|
type: "oauth",
|
|
1366
|
-
credentialId,
|
|
1360
|
+
credentialId: oauthResult.credentialId,
|
|
1367
1361
|
tokenUrl: oauth.tokenUrl,
|
|
1368
|
-
clientId:
|
|
1369
|
-
clientSecret:
|
|
1362
|
+
clientId: persistedClientId,
|
|
1363
|
+
clientSecret: persistedClientSecret,
|
|
1364
|
+
},
|
|
1365
|
+
oauth: {
|
|
1366
|
+
...found.config.oauth,
|
|
1367
|
+
clientId: persistedClientId ?? found.config.oauth?.clientId,
|
|
1368
|
+
clientSecret: persistedClientSecret ?? found.config.oauth?.clientSecret,
|
|
1370
1369
|
},
|
|
1371
1370
|
};
|
|
1372
1371
|
await updateMCPServer(found.filePath, name, updated);
|
|
@@ -1929,10 +1928,6 @@ export class MCPCommandController {
|
|
|
1929
1928
|
* Show a message in the chat
|
|
1930
1929
|
*/
|
|
1931
1930
|
#showMessage(text: string): void {
|
|
1932
|
-
this.ctx
|
|
1933
|
-
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
1934
|
-
this.ctx.chatContainer.addChild(new Text(text, 1, 1));
|
|
1935
|
-
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
1936
|
-
this.ctx.ui.requestRender();
|
|
1931
|
+
showCommandMessage(this.ctx, text);
|
|
1937
1932
|
}
|
|
1938
1933
|
}
|