@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
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
7
|
-
import { Container, Loader,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
7
|
+
import { Container, type Loader, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
8
|
+
import { highlightCode, theme } from "../../modes/theme/theme";
|
|
9
|
+
import type { TruncationMeta } from "../../tools/output-meta";
|
|
10
|
+
import {
|
|
11
|
+
buildExecutionFrame,
|
|
12
|
+
buildStatusFooter,
|
|
13
|
+
createCollapsedPreview,
|
|
14
|
+
type ExecutionColorKey,
|
|
15
|
+
type ExecutionStatus,
|
|
16
|
+
resolveExecutionStatus,
|
|
17
|
+
} from "./execution-shared";
|
|
12
18
|
|
|
13
19
|
const PREVIEW_LINES = 20;
|
|
14
20
|
const MAX_DISPLAY_LINE_CHARS = 4000;
|
|
@@ -17,7 +23,7 @@ export type EvalExecutionLanguage = "python" | "js";
|
|
|
17
23
|
|
|
18
24
|
export class EvalExecutionComponent extends Container {
|
|
19
25
|
#outputLines: string[] = [];
|
|
20
|
-
#status:
|
|
26
|
+
#status: ExecutionStatus = "running";
|
|
21
27
|
#exitCode: number | undefined = undefined;
|
|
22
28
|
#loader: Loader;
|
|
23
29
|
#truncation?: TruncationMeta;
|
|
@@ -28,7 +34,7 @@ export class EvalExecutionComponent extends Container {
|
|
|
28
34
|
return this.language === "js" ? "javascript" : "python";
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
#formatHeader(colorKey:
|
|
37
|
+
#formatHeader(colorKey: ExecutionColorKey): Text {
|
|
32
38
|
const prompt = theme.fg(colorKey, theme.bold(">>>"));
|
|
33
39
|
const continuation = theme.fg(colorKey, " ");
|
|
34
40
|
const codeLines = highlightCode(this.code, this.#highlightLang());
|
|
@@ -46,26 +52,13 @@ export class EvalExecutionComponent extends Container {
|
|
|
46
52
|
) {
|
|
47
53
|
super();
|
|
48
54
|
|
|
49
|
-
const colorKey = this.excludeFromContext ? "dim" : "pythonMode";
|
|
50
|
-
const
|
|
55
|
+
const colorKey: ExecutionColorKey = this.excludeFromContext ? "dim" : "pythonMode";
|
|
56
|
+
const { contentContainer, loader } = buildExecutionFrame(this, ui, colorKey);
|
|
57
|
+
this.#contentContainer = contentContainer;
|
|
58
|
+
this.#loader = loader;
|
|
51
59
|
|
|
52
|
-
this.addChild(new Spacer(1));
|
|
53
|
-
this.addChild(new DynamicBorder(borderColor));
|
|
54
|
-
|
|
55
|
-
this.#contentContainer = new Container();
|
|
56
|
-
this.addChild(this.#contentContainer);
|
|
57
60
|
this.#contentContainer.addChild(this.#formatHeader(colorKey));
|
|
58
|
-
|
|
59
|
-
this.#loader = new Loader(
|
|
60
|
-
ui,
|
|
61
|
-
spinner => theme.fg(colorKey, spinner),
|
|
62
|
-
text => theme.fg("muted", text),
|
|
63
|
-
`Running… (esc to cancel)`,
|
|
64
|
-
getSymbolTheme().spinnerFrames,
|
|
65
|
-
);
|
|
66
61
|
this.#contentContainer.addChild(this.#loader);
|
|
67
|
-
|
|
68
|
-
this.addChild(new DynamicBorder(borderColor));
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
setExpanded(expanded: boolean): void {
|
|
@@ -99,11 +92,7 @@ export class EvalExecutionComponent extends Container {
|
|
|
99
92
|
options?: { output?: string; truncation?: TruncationMeta },
|
|
100
93
|
): void {
|
|
101
94
|
this.#exitCode = exitCode;
|
|
102
|
-
this.#status = cancelled
|
|
103
|
-
? "cancelled"
|
|
104
|
-
: exitCode !== 0 && exitCode !== undefined && exitCode !== null
|
|
105
|
-
? "error"
|
|
106
|
-
: "complete";
|
|
95
|
+
this.#status = resolveExecutionStatus(exitCode, cancelled);
|
|
107
96
|
this.#truncation = options?.truncation;
|
|
108
97
|
if (options?.output !== undefined) {
|
|
109
98
|
this.#setOutput(options.output);
|
|
@@ -120,7 +109,7 @@ export class EvalExecutionComponent extends Container {
|
|
|
120
109
|
|
|
121
110
|
this.#contentContainer.clear();
|
|
122
111
|
|
|
123
|
-
const colorKey = this.excludeFromContext ? "dim" : "pythonMode";
|
|
112
|
+
const colorKey: ExecutionColorKey = this.excludeFromContext ? "dim" : "pythonMode";
|
|
124
113
|
this.#contentContainer.addChild(this.#formatHeader(colorKey));
|
|
125
114
|
|
|
126
115
|
if (availableLines.length > 0) {
|
|
@@ -129,39 +118,20 @@ export class EvalExecutionComponent extends Container {
|
|
|
129
118
|
this.#contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
|
130
119
|
} else {
|
|
131
120
|
const styledOutput = previewLogicalLines.map(line => theme.fg("muted", line)).join("\n");
|
|
132
|
-
|
|
133
|
-
this.#contentContainer.addChild({
|
|
134
|
-
render: (width: number) => {
|
|
135
|
-
const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
|
|
136
|
-
return visualLines;
|
|
137
|
-
},
|
|
138
|
-
invalidate: () => {},
|
|
139
|
-
});
|
|
121
|
+
this.#contentContainer.addChild(createCollapsedPreview(`\n${styledOutput}`, PREVIEW_LINES));
|
|
140
122
|
}
|
|
141
123
|
}
|
|
142
124
|
|
|
143
125
|
if (this.#status === "running") {
|
|
144
126
|
this.#contentContainer.addChild(this.#loader);
|
|
145
127
|
} else {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (this.#
|
|
153
|
-
statusParts.push(theme.fg("warning", "(cancelled)"));
|
|
154
|
-
} else if (this.#status === "error") {
|
|
155
|
-
statusParts.push(theme.fg("error", `(exit ${this.#exitCode})`));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (this.#truncation) {
|
|
159
|
-
statusParts.push(theme.fg("warning", formatTruncationMetaNotice(this.#truncation)));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (statusParts.length > 0) {
|
|
163
|
-
this.#contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0));
|
|
164
|
-
}
|
|
128
|
+
const footer = buildStatusFooter({
|
|
129
|
+
status: this.#status,
|
|
130
|
+
exitCode: this.#exitCode,
|
|
131
|
+
truncation: this.#truncation,
|
|
132
|
+
hiddenLineCount,
|
|
133
|
+
});
|
|
134
|
+
if (footer) this.#contentContainer.addChild(footer);
|
|
165
135
|
}
|
|
166
136
|
}
|
|
167
137
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared rendering primitives for bash/eval execution components.
|
|
3
|
+
*
|
|
4
|
+
* Each helper isolates a piece of structure both components share verbatim
|
|
5
|
+
* (frame layout, collapsed preview, post-run status line). Differences in
|
|
6
|
+
* how each component prepares its header, output lines, or sixel masking
|
|
7
|
+
* stay in their respective files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Component, Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
11
|
+
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
12
|
+
import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
|
|
13
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
14
|
+
import { truncateToVisualLines } from "./visual-truncate";
|
|
15
|
+
|
|
16
|
+
export type ExecutionStatus = "running" | "complete" | "cancelled" | "error";
|
|
17
|
+
|
|
18
|
+
/** Theme color keys valid for an execution frame. */
|
|
19
|
+
export type ExecutionColorKey = "dim" | "bashMode" | "pythonMode";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the spacer + top border + content container + bottom border scaffold
|
|
23
|
+
* that bash and eval execution components share. The caller appends the
|
|
24
|
+
* header (command vs `>>>` prompt) and the returned loader to
|
|
25
|
+
* `contentContainer` so per-mode order is preserved.
|
|
26
|
+
*/
|
|
27
|
+
export function buildExecutionFrame(
|
|
28
|
+
parent: Container,
|
|
29
|
+
ui: TUI,
|
|
30
|
+
colorKey: ExecutionColorKey,
|
|
31
|
+
): { contentContainer: Container; loader: Loader } {
|
|
32
|
+
const borderColor = (str: string) => theme.fg(colorKey, str);
|
|
33
|
+
|
|
34
|
+
parent.addChild(new Spacer(1));
|
|
35
|
+
parent.addChild(new DynamicBorder(borderColor));
|
|
36
|
+
|
|
37
|
+
const contentContainer = new Container();
|
|
38
|
+
parent.addChild(contentContainer);
|
|
39
|
+
|
|
40
|
+
const loader = new Loader(
|
|
41
|
+
ui,
|
|
42
|
+
spinner => theme.fg(colorKey, spinner),
|
|
43
|
+
text => theme.fg("muted", text),
|
|
44
|
+
`Running… (esc to cancel)`,
|
|
45
|
+
getSymbolTheme().spinnerFrames,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
parent.addChild(new DynamicBorder(borderColor));
|
|
49
|
+
return { contentContainer, loader };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wrap a styled preview block in a render-time visual-line truncator.
|
|
54
|
+
* Recomputed per render width so wrapping stays in sync with terminal size.
|
|
55
|
+
*/
|
|
56
|
+
export function createCollapsedPreview(previewText: string, previewLines: number): Component {
|
|
57
|
+
return {
|
|
58
|
+
render: (width: number) => truncateToVisualLines(previewText, previewLines, width, 1).visualLines,
|
|
59
|
+
invalidate: () => {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the post-run status block (hidden-line hint, exit/cancel marker,
|
|
65
|
+
* truncation notice). Returns undefined when there is nothing to display so
|
|
66
|
+
* callers can skip appending a stray Text child.
|
|
67
|
+
*/
|
|
68
|
+
export function buildStatusFooter(opts: {
|
|
69
|
+
status: ExecutionStatus;
|
|
70
|
+
exitCode: number | undefined;
|
|
71
|
+
truncation: TruncationMeta | undefined;
|
|
72
|
+
hiddenLineCount: number;
|
|
73
|
+
/** Suppress the "… N more lines" hint (used when sixel passthrough renders the full output). */
|
|
74
|
+
suppressHiddenCount?: boolean;
|
|
75
|
+
}): Text | undefined {
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
|
|
78
|
+
if (opts.hiddenLineCount > 0 && !opts.suppressHiddenCount) {
|
|
79
|
+
parts.push(theme.fg("dim", `… ${opts.hiddenLineCount} more lines (ctrl+o to expand)`));
|
|
80
|
+
}
|
|
81
|
+
if (opts.status === "cancelled") {
|
|
82
|
+
parts.push(theme.fg("warning", "(cancelled)"));
|
|
83
|
+
} else if (opts.status === "error") {
|
|
84
|
+
parts.push(theme.fg("error", `(exit ${opts.exitCode})`));
|
|
85
|
+
}
|
|
86
|
+
if (opts.truncation) {
|
|
87
|
+
parts.push(theme.fg("warning", formatTruncationMetaNotice(opts.truncation)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (parts.length === 0) return undefined;
|
|
91
|
+
return new Text(`\n${parts.join("\n")}`, 1, 0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Derive the post-run status from an exit code + cancellation flag using the
|
|
96
|
+
* same precedence both execution components apply.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveExecutionStatus(exitCode: number | undefined, cancelled: boolean): ExecutionStatus {
|
|
99
|
+
if (cancelled) return "cancelled";
|
|
100
|
+
if (exitCode !== 0 && exitCode !== undefined && exitCode !== null) return "error";
|
|
101
|
+
return "complete";
|
|
102
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
2
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { Box, Container,
|
|
2
|
+
import { Box, Container, Spacer } from "@oh-my-pi/pi-tui";
|
|
4
3
|
import type { HookMessageRenderer } from "../../extensibility/hooks/types";
|
|
5
|
-
import {
|
|
4
|
+
import { theme } from "../../modes/theme/theme";
|
|
6
5
|
import type { HookMessage } from "../../session/messages";
|
|
6
|
+
import { renderFramedMessage } from "./message-frame";
|
|
7
|
+
|
|
8
|
+
/** Lines of default markdown body shown before the "…" fold when collapsed. */
|
|
9
|
+
const HOOK_COLLAPSED_LINES = 5;
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Component that renders a custom message entry from hooks.
|
|
@@ -41,60 +44,25 @@ export class HookMessageComponent extends Container {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
#rebuild(): void {
|
|
44
|
-
// Remove previous content component
|
|
45
47
|
if (this.#customComponent) {
|
|
46
48
|
this.removeChild(this.#customComponent);
|
|
47
49
|
this.#customComponent = undefined;
|
|
48
50
|
}
|
|
49
51
|
this.removeChild(this.#box);
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.addChild(component);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// Fall through to default rendering
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Default rendering uses our box
|
|
67
|
-
this.addChild(this.#box);
|
|
68
|
-
this.#box.clear();
|
|
69
|
-
|
|
70
|
-
// Default rendering: label + content
|
|
71
|
-
const label = theme.fg("customMessageLabel", theme.bold(`[${this.message.customType}]`));
|
|
72
|
-
this.#box.addChild(new Text(label, 0, 0));
|
|
73
|
-
this.#box.addChild(new Spacer(1));
|
|
53
|
+
const custom = renderFramedMessage({
|
|
54
|
+
message: this.message,
|
|
55
|
+
box: this.#box,
|
|
56
|
+
expanded: this.#expanded,
|
|
57
|
+
customRenderer: this.customRenderer,
|
|
58
|
+
collapseAfterLines: HOOK_COLLAPSED_LINES,
|
|
59
|
+
});
|
|
74
60
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
text = this.message.content;
|
|
61
|
+
if (custom) {
|
|
62
|
+
this.#customComponent = custom;
|
|
63
|
+
this.addChild(custom);
|
|
79
64
|
} else {
|
|
80
|
-
|
|
81
|
-
.filter((c): c is TextContent => c.type === "text")
|
|
82
|
-
.map(c => c.text)
|
|
83
|
-
.join("\n");
|
|
65
|
+
this.addChild(this.#box);
|
|
84
66
|
}
|
|
85
|
-
|
|
86
|
-
// Limit lines when collapsed
|
|
87
|
-
if (!this.#expanded) {
|
|
88
|
-
const lines = text.split("\n");
|
|
89
|
-
if (lines.length > 5) {
|
|
90
|
-
text = `${lines.slice(0, 5).join("\n")}\n…`;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
this.#box.addChild(
|
|
95
|
-
new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
96
|
-
color: (text: string) => theme.fg("customMessageText", text),
|
|
97
|
-
}),
|
|
98
|
-
);
|
|
99
67
|
}
|
|
100
68
|
}
|
|
@@ -47,6 +47,18 @@ type WizardStep =
|
|
|
47
47
|
| "scope"
|
|
48
48
|
| "confirm";
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Result of the wizard's OAuth callback. `credentialId` is mandatory;
|
|
52
|
+
* `clientId`/`clientSecret` are populated when the OAuth provider performed
|
|
53
|
+
* dynamic client registration (or when the caller pre-supplied them) so the
|
|
54
|
+
* wizard can fold them into the final `mcp.json` entry for refresh.
|
|
55
|
+
*/
|
|
56
|
+
export interface MCPAddWizardOAuthResult {
|
|
57
|
+
credentialId: string;
|
|
58
|
+
clientId?: string;
|
|
59
|
+
clientSecret?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
interface WizardState {
|
|
51
63
|
name: string;
|
|
52
64
|
transport: TransportType | null;
|
|
@@ -104,7 +116,13 @@ export class MCPAddWizard extends Container {
|
|
|
104
116
|
#onCompleteCallback: (name: string, config: MCPServerConfig, scope: Scope) => void;
|
|
105
117
|
#onCancelCallback: () => void;
|
|
106
118
|
#onOAuthCallback:
|
|
107
|
-
| ((
|
|
119
|
+
| ((
|
|
120
|
+
authUrl: string,
|
|
121
|
+
tokenUrl: string,
|
|
122
|
+
clientId: string,
|
|
123
|
+
clientSecret: string,
|
|
124
|
+
scopes: string,
|
|
125
|
+
) => Promise<MCPAddWizardOAuthResult>)
|
|
108
126
|
| null = null;
|
|
109
127
|
#onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
|
|
110
128
|
#onRenderCallback: (() => void) | null = null;
|
|
@@ -118,7 +136,7 @@ export class MCPAddWizard extends Container {
|
|
|
118
136
|
clientId: string,
|
|
119
137
|
clientSecret: string,
|
|
120
138
|
scopes: string,
|
|
121
|
-
) => Promise<
|
|
139
|
+
) => Promise<MCPAddWizardOAuthResult>,
|
|
122
140
|
onTestConnection?: (config: MCPServerConfig) => Promise<void>,
|
|
123
141
|
onRender?: () => void,
|
|
124
142
|
initialName?: string,
|
|
@@ -1120,7 +1138,7 @@ export class MCPAddWizard extends Container {
|
|
|
1120
1138
|
|
|
1121
1139
|
try {
|
|
1122
1140
|
// Call OAuth handler
|
|
1123
|
-
const
|
|
1141
|
+
const oauthResult = await this.#onOAuthCallback(
|
|
1124
1142
|
this.#state.oauthAuthUrl,
|
|
1125
1143
|
this.#state.oauthTokenUrl,
|
|
1126
1144
|
this.#state.oauthClientId,
|
|
@@ -1128,8 +1146,11 @@ export class MCPAddWizard extends Container {
|
|
|
1128
1146
|
this.#state.oauthScopes,
|
|
1129
1147
|
);
|
|
1130
1148
|
|
|
1131
|
-
// Store credential ID
|
|
1132
|
-
|
|
1149
|
+
// Store credential ID + any dynamically-registered client credentials,
|
|
1150
|
+
// so the final mcp.json entry persists everything needed for refresh.
|
|
1151
|
+
this.#state.oauthCredentialId = oauthResult.credentialId;
|
|
1152
|
+
if (oauthResult.clientId) this.#state.oauthClientId = oauthResult.clientId;
|
|
1153
|
+
if (oauthResult.clientSecret) this.#state.oauthClientSecret = oauthResult.clientSecret;
|
|
1133
1154
|
|
|
1134
1155
|
// Show success message
|
|
1135
1156
|
this.#contentContainer.clear();
|
|
@@ -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
|
|
|
@@ -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));
|