@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/dist/cli.js +6325 -4420
- package/dist/types/cli/bench-cli.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +6 -3
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/launch/broker-list-order.test.d.ts +1 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +10 -2
- package/dist/types/live/protocol.d.ts +1 -1
- package/dist/types/live/transport.d.ts +6 -19
- package/dist/types/live/visualizer.d.ts +8 -11
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -6
- package/dist/types/modes/types.d.ts +9 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/eval-runner.d.ts +2 -0
- package/dist/types/session/messages.d.ts +2 -0
- package/dist/types/session/session-tools.d.ts +15 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/structured-subagent.d.ts +3 -0
- package/dist/types/task/types.d.ts +11 -11
- package/dist/types/thinking.d.ts +13 -0
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/eval-format/index.d.ts +7 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/julia.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval-format/ruby.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +4 -1
- package/dist/types/tts/streaming-player.d.ts +10 -43
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/web/search/providers/base.d.ts +16 -0
- package/dist/types/web/search/providers/brave.d.ts +2 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
- package/dist/types/web/search/providers/gemini.d.ts +3 -0
- package/dist/types/web/search/providers/jina.d.ts +2 -0
- package/dist/types/web/search/providers/kagi.d.ts +2 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/parallel.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +3 -0
- package/dist/types/web/search/providers/searxng.d.ts +10 -0
- package/dist/types/web/search/providers/tavily.d.ts +8 -0
- package/dist/types/web/search/query.d.ts +190 -0
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/cli/bench-cli.ts +12 -1
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/web-search-cli.ts +7 -0
- package/src/cli.ts +8 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/model-resolver.ts +17 -6
- package/src/config/settings-schema.ts +66 -0
- package/src/debug/raw-sse-buffer.ts +157 -30
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/exec/bash-executor.ts +1 -0
- package/src/export/share.ts +4 -3
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/launch/broker-list-order.test.ts +89 -0
- package/src/launch/broker.ts +49 -8
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +76 -23
- package/src/live/protocol.test.ts +3 -3
- package/src/live/protocol.ts +1 -1
- package/src/live/transport.ts +72 -140
- package/src/live/visualizer.ts +114 -134
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/tool-execution.ts +17 -110
- package/src/modes/controllers/input-controller.ts +55 -47
- package/src/modes/controllers/live-command-controller.ts +82 -5
- package/src/modes/controllers/selector-controller.ts +62 -0
- package/src/modes/interactive-mode.ts +79 -5
- package/src/modes/types.ts +10 -4
- package/src/modes/utils/ui-helpers.ts +7 -8
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/resolve-device-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +5 -2
- package/src/prompts/tools/web-search.md +2 -0
- package/src/sdk.ts +13 -4
- package/src/session/agent-session-types.ts +9 -0
- package/src/session/agent-session.ts +70 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/eval-runner.ts +5 -0
- package/src/session/messages.ts +3 -0
- package/src/session/queued-messages.ts +7 -1
- package/src/session/session-tools.ts +37 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/slash-commands/builtin-registry.ts +165 -9
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +17 -9
- package/src/task/index.ts +14 -34
- package/src/task/structured-subagent.ts +4 -0
- package/src/task/types.ts +15 -13
- package/src/thinking.ts +27 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/ast-edit.ts +4 -1
- package/src/tools/bash-interactive.ts +90 -86
- package/src/tools/bash.ts +13 -0
- package/src/tools/browser/render.ts +2 -1
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/eval-format/index.ts +24 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/julia.ts +446 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval-format/ruby.ts +380 -0
- package/src/tools/eval-render.ts +12 -6
- package/src/tools/index.ts +5 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +10 -1
- package/src/tools/todo.ts +58 -6
- package/src/tools/xdev.ts +54 -26
- package/src/tts/streaming-player.ts +81 -340
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/web/search/index.ts +28 -5
- package/src/web/search/providers/anthropic.ts +62 -4
- package/src/web/search/providers/base.ts +16 -0
- package/src/web/search/providers/brave.ts +30 -3
- package/src/web/search/providers/codex.ts +14 -1
- package/src/web/search/providers/duckduckgo.ts +23 -1
- package/src/web/search/providers/ecosia.ts +5 -1
- package/src/web/search/providers/exa.ts +28 -1
- package/src/web/search/providers/firecrawl.ts +37 -3
- package/src/web/search/providers/gemini.ts +12 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +31 -8
- package/src/web/search/providers/kagi.ts +10 -1
- package/src/web/search/providers/kimi.ts +16 -1
- package/src/web/search/providers/mojeek.ts +14 -1
- package/src/web/search/providers/parallel.ts +48 -2
- package/src/web/search/providers/perplexity.ts +94 -6
- package/src/web/search/providers/searxng.ts +145 -9
- package/src/web/search/providers/startpage.ts +8 -2
- package/src/web/search/providers/synthetic.ts +7 -1
- package/src/web/search/providers/tavily.ts +52 -3
- package/src/web/search/providers/tinyfish.ts +6 -1
- package/src/web/search/providers/xai.ts +49 -2
- package/src/web/search/providers/zai.ts +19 -1
- package/src/web/search/query.ts +850 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/live/audio-worklet.txt +0 -59
- package/src/live/browser-runtime.txt +0 -221
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
package/src/live/visualizer.ts
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
matchesKey,
|
|
4
|
+
replaceTabs,
|
|
5
|
+
sliceWithWidth,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
visibleWidth,
|
|
8
|
+
} from "@oh-my-pi/pi-tui";
|
|
2
9
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
3
10
|
import { type ThemeColor, theme } from "../modes/theme/theme";
|
|
4
11
|
|
|
5
12
|
/** Distinct states of a realtime call connection. */
|
|
6
13
|
export type LivePhase = "connecting" | "listening" | "working" | "speaking" | "muted" | "error";
|
|
7
|
-
|
|
8
|
-
/** A transcribed turn in the realtime call. */
|
|
9
|
-
export interface LiveTranscript {
|
|
10
|
-
role: "user" | "assistant";
|
|
11
|
-
text: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
14
|
/** Configuration callbacks for user interactions in the visualizer. */
|
|
15
15
|
export interface LiveVisualizerOptions {
|
|
16
16
|
onStop(): void;
|
|
17
17
|
onToggleMute(): void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function normalizeTranscript(text: string): string {
|
|
21
|
+
return replaceTabs(sanitizeText(text)).replace(/\s+/g, " ").trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function truncateFromStart(text: string, width: number): string {
|
|
25
|
+
if (width <= 0) return "";
|
|
26
|
+
const textWidth = visibleWidth(text);
|
|
27
|
+
if (textWidth <= width) return text;
|
|
28
|
+
if (width === 1) return "…";
|
|
29
|
+
return `…${sliceWithWidth(text, textWidth - width + 1, width - 1, true).text}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/** A compact, fixed-height terminal component for displaying a realtime call. */
|
|
21
33
|
export class LiveVisualizer implements Component {
|
|
22
34
|
readonly wantsKeyRelease = false;
|
|
@@ -25,19 +37,17 @@ export class LiveVisualizer implements Component {
|
|
|
25
37
|
|
|
26
38
|
#phase: LivePhase = "connecting";
|
|
27
39
|
#inputLevel = 0;
|
|
28
|
-
#
|
|
29
|
-
#transcript: LiveTranscript | undefined;
|
|
40
|
+
#displayLevel = 0;
|
|
30
41
|
#frame = 0;
|
|
42
|
+
#userTranscript = "";
|
|
31
43
|
|
|
32
44
|
#cache:
|
|
33
45
|
| {
|
|
34
46
|
width: number;
|
|
35
47
|
phase: LivePhase;
|
|
36
|
-
|
|
37
|
-
outputLevel: number;
|
|
38
|
-
transcriptRole: string | undefined;
|
|
39
|
-
transcriptText: string | undefined;
|
|
48
|
+
displayLevel: number;
|
|
40
49
|
frame: number;
|
|
50
|
+
userTranscript: string;
|
|
41
51
|
lines: readonly string[];
|
|
42
52
|
}
|
|
43
53
|
| undefined;
|
|
@@ -54,34 +64,43 @@ export class LiveVisualizer implements Component {
|
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
/** Updates the
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Updates the latest transcript fragment. */
|
|
67
|
-
setTranscript(transcript: LiveTranscript | undefined): void {
|
|
68
|
-
if (this.#transcript?.role !== transcript?.role || this.#transcript?.text !== transcript?.text) {
|
|
69
|
-
this.#transcript = transcript ? { ...transcript } : undefined;
|
|
70
|
-
this.invalidate();
|
|
71
|
-
}
|
|
67
|
+
/** Updates the microphone volume level (0..1). */
|
|
68
|
+
setInputLevel(level: number): void {
|
|
69
|
+
const next = Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : 0;
|
|
70
|
+
if (this.#inputLevel === next) return;
|
|
71
|
+
this.#inputLevel = next;
|
|
72
|
+
if (next > this.#displayLevel) this.#displayLevel = next;
|
|
73
|
+
this.invalidate();
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
/**
|
|
76
|
+
/** Advances the spectrum animation and its peak decay. */
|
|
75
77
|
setFrame(frame: number): void {
|
|
76
|
-
|
|
78
|
+
const nextLevel = Math.max(this.#inputLevel, this.#displayLevel * 0.84);
|
|
79
|
+
if (this.#frame !== frame || this.#displayLevel !== nextLevel) {
|
|
77
80
|
this.#frame = frame;
|
|
81
|
+
this.#displayLevel = nextLevel;
|
|
78
82
|
this.invalidate();
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
/** Updates the user's streaming voice transcript. */
|
|
87
|
+
setTranscript(text: string): void {
|
|
88
|
+
const normalized = normalizeTranscript(text);
|
|
89
|
+
if (this.#userTranscript === normalized) return;
|
|
90
|
+
this.#userTranscript = normalized;
|
|
91
|
+
this.invalidate();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Clears the user's voice transcript row. */
|
|
95
|
+
clearTranscript(): void {
|
|
96
|
+
if (!this.#userTranscript) return;
|
|
97
|
+
this.#userTranscript = "";
|
|
98
|
+
this.invalidate();
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
/** Processes user keypresses. */
|
|
83
102
|
handleInput(data: string): void {
|
|
84
|
-
if (matchesKey(data, "escape")) {
|
|
103
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
85
104
|
this.#options.onStop();
|
|
86
105
|
} else if (matchesKey(data, "space")) {
|
|
87
106
|
this.#options.onToggleMute();
|
|
@@ -93,67 +112,61 @@ export class LiveVisualizer implements Component {
|
|
|
93
112
|
this.#cache = undefined;
|
|
94
113
|
}
|
|
95
114
|
|
|
96
|
-
/** Renders the
|
|
115
|
+
/** Renders the microphone spectrum into a compact fixed-height panel. */
|
|
97
116
|
render(width: number): readonly string[] {
|
|
98
117
|
if (
|
|
99
118
|
this.#cache &&
|
|
100
119
|
this.#cache.width === width &&
|
|
101
120
|
this.#cache.phase === this.#phase &&
|
|
102
|
-
this.#cache.
|
|
103
|
-
this.#cache.outputLevel === this.#outputLevel &&
|
|
121
|
+
this.#cache.displayLevel === this.#displayLevel &&
|
|
104
122
|
this.#cache.frame === this.#frame &&
|
|
105
|
-
this.#cache.
|
|
106
|
-
this.#cache.transcriptText === this.#transcript?.text
|
|
123
|
+
this.#cache.userTranscript === this.#userTranscript
|
|
107
124
|
) {
|
|
108
125
|
return this.#cache.lines;
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
const lines = this.#renderLines(width);
|
|
112
|
-
|
|
113
129
|
this.#cache = {
|
|
114
130
|
width,
|
|
115
131
|
phase: this.#phase,
|
|
116
|
-
|
|
117
|
-
outputLevel: this.#outputLevel,
|
|
132
|
+
displayLevel: this.#displayLevel,
|
|
118
133
|
frame: this.#frame,
|
|
119
|
-
|
|
120
|
-
transcriptText: this.#transcript?.text,
|
|
134
|
+
userTranscript: this.#userTranscript,
|
|
121
135
|
lines,
|
|
122
136
|
};
|
|
123
|
-
|
|
124
137
|
return lines;
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
#renderLines(maxWidth: number): readonly string[] {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const inColor: ThemeColor = this.#phase === "muted" ? "dim" : "success";
|
|
140
|
-
const outColor: ThemeColor = this.#phase === "error" ? "error" : "accent";
|
|
141
|
-
|
|
142
|
-
const waveContent = truncateToWidth(
|
|
143
|
-
theme.fg(inColor, inWave) + theme.fg("dim", " │ ") + theme.fg(outColor, outWave),
|
|
144
|
-
innerW,
|
|
145
|
-
);
|
|
146
|
-
const waveLen = visibleWidth(waveContent);
|
|
141
|
+
const width = Math.max(2, maxWidth);
|
|
142
|
+
const innerWidth = width - 2;
|
|
143
|
+
const border = (content: string): string =>
|
|
144
|
+
theme.fg("border", "│") + content + (width > 1 ? theme.fg("border", "│") : "");
|
|
145
|
+
const top = theme.fg("border", `┌${"─".repeat(innerWidth)}${width > 1 ? "┐" : ""}`);
|
|
146
|
+
const spectrumColor: ThemeColor = this.#phase === "muted" ? "dim" : this.#phase === "error" ? "error" : "success";
|
|
147
|
+
const spectrum = this.#generateSpectrum(innerWidth, 2);
|
|
148
|
+
const spectrumRows = spectrum.map(row => border(theme.fg(spectrumColor, row)));
|
|
149
|
+
const transcript = this.#renderTranscript(this.#userTranscript, innerWidth, border);
|
|
150
|
+
return [top, ...spectrumRows, transcript, this.#renderFooter(width, innerWidth)];
|
|
151
|
+
}
|
|
147
152
|
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
waveContent +
|
|
154
|
-
" ".repeat(Math.max(0, wavePadR)) +
|
|
155
|
-
(w > 1 ? theme.fg("border", "│") : "");
|
|
153
|
+
#renderTranscript(transcript: string, innerWidth: number, border: (content: string) => string): string {
|
|
154
|
+
const content = truncateFromStart(transcript, innerWidth);
|
|
155
|
+
const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
|
|
156
|
+
return border(theme.fg("accent", content) + padding);
|
|
157
|
+
}
|
|
156
158
|
|
|
159
|
+
#renderFooter(width: number, innerWidth: number): string {
|
|
160
|
+
const spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
161
|
+
const staticIcons: Record<LivePhase, string> = {
|
|
162
|
+
connecting: "○",
|
|
163
|
+
listening: "●",
|
|
164
|
+
working: "○",
|
|
165
|
+
speaking: "»",
|
|
166
|
+
muted: "×",
|
|
167
|
+
error: "!",
|
|
168
|
+
};
|
|
169
|
+
const icon = this.#phase === "working" ? spinners[this.#frame % spinners.length] : staticIcons[this.#phase];
|
|
157
170
|
const phaseColors: Record<LivePhase, ThemeColor> = {
|
|
158
171
|
connecting: "dim",
|
|
159
172
|
listening: "success",
|
|
@@ -162,73 +175,40 @@ export class LiveVisualizer implements Component {
|
|
|
162
175
|
muted: "dim",
|
|
163
176
|
error: "error",
|
|
164
177
|
};
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
muted: "×",
|
|
177
|
-
error: "!",
|
|
178
|
-
};
|
|
179
|
-
phaseIcon = staticIcons[this.#phase];
|
|
178
|
+
const status = `${icon} ${this.#phase}`;
|
|
179
|
+
const fullLabel = ` ${status} · space mute · esc end `;
|
|
180
|
+
const shortLabel = ` ${status} `;
|
|
181
|
+
const label =
|
|
182
|
+
innerWidth >= visibleWidth(fullLabel) + 1
|
|
183
|
+
? fullLabel
|
|
184
|
+
: innerWidth >= visibleWidth(shortLabel) + 1
|
|
185
|
+
? shortLabel
|
|
186
|
+
: "";
|
|
187
|
+
if (!label) {
|
|
188
|
+
return theme.fg("border", `└${"─".repeat(innerWidth)}${width > 1 ? "┘" : ""}`);
|
|
180
189
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const pPadR = innerW - pLen - pPadL;
|
|
188
|
-
const row2 =
|
|
189
|
-
theme.fg("border", "│") +
|
|
190
|
-
" ".repeat(Math.max(0, pPadL)) +
|
|
191
|
-
coloredPhase +
|
|
192
|
-
" ".repeat(Math.max(0, pPadR)) +
|
|
193
|
-
(w > 1 ? theme.fg("border", "│") : "");
|
|
194
|
-
|
|
195
|
-
const row3 = theme.fg("border", "│") + " ".repeat(Math.max(0, innerW)) + (w > 1 ? theme.fg("border", "│") : "");
|
|
196
|
-
|
|
197
|
-
let transcriptText = "";
|
|
198
|
-
if (this.#transcript) {
|
|
199
|
-
const cleanText = sanitizeText(this.#transcript.text).replace(/[\r\n\t]+/g, " ");
|
|
200
|
-
const roleStr = this.#transcript.role === "user" ? "User: " : "Assistant: ";
|
|
201
|
-
transcriptText = theme.fg("dim", roleStr + cleanText);
|
|
202
|
-
}
|
|
203
|
-
const row4Content = truncateToWidth(` ${transcriptText}`, Math.max(0, innerW - 2)) || "";
|
|
204
|
-
const r4Pad = Math.max(0, innerW - visibleWidth(row4Content));
|
|
205
|
-
const row4 = theme.fg("border", "│") + row4Content + " ".repeat(r4Pad) + (w > 1 ? theme.fg("border", "│") : "");
|
|
206
|
-
|
|
207
|
-
const footerContent = truncateToWidth(" space mute · esc end ", innerW);
|
|
208
|
-
const dashCount = Math.max(0, innerW - visibleWidth(footerContent));
|
|
209
|
-
let bottomBorder: string;
|
|
210
|
-
if (innerW >= visibleWidth(footerContent) + 2) {
|
|
211
|
-
bottomBorder =
|
|
212
|
-
theme.fg("border", "└─") +
|
|
213
|
-
theme.fg("dim", footerContent) +
|
|
214
|
-
theme.fg("border", "─".repeat(dashCount - 2) + (w > 1 ? "┘" : ""));
|
|
215
|
-
} else {
|
|
216
|
-
bottomBorder = theme.fg("border", `└${"─".repeat(Math.max(0, innerW))}${w > 1 ? "┘" : ""}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return [topBorder, row1, row2, row3, row4, bottomBorder];
|
|
190
|
+
const remaining = Math.max(0, innerWidth - visibleWidth(label) - 1);
|
|
191
|
+
return (
|
|
192
|
+
theme.fg("border", "└─") +
|
|
193
|
+
theme.fg(phaseColors[this.#phase], truncateToWidth(label, innerWidth - 1)) +
|
|
194
|
+
theme.fg("border", `${"─".repeat(remaining)}${width > 1 ? "┘" : ""}`)
|
|
195
|
+
);
|
|
220
196
|
}
|
|
221
197
|
|
|
222
|
-
#
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
198
|
+
#generateSpectrum(width: number, rows: number): string[] {
|
|
199
|
+
const blocks = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
200
|
+
const output = Array.from({ length: rows }, () => "");
|
|
201
|
+
const energy = this.#phase === "muted" ? 0 : Math.min(1, Math.sqrt(this.#displayLevel * 5));
|
|
202
|
+
const maxHeight = rows * (blocks.length - 1);
|
|
203
|
+
for (let column = 0; column < width; column += 1) {
|
|
204
|
+
const carrier = 0.5 + 0.5 * Math.sin(this.#frame * 0.43 + column * 0.71);
|
|
205
|
+
const shimmer = 0.5 + 0.5 * Math.sin(this.#frame * 0.19 - column * 1.17);
|
|
206
|
+
const height = Math.round(energy * (0.3 + carrier * 0.5 + shimmer * 0.2) * maxHeight);
|
|
207
|
+
for (let row = 0; row < rows; row += 1) {
|
|
208
|
+
const units = Math.max(0, Math.min(blocks.length - 1, height - (rows - row - 1) * 8));
|
|
209
|
+
output[row] += blocks[units];
|
|
210
|
+
}
|
|
231
211
|
}
|
|
232
|
-
return
|
|
212
|
+
return output;
|
|
233
213
|
}
|
|
234
214
|
}
|
|
@@ -235,6 +235,11 @@ export class AssistantMessageComponent extends Container {
|
|
|
235
235
|
* on a fresh block that has no live token throughput of its own. */
|
|
236
236
|
#thinkingRateLive = false;
|
|
237
237
|
|
|
238
|
+
#textColorTransform?: (text: string) => string;
|
|
239
|
+
|
|
240
|
+
setTextColorTransform(transform?: (text: string) => string): void {
|
|
241
|
+
this.#textColorTransform = transform;
|
|
242
|
+
}
|
|
238
243
|
constructor(
|
|
239
244
|
message?: AssistantMessage,
|
|
240
245
|
private hideThinkingBlock = false,
|
|
@@ -796,8 +801,8 @@ export class AssistantMessageComponent extends Container {
|
|
|
796
801
|
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
797
802
|
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
798
803
|
const trimmed = content.text.trim();
|
|
799
|
-
const
|
|
800
|
-
md
|
|
804
|
+
const mdOptions = this.#textColorTransform ? { color: this.#textColorTransform } : undefined;
|
|
805
|
+
const md = new Markdown(trimmed, 1, 0, getMarkdownTheme(), mdOptions);
|
|
801
806
|
this.#contentContainer.addChild(md);
|
|
802
807
|
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
803
808
|
hasRenderedContent = true;
|
|
@@ -2,7 +2,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
2
2
|
import { Box, Container } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
|
-
import type
|
|
5
|
+
import { type CustomMessage, LIVE_DELEGATION_MESSAGE_TYPE } from "../../session/messages";
|
|
6
6
|
import { renderFramedMessage } from "./message-frame";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -49,12 +49,15 @@ export class CustomMessageComponent extends Container {
|
|
|
49
49
|
// The transcript dispatch routes both `custom` and legacy `hookMessage` roles here:
|
|
50
50
|
// tag hooks with the hook glyph, other injected messages with a neutral package.
|
|
51
51
|
const isHook = (this.message.role as string) === "hookMessage";
|
|
52
|
+
const isLiveDelegation = this.message.customType === LIVE_DELEGATION_MESSAGE_TYPE;
|
|
52
53
|
const custom = renderFramedMessage({
|
|
53
54
|
message: this.message,
|
|
54
55
|
box: this.#box,
|
|
55
56
|
expanded: this.#expanded,
|
|
56
57
|
customRenderer: this.customRenderer,
|
|
57
58
|
icon: isHook ? theme.icon.extensionHook : theme.icon.package,
|
|
59
|
+
hideHeader: isLiveDelegation,
|
|
60
|
+
borderColor: isLiveDelegation ? "borderAccent" : undefined,
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
if (custom) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
12
12
|
import type { Box, Component } from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
14
|
-
import { getMarkdownTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
14
|
+
import { getMarkdownTheme, type Theme, type ThemeColor, theme } from "../../modes/theme/theme";
|
|
15
15
|
|
|
16
16
|
/** Message shape consumed by the shared frame. */
|
|
17
17
|
export interface FramedMessage {
|
|
@@ -36,6 +36,10 @@ export interface RebuildFrameOptions<M extends FramedMessage> {
|
|
|
36
36
|
expanded: boolean;
|
|
37
37
|
/** Icon glyph shown before the customType in the default header (e.g. a hook/extension icon). */
|
|
38
38
|
icon?: string;
|
|
39
|
+
/** Hide the default type header while retaining the message body. */
|
|
40
|
+
hideHeader?: boolean;
|
|
41
|
+
/** Semantic color for the outline, defaulting to the muted border. */
|
|
42
|
+
borderColor?: ThemeColor;
|
|
39
43
|
/** Collapse the markdown body to this many lines when `expanded` is false. Omit to never collapse. */
|
|
40
44
|
collapseAfterLines?: number;
|
|
41
45
|
customRenderer?: FramedRenderer<M>;
|
|
@@ -43,9 +47,9 @@ export interface RebuildFrameOptions<M extends FramedMessage> {
|
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
49
|
* Attempt the custom renderer; on failure or undefined return, populate `box`
|
|
46
|
-
* with the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
50
|
+
* with the configured outline, optional type header, and markdown body. When
|
|
51
|
+
* the custom renderer succeeds, return its Component so the caller can mount
|
|
52
|
+
* it and skip the default box.
|
|
49
53
|
*/
|
|
50
54
|
export function renderFramedMessage<M extends FramedMessage>(opts: RebuildFrameOptions<M>): Component | undefined {
|
|
51
55
|
if (opts.customRenderer) {
|
|
@@ -59,11 +63,13 @@ export function renderFramedMessage<M extends FramedMessage>(opts: RebuildFrameO
|
|
|
59
63
|
|
|
60
64
|
opts.box.clear();
|
|
61
65
|
// Match the skill card: a subtle rounded outline so injected messages read as cards.
|
|
62
|
-
opts.box.setBorder({ chars: theme.boxRound, color: t => theme.fg("borderMuted", t) });
|
|
66
|
+
opts.box.setBorder({ chars: theme.boxRound, color: t => theme.fg(opts.borderColor ?? "borderMuted", t) });
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
if (!opts.hideHeader) {
|
|
69
|
+
const tag = opts.icon ? `${opts.icon} ${opts.message.customType}` : opts.message.customType;
|
|
70
|
+
opts.box.addChild(new Text(theme.fg("customMessageLabel", theme.bold(tag)), 0, 0));
|
|
71
|
+
opts.box.addChild(new Spacer(1));
|
|
72
|
+
}
|
|
67
73
|
|
|
68
74
|
let text: string;
|
|
69
75
|
if (typeof opts.message.content === "string") {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Container, type SelectItem, SelectList, type SgrMouseEvent, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { getSelectListTheme, theme } from "../../modes/theme/theme";
|
|
3
|
+
import type { SessionPinAccount } from "../../slash-commands/helpers/session-pin";
|
|
4
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
5
|
+
|
|
6
|
+
const ACCOUNT_SELECTOR_MAX_VISIBLE = 10;
|
|
7
|
+
const ACCOUNT_LIST_ROW_OFFSET = 4;
|
|
8
|
+
|
|
9
|
+
/** Account picker opened by `/session pin` for the current model provider. */
|
|
10
|
+
export class SessionAccountSelectorComponent extends Container {
|
|
11
|
+
#selectList: SelectList;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
providerName: string,
|
|
15
|
+
accounts: readonly SessionPinAccount[],
|
|
16
|
+
onSelect: (account: SessionPinAccount) => void,
|
|
17
|
+
onCancel: () => void,
|
|
18
|
+
) {
|
|
19
|
+
super();
|
|
20
|
+
const accountsByValue = new Map<string, SessionPinAccount>();
|
|
21
|
+
const items: SelectItem[] = accounts.map(account => {
|
|
22
|
+
const value = String(account.credentialId);
|
|
23
|
+
accountsByValue.set(value, account);
|
|
24
|
+
return {
|
|
25
|
+
value,
|
|
26
|
+
label: account.label,
|
|
27
|
+
description: account.active ? "active for this session" : undefined,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
this.addChild(new DynamicBorder());
|
|
32
|
+
this.addChild(new Spacer(1));
|
|
33
|
+
this.addChild(new TruncatedText(theme.bold(`Select a ${providerName} account for this session:`)));
|
|
34
|
+
this.addChild(new Spacer(1));
|
|
35
|
+
|
|
36
|
+
this.#selectList = new SelectList(
|
|
37
|
+
items,
|
|
38
|
+
Math.min(Math.max(items.length, 1), ACCOUNT_SELECTOR_MAX_VISIBLE),
|
|
39
|
+
getSelectListTheme(),
|
|
40
|
+
);
|
|
41
|
+
const activeIndex = accounts.findIndex(account => account.active);
|
|
42
|
+
if (activeIndex >= 0) this.#selectList.setSelectedIndex(activeIndex);
|
|
43
|
+
this.#selectList.onSelect = item => {
|
|
44
|
+
const account = accountsByValue.get(item.value);
|
|
45
|
+
if (account) onSelect(account);
|
|
46
|
+
};
|
|
47
|
+
this.#selectList.onCancel = onCancel;
|
|
48
|
+
this.addChild(this.#selectList);
|
|
49
|
+
this.addChild(new Spacer(1));
|
|
50
|
+
this.addChild(new DynamicBorder());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Forward keyboard navigation and cancellation when the wrapper owns focus. */
|
|
54
|
+
handleInput(keyData: string): void {
|
|
55
|
+
this.#selectList.handleInput(keyData);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Route mouse selection through the title rows into the account list. */
|
|
59
|
+
routeMouse(event: SgrMouseEvent, line: number, col: number): void {
|
|
60
|
+
this.#selectList.routeMouse(event, line - ACCOUNT_LIST_ROW_OFFSET, col);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -19,25 +19,10 @@ import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "..
|
|
|
19
19
|
import type { Theme } from "../../modes/theme/theme";
|
|
20
20
|
import { getThemeEpoch, theme } from "../../modes/theme/theme";
|
|
21
21
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
22
|
+
import { formatDefaultToolExecution } from "../../tools/default-renderer";
|
|
22
23
|
import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
|
|
23
24
|
import { isWaitingPollDetails } from "../../tools/hub";
|
|
24
|
-
import {
|
|
25
|
-
formatArgsInline,
|
|
26
|
-
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
27
|
-
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
28
|
-
JSON_TREE_MAX_LINES_COLLAPSED,
|
|
29
|
-
JSON_TREE_MAX_LINES_EXPANDED,
|
|
30
|
-
JSON_TREE_SCALAR_LEN_COLLAPSED,
|
|
31
|
-
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
32
|
-
renderJsonTreeLines,
|
|
33
|
-
} from "../../tools/json-tree";
|
|
34
|
-
import {
|
|
35
|
-
formatExpandHint,
|
|
36
|
-
formatStatusIcon,
|
|
37
|
-
replaceTabs,
|
|
38
|
-
resolveImageOptions,
|
|
39
|
-
truncateToWidth,
|
|
40
|
-
} from "../../tools/render-utils";
|
|
25
|
+
import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../../tools/render-utils";
|
|
41
26
|
import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
|
|
42
27
|
import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
|
|
43
28
|
import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
|
|
@@ -402,7 +387,21 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
402
387
|
// strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
|
|
403
388
|
// lines and keep their tight spacing — only tinted lines survive.
|
|
404
389
|
this.#contentBox = new Box(0, 1);
|
|
405
|
-
this.#contentText = new WidthAwareText(
|
|
390
|
+
this.#contentText = new WidthAwareText(
|
|
391
|
+
contentWidth =>
|
|
392
|
+
formatDefaultToolExecution(
|
|
393
|
+
{
|
|
394
|
+
label: this.#toolLabel,
|
|
395
|
+
args: this.#args,
|
|
396
|
+
result: this.#result ? { output: this.#getTextOutput(), isError: this.#result.isError } : undefined,
|
|
397
|
+
options: this.#renderState,
|
|
398
|
+
},
|
|
399
|
+
contentWidth,
|
|
400
|
+
theme,
|
|
401
|
+
),
|
|
402
|
+
1,
|
|
403
|
+
1,
|
|
404
|
+
);
|
|
406
405
|
|
|
407
406
|
// Use Box for custom tools or built-in tools that have renderers
|
|
408
407
|
const hasRenderer = toolName in toolRenderers;
|
|
@@ -1353,96 +1352,4 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
1353
1352
|
|
|
1354
1353
|
return output;
|
|
1355
1354
|
}
|
|
1356
|
-
|
|
1357
|
-
/**
|
|
1358
|
-
* Format a generic tool execution (fallback for tools without custom renderers)
|
|
1359
|
-
*/
|
|
1360
|
-
#formatToolExecution(contentWidth: number): string {
|
|
1361
|
-
const lines: string[] = [];
|
|
1362
|
-
const icon = this.#isPartial
|
|
1363
|
-
? this.#spinnerFrame !== undefined
|
|
1364
|
-
? "running"
|
|
1365
|
-
: "pending"
|
|
1366
|
-
: this.#result?.isError
|
|
1367
|
-
? "error"
|
|
1368
|
-
: "done";
|
|
1369
|
-
lines.push(renderStatusLine({ icon, spinnerFrame: this.#spinnerFrame, title: this.#toolLabel }, theme));
|
|
1370
|
-
|
|
1371
|
-
const argsObject = this.#args && typeof this.#args === "object" ? (this.#args as Record<string, unknown>) : null;
|
|
1372
|
-
if (!this.#expanded && argsObject && Object.keys(argsObject).length > 0) {
|
|
1373
|
-
// Budget the inline preview against the render width, leaving room for
|
|
1374
|
-
// the ` └─ ` connector prefix instead of a fixed cap.
|
|
1375
|
-
const inlineBudget = Math.max(20, contentWidth - Bun.stringWidth(theme.tree.last) - 2);
|
|
1376
|
-
const preview = formatArgsInline(argsObject, inlineBudget);
|
|
1377
|
-
if (preview) {
|
|
1378
|
-
lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
if (this.#expanded && this.#args !== undefined) {
|
|
1383
|
-
lines.push("");
|
|
1384
|
-
lines.push(theme.fg("dim", "Args"));
|
|
1385
|
-
const tree = renderJsonTreeLines(
|
|
1386
|
-
this.#args,
|
|
1387
|
-
theme,
|
|
1388
|
-
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
1389
|
-
JSON_TREE_MAX_LINES_EXPANDED,
|
|
1390
|
-
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
1391
|
-
);
|
|
1392
|
-
lines.push(...tree.lines);
|
|
1393
|
-
if (tree.truncated) {
|
|
1394
|
-
lines.push(theme.fg("dim", "…"));
|
|
1395
|
-
}
|
|
1396
|
-
lines.push("");
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
if (!this.#result) {
|
|
1400
|
-
return lines.join("\n");
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
const textContent = this.#getTextOutput().trimEnd();
|
|
1404
|
-
if (!textContent) {
|
|
1405
|
-
lines.push(theme.fg("dim", "(no output)"));
|
|
1406
|
-
return lines.join("\n");
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
if (textContent.startsWith("{") || textContent.startsWith("[")) {
|
|
1410
|
-
try {
|
|
1411
|
-
const parsed = JSON.parse(textContent);
|
|
1412
|
-
const maxDepth = this.#expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
1413
|
-
const maxLines = this.#expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
1414
|
-
const maxScalarLen = this.#expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
1415
|
-
const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
|
|
1416
|
-
|
|
1417
|
-
if (tree.lines.length > 0) {
|
|
1418
|
-
lines.push(...tree.lines);
|
|
1419
|
-
if (!this.#expanded) {
|
|
1420
|
-
lines.push(formatExpandHint(theme, this.#expanded, true));
|
|
1421
|
-
} else if (tree.truncated) {
|
|
1422
|
-
lines.push(theme.fg("dim", "…"));
|
|
1423
|
-
}
|
|
1424
|
-
return lines.join("\n");
|
|
1425
|
-
}
|
|
1426
|
-
} catch {
|
|
1427
|
-
// Fall through to raw output
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
const outputLines = textContent.split("\n");
|
|
1432
|
-
const maxOutputLines = this.#expanded ? 12 : 4;
|
|
1433
|
-
const displayLines = outputLines.slice(0, maxOutputLines);
|
|
1434
|
-
|
|
1435
|
-
for (const line of displayLines) {
|
|
1436
|
-
lines.push(theme.fg("toolOutput", truncateToWidth(replaceTabs(line), contentWidth)));
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
if (outputLines.length > maxOutputLines) {
|
|
1440
|
-
const remaining = outputLines.length - maxOutputLines;
|
|
1441
|
-
lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, this.#expanded, true)}`);
|
|
1442
|
-
} else if (!this.#expanded) {
|
|
1443
|
-
lines.push(formatExpandHint(theme, this.#expanded, true));
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
return lines.join("\n");
|
|
1447
|
-
}
|
|
1448
1355
|
}
|