@herbertgao/pi-subagents 0.16.0 → 0.17.0
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 +18 -0
- package/README.md +36 -17
- package/package.json +3 -3
- package/src/agent-file-toggle.ts +11 -8
- package/src/agent-manager.ts +184 -106
- package/src/cross-extension-rpc.ts +46 -15
- package/src/custom-agents.ts +9 -1
- package/src/index.ts +172 -31
- package/src/invocation-config.ts +17 -0
- package/src/model-resolver.ts +14 -0
- package/src/schedule.ts +8 -0
- package/src/settings.ts +32 -1
- package/src/types.ts +17 -2
- package/src/ui/agent-widget.ts +23 -2
- package/src/ui/conversation-viewer.ts +264 -25
package/src/ui/agent-widget.ts
CHANGED
|
@@ -226,18 +226,31 @@ export function getPromptModeLabel(type: SubagentType): string | undefined {
|
|
|
226
226
|
/** Mode label is not included — callers add it where they want it. */
|
|
227
227
|
export function buildInvocationTags(invocation: AgentInvocation | undefined): {
|
|
228
228
|
modelName?: string
|
|
229
|
+
modelId?: string
|
|
229
230
|
tags: string[]
|
|
230
231
|
} {
|
|
231
232
|
const tags: string[] = []
|
|
232
233
|
if (!invocation) return { tags }
|
|
233
|
-
|
|
234
|
+
const asked = (
|
|
235
|
+
value: string | undefined,
|
|
236
|
+
requested: string | undefined,
|
|
237
|
+
): string | undefined =>
|
|
238
|
+
value && requested && requested !== value
|
|
239
|
+
? `${value} (asked ${requested})`
|
|
240
|
+
: value
|
|
241
|
+
const thinking = asked(invocation.thinking, invocation.requestedThinking)
|
|
242
|
+
if (thinking) tags.push(`thinking: ${thinking}`)
|
|
234
243
|
if (invocation.isolated) tags.push("isolated")
|
|
235
244
|
if (invocation.isolation === "worktree") tags.push("worktree")
|
|
236
245
|
if (invocation.inheritContext) tags.push("inherit context")
|
|
237
246
|
if (invocation.runInBackground) tags.push("background")
|
|
238
247
|
if (invocation.maxTurns != null)
|
|
239
248
|
tags.push(`max turns: ${invocation.maxTurns}`)
|
|
240
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
modelName: asked(invocation.modelName, invocation.requestedModel),
|
|
251
|
+
modelId: asked(invocation.modelId, invocation.requestedModel),
|
|
252
|
+
tags,
|
|
253
|
+
}
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
/** Truncate text to a single line, max `len` chars. */
|
|
@@ -317,6 +330,8 @@ export class AgentWidget {
|
|
|
317
330
|
* supplies the user's `showCost` setting.
|
|
318
331
|
*/
|
|
319
332
|
private showCost: () => boolean = () => false,
|
|
333
|
+
/** Whether running rows show the model and thinking level. */
|
|
334
|
+
private showModel: () => boolean = () => false,
|
|
320
335
|
) {}
|
|
321
336
|
|
|
322
337
|
/**
|
|
@@ -526,6 +541,12 @@ export class AgentWidget {
|
|
|
526
541
|
: ""
|
|
527
542
|
|
|
528
543
|
const parts: string[] = []
|
|
544
|
+
if (this.showModel()) {
|
|
545
|
+
const { modelName, tags } = buildInvocationTags(a.invocation)
|
|
546
|
+
if (modelName) parts.push(modelName)
|
|
547
|
+
const thinkingTag = tags.find((tag) => tag.startsWith("thinking: "))
|
|
548
|
+
if (thinkingTag) parts.push(thinkingTag)
|
|
549
|
+
}
|
|
529
550
|
if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns))
|
|
530
551
|
if (toolUses > 0)
|
|
531
552
|
parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`)
|
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
* Subscribes to session events for real-time streaming updates.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
type AgentSession,
|
|
10
|
+
getMarkdownTheme,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent"
|
|
9
12
|
import {
|
|
10
13
|
type Component,
|
|
11
14
|
Input,
|
|
15
|
+
Markdown,
|
|
16
|
+
type MarkdownOptions,
|
|
17
|
+
type MarkdownTheme,
|
|
12
18
|
matchesKey,
|
|
13
19
|
type TUI,
|
|
14
20
|
truncateToWidth,
|
|
@@ -17,7 +23,7 @@ import {
|
|
|
17
23
|
} from "@earendil-works/pi-tui"
|
|
18
24
|
import { renderAgentName } from "../agent-color.js"
|
|
19
25
|
import { extractText } from "../context.js"
|
|
20
|
-
import type { AgentRecord } from "../types.js"
|
|
26
|
+
import type { AgentRecord, ViewerMarkdownMode } from "../types.js"
|
|
21
27
|
import {
|
|
22
28
|
getLifetimeCost,
|
|
23
29
|
getLifetimeTotal,
|
|
@@ -46,6 +52,114 @@ const MIN_VIEWPORT = 3
|
|
|
46
52
|
/** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
|
|
47
53
|
export const VIEWPORT_HEIGHT_PCT = 70
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Cap on a single tool result or bash output before the viewer elides the rest.
|
|
57
|
+
*
|
|
58
|
+
* The cap is not cosmetic — it bounds render cost. `buildContentLines()` runs on
|
|
59
|
+
* every render *and* on every scroll key (`handleInput` calls it to compute
|
|
60
|
+
* `maxScroll`), so an uncapped 200 KB result costs ~6 ms per keystroke to parse
|
|
61
|
+
* as Markdown, against ~0.5 ms once capped and effectively nothing on a cache
|
|
62
|
+
* hit (best of 5, width 76). 16 KB is roughly a screenful at every terminal size
|
|
63
|
+
* and still ~30x the 500 characters this replaces, which was small enough to cut
|
|
64
|
+
* most real results mid-sentence.
|
|
65
|
+
*/
|
|
66
|
+
export const RESULT_MAX_CHARS = 16_000
|
|
67
|
+
|
|
68
|
+
/** Cycle order for the viewer's `m` key. */
|
|
69
|
+
const MARKDOWN_MODES: readonly ViewerMarkdownMode[] = [
|
|
70
|
+
"off",
|
|
71
|
+
"assistant",
|
|
72
|
+
"all",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
/** Footer labels — short, because the idle footer is already full at 80 columns. */
|
|
76
|
+
const MARKDOWN_MODE_LABELS: Record<ViewerMarkdownMode, string> = {
|
|
77
|
+
off: "raw",
|
|
78
|
+
assistant: "md",
|
|
79
|
+
all: "md+",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Both options keep the renderer from *rewriting* source that only looks like
|
|
84
|
+
* Markdown: without them `3) a / 7) b / 9) c` comes back renumbered `3. 4. 5.`
|
|
85
|
+
* and backslash escapes are normalized away. Neither is a safe edit to make to
|
|
86
|
+
* a tool's output, and both are cheap to switch off.
|
|
87
|
+
*/
|
|
88
|
+
const MARKDOWN_OPTIONS: MarkdownOptions = {
|
|
89
|
+
preserveOrderedListMarkers: true,
|
|
90
|
+
preserveBackslashEscapes: true,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Pi's own Markdown theme when this process has one, else a theme built from the
|
|
95
|
+
* viewer's `Theme`.
|
|
96
|
+
*
|
|
97
|
+
* Preferring pi's is what buys syntax-highlighted code fences (it carries a
|
|
98
|
+
* `highlightCode`), and it keeps this surface consistent with the notification
|
|
99
|
+
* renderer, which uses the same source. It has to be *probed* rather than
|
|
100
|
+
* try/caught around the call: `getMarkdownTheme()` returns arrow functions that
|
|
101
|
+
* read pi's global theme lazily, so an uninitialized theme throws inside
|
|
102
|
+
* `render()` — long after this returns — and takes the overlay with it. That is
|
|
103
|
+
* the case in tests and any embedded session that never called `initTheme()`.
|
|
104
|
+
*/
|
|
105
|
+
function resolveMarkdownTheme(th: Theme): MarkdownTheme {
|
|
106
|
+
try {
|
|
107
|
+
const piTheme = getMarkdownTheme()
|
|
108
|
+
piTheme.heading("probe")
|
|
109
|
+
return piTheme
|
|
110
|
+
} catch {
|
|
111
|
+
return fallbackMarkdownTheme(th)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `Theme` carries only `fg` and `bold`, so the three remaining styles are
|
|
117
|
+
* written as raw SGR. Rendering them as plain text instead would silently drop
|
|
118
|
+
* `*emphasis*`'s markers with nothing in their place, turning a formatting
|
|
119
|
+
* change into a content change.
|
|
120
|
+
*/
|
|
121
|
+
function fallbackMarkdownTheme(th: Theme): MarkdownTheme {
|
|
122
|
+
const sgr = (on: number, off: number) => (text: string) =>
|
|
123
|
+
`\x1b[${on}m${text}\x1b[${off}m`
|
|
124
|
+
return {
|
|
125
|
+
heading: (text) => th.bold(th.fg("accent", text)),
|
|
126
|
+
link: (text) => th.fg("accent", text),
|
|
127
|
+
linkUrl: (text) => th.fg("muted", text),
|
|
128
|
+
code: (text) => th.fg("muted", text),
|
|
129
|
+
codeBlock: (text) => th.fg("muted", text),
|
|
130
|
+
codeBlockBorder: (text) => th.fg("dim", text),
|
|
131
|
+
quote: (text) => th.fg("muted", text),
|
|
132
|
+
quoteBorder: (text) => th.fg("dim", text),
|
|
133
|
+
hr: (text) => th.fg("dim", text),
|
|
134
|
+
listBullet: (text) => th.fg("accent", text),
|
|
135
|
+
bold: (text) => th.bold(text),
|
|
136
|
+
italic: sgr(3, 23),
|
|
137
|
+
underline: sgr(4, 24),
|
|
138
|
+
strikethrough: sgr(9, 29),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Cap `text` at `RESULT_MAX_CHARS`, reporting the elision separately rather than
|
|
144
|
+
* appending it.
|
|
145
|
+
*
|
|
146
|
+
* Separately because the notice is the viewer's chrome, not the tool's output.
|
|
147
|
+
* Appended into the string it becomes content: a cut landing inside a fenced
|
|
148
|
+
* code block — likely, on exactly the large `ctx_execute` results this is for —
|
|
149
|
+
* renders the notice as a line of source inside the fence.
|
|
150
|
+
*/
|
|
151
|
+
function capResult(text: string): { text: string; elided: number } {
|
|
152
|
+
if (text.length <= RESULT_MAX_CHARS) return { text, elided: 0 }
|
|
153
|
+
return {
|
|
154
|
+
text: text.slice(0, RESULT_MAX_CHARS),
|
|
155
|
+
elided: text.length - RESULT_MAX_CHARS,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function truncationNote(elided: number): string {
|
|
160
|
+
return `... (truncated, ${elided} more character${elided === 1 ? "" : "s"})`
|
|
161
|
+
}
|
|
162
|
+
|
|
49
163
|
export class ConversationViewer implements Component {
|
|
50
164
|
private scrollOffset = 0
|
|
51
165
|
private autoScroll = true
|
|
@@ -57,6 +171,20 @@ export class ConversationViewer implements Component {
|
|
|
57
171
|
private keys: ViewerKeys
|
|
58
172
|
/** Steering composer — present while the user is typing a message to the agent. */
|
|
59
173
|
private composer: Input | undefined
|
|
174
|
+
/** Resolved once: pi's Markdown theme is fixed for the life of the process. */
|
|
175
|
+
private readonly markdownTheme: MarkdownTheme
|
|
176
|
+
/** Set by the `m` key. Wins over the setting so `m` works without a persist hook. */
|
|
177
|
+
private markdownModeOverride: ViewerMarkdownMode | undefined
|
|
178
|
+
/**
|
|
179
|
+
* One `Markdown` per message, so its own text/width cache does the work. A
|
|
180
|
+
* fresh instance per render would re-parse the whole transcript on every
|
|
181
|
+
* keystroke — the component caches, but only across calls to the same object.
|
|
182
|
+
* Weak so a compacted-away message doesn't pin its render.
|
|
183
|
+
*/
|
|
184
|
+
private readonly markdownCache = new WeakMap<
|
|
185
|
+
object,
|
|
186
|
+
{ md: Markdown; text: string; failed?: boolean }
|
|
187
|
+
>()
|
|
60
188
|
|
|
61
189
|
constructor(
|
|
62
190
|
private tui: TUI,
|
|
@@ -77,7 +205,19 @@ export class ConversationViewer implements Component {
|
|
|
77
205
|
* cannot change while it is on screen.
|
|
78
206
|
*/
|
|
79
207
|
private showCost = false,
|
|
208
|
+
/**
|
|
209
|
+
* The current `viewerMarkdown` setting. Read live rather than captured,
|
|
210
|
+
* unlike `showCost`: `m` changes it while the overlay is on screen.
|
|
211
|
+
* Omitted → `assistant`.
|
|
212
|
+
*/
|
|
213
|
+
private viewerMarkdown?: () => ViewerMarkdownMode,
|
|
214
|
+
/**
|
|
215
|
+
* Persist a mode chosen with `m`, so the key and `/agents → Settings` mean
|
|
216
|
+
* the same thing. Omitted → `m` still cycles, viewer-locally.
|
|
217
|
+
*/
|
|
218
|
+
private onMarkdownMode?: (mode: ViewerMarkdownMode) => void,
|
|
80
219
|
) {
|
|
220
|
+
this.markdownTheme = resolveMarkdownTheme(theme)
|
|
81
221
|
this.keys = createViewerKeys(keybindings)
|
|
82
222
|
this.unsubscribe = session.subscribe(() => {
|
|
83
223
|
if (this.closed) return
|
|
@@ -94,7 +234,11 @@ export class ConversationViewer implements Component {
|
|
|
94
234
|
return
|
|
95
235
|
}
|
|
96
236
|
|
|
97
|
-
if (
|
|
237
|
+
if (
|
|
238
|
+
matchesKey(data, "escape") ||
|
|
239
|
+
matchesKey(data, "ctrl+c") ||
|
|
240
|
+
matchesKey(data, "q")
|
|
241
|
+
) {
|
|
98
242
|
this.closed = true
|
|
99
243
|
this.done(undefined)
|
|
100
244
|
return
|
|
@@ -123,6 +267,22 @@ export class ConversationViewer implements Component {
|
|
|
123
267
|
}
|
|
124
268
|
return
|
|
125
269
|
}
|
|
270
|
+
|
|
271
|
+
// Cycle raw → assistant-only → everything. The escape hatch that makes
|
|
272
|
+
// Markdown rendering safe to default on: a result the renderer reshapes
|
|
273
|
+
// (a diff, an indented log, a `#`-commented script) is one key from verbatim.
|
|
274
|
+
if (matchesKey(data, "m")) {
|
|
275
|
+
this.stopArmed = false
|
|
276
|
+
const next =
|
|
277
|
+
MARKDOWN_MODES[
|
|
278
|
+
(MARKDOWN_MODES.indexOf(this.markdownMode()) + 1) %
|
|
279
|
+
MARKDOWN_MODES.length
|
|
280
|
+
]
|
|
281
|
+
this.markdownModeOverride = next
|
|
282
|
+
this.onMarkdownMode?.(next)
|
|
283
|
+
this.tui.requestRender()
|
|
284
|
+
return
|
|
285
|
+
}
|
|
126
286
|
if (this.stopArmed) this.stopArmed = false
|
|
127
287
|
|
|
128
288
|
const totalLines = this.buildContentLines(this.lastInnerW).length
|
|
@@ -264,6 +424,12 @@ export class ConversationViewer implements Component {
|
|
|
264
424
|
: th.fg("dim", "x stop"),
|
|
265
425
|
)
|
|
266
426
|
}
|
|
427
|
+
// Abbreviated (`raw`/`md`/`md+`) because the idle footer is already full
|
|
428
|
+
// at 80 columns with steer + stop present, and this group has no
|
|
429
|
+
// degradation step below "drop the line-count readout".
|
|
430
|
+
actions.push(
|
|
431
|
+
th.fg("dim", `m ${MARKDOWN_MODE_LABELS[this.markdownMode()]}`),
|
|
432
|
+
)
|
|
267
433
|
const footerRight = th.fg(
|
|
268
434
|
"dim",
|
|
269
435
|
"↑↓ scroll · PgUp/PgDn or Shift+↑↓ · Esc close",
|
|
@@ -301,6 +467,67 @@ export class ConversationViewer implements Component {
|
|
|
301
467
|
)
|
|
302
468
|
}
|
|
303
469
|
|
|
470
|
+
/** The mode in force: an `m` press, else the setting, else the default. */
|
|
471
|
+
private markdownMode(): ViewerMarkdownMode {
|
|
472
|
+
return this.markdownModeOverride ?? this.viewerMarkdown?.() ?? "assistant"
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Wrap `text` literally — the pre-Markdown path, and the fallback from it. */
|
|
476
|
+
private rawLines(text: string, width: number, dim: boolean): string[] {
|
|
477
|
+
const lines = wrapTextWithAnsi(text, width)
|
|
478
|
+
return dim ? lines.map((l) => this.theme.fg("dim", l)) : lines
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Render `text` as Markdown, reusing this message's component instance. */
|
|
482
|
+
private markdownLines(
|
|
483
|
+
msg: AgentSession["messages"][number],
|
|
484
|
+
text: string,
|
|
485
|
+
width: number,
|
|
486
|
+
dim: boolean,
|
|
487
|
+
): string[] {
|
|
488
|
+
let entry = this.markdownCache.get(msg)
|
|
489
|
+
if (!entry) {
|
|
490
|
+
entry = {
|
|
491
|
+
md: new Markdown(
|
|
492
|
+
text,
|
|
493
|
+
0,
|
|
494
|
+
0,
|
|
495
|
+
this.markdownTheme,
|
|
496
|
+
// Keeps result prose visually receded, the way the raw path's
|
|
497
|
+
// per-line `fg("dim", …)` did. Fenced code is the exception and is
|
|
498
|
+
// left alone deliberately: pi's theme highlights it with its own
|
|
499
|
+
// colors, which this would otherwise flatten.
|
|
500
|
+
dim ? { color: (t: string) => this.theme.fg("dim", t) } : undefined,
|
|
501
|
+
MARKDOWN_OPTIONS,
|
|
502
|
+
),
|
|
503
|
+
text,
|
|
504
|
+
}
|
|
505
|
+
this.markdownCache.set(msg, entry)
|
|
506
|
+
} else if (entry.text !== text) {
|
|
507
|
+
// Streaming: the message object is stable, its text grows. A failed
|
|
508
|
+
// prefix remains unsafe after append-only deltas, so retry only when the
|
|
509
|
+
// content was replaced or truncated.
|
|
510
|
+
const shouldRetry = !text.startsWith(entry.text)
|
|
511
|
+
entry.md.setText(text)
|
|
512
|
+
entry.text = text
|
|
513
|
+
if (shouldRetry) entry.failed = false
|
|
514
|
+
}
|
|
515
|
+
if (entry.failed) return this.rawLines(text, width, dim)
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
return entry.md.render(width)
|
|
519
|
+
} catch {
|
|
520
|
+
// The parser is recursive and this is arbitrary tool output: ~54 nested
|
|
521
|
+
// blockquotes overflow the stack, and no amount of fuzzing proves that is
|
|
522
|
+
// the only such input. `render()` is on the TUI's critical path, so a
|
|
523
|
+
// throw here takes the overlay down for content the literal path shows
|
|
524
|
+
// fine — degrade to that instead, and remember, since the throw would
|
|
525
|
+
// otherwise repeat on every render and every scroll key.
|
|
526
|
+
entry.failed = true
|
|
527
|
+
return this.rawLines(text, width, dim)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
304
531
|
/** Steerable only when a steer handler exists and the agent is still active. */
|
|
305
532
|
private canSteer(): boolean {
|
|
306
533
|
return (
|
|
@@ -360,8 +587,14 @@ export class ConversationViewer implements Component {
|
|
|
360
587
|
}
|
|
361
588
|
|
|
362
589
|
private invocationLine(): string | undefined {
|
|
363
|
-
|
|
364
|
-
|
|
590
|
+
// Canonical id here, short label everywhere else: this overlay is opened to
|
|
591
|
+
// inspect one agent and has the width for it, and two providers can serve
|
|
592
|
+
// models whose short names read alike.
|
|
593
|
+
const { modelName, modelId, tags } = buildInvocationTags(
|
|
594
|
+
this.record.invocation,
|
|
595
|
+
)
|
|
596
|
+
const model = modelId ?? modelName
|
|
597
|
+
const parts = model ? [model, ...tags] : tags
|
|
365
598
|
if (parts.length === 0) return undefined
|
|
366
599
|
return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`)
|
|
367
600
|
}
|
|
@@ -378,6 +611,7 @@ export class ConversationViewer implements Component {
|
|
|
378
611
|
return lines
|
|
379
612
|
}
|
|
380
613
|
|
|
614
|
+
const mode = this.markdownMode()
|
|
381
615
|
let needsSeparator = false
|
|
382
616
|
for (const msg of messages) {
|
|
383
617
|
if (msg.role === "user") {
|
|
@@ -403,12 +637,12 @@ export class ConversationViewer implements Component {
|
|
|
403
637
|
if (needsSeparator) lines.push(th.fg("dim", "───"))
|
|
404
638
|
lines.push(th.bold("[Assistant]"))
|
|
405
639
|
if (textParts.length > 0) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
640
|
+
const text = textParts.join("\n").trim()
|
|
641
|
+
lines.push(
|
|
642
|
+
...(mode === "off"
|
|
643
|
+
? this.rawLines(text, width, false)
|
|
644
|
+
: this.markdownLines(msg, text, width, false)),
|
|
645
|
+
)
|
|
412
646
|
}
|
|
413
647
|
for (const name of toolCalls) {
|
|
414
648
|
lines.push(
|
|
@@ -416,15 +650,19 @@ export class ConversationViewer implements Component {
|
|
|
416
650
|
)
|
|
417
651
|
}
|
|
418
652
|
} else if (msg.role === "toolResult") {
|
|
419
|
-
const text = extractText(msg.content)
|
|
420
|
-
|
|
421
|
-
text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text
|
|
422
|
-
if (!truncated.trim()) continue
|
|
653
|
+
const { text, elided } = capResult(extractText(msg.content).trim())
|
|
654
|
+
if (!text) continue
|
|
423
655
|
if (needsSeparator) lines.push(th.fg("dim", "───"))
|
|
424
656
|
lines.push(th.fg("dim", "[Result]"))
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
657
|
+
lines.push(
|
|
658
|
+
...(mode === "all"
|
|
659
|
+
? this.markdownLines(msg, text, width, true)
|
|
660
|
+
: this.rawLines(text, width, true)),
|
|
661
|
+
)
|
|
662
|
+
if (elided)
|
|
663
|
+
lines.push(
|
|
664
|
+
truncateToWidth(th.fg("dim", truncationNote(elided)), width),
|
|
665
|
+
)
|
|
428
666
|
} else if ((msg as any).role === "bashExecution") {
|
|
429
667
|
const bash = msg as any
|
|
430
668
|
if (needsSeparator) lines.push(th.fg("dim", "───"))
|
|
@@ -432,13 +670,14 @@ export class ConversationViewer implements Component {
|
|
|
432
670
|
truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width),
|
|
433
671
|
)
|
|
434
672
|
if (bash.output?.trim()) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
lines.push(
|
|
441
|
-
|
|
673
|
+
// Same cap as a tool result, never Markdown: command output is the one
|
|
674
|
+
// thing here that is definitionally not authored as Markdown.
|
|
675
|
+
const { text, elided } = capResult(bash.output.trim())
|
|
676
|
+
lines.push(...this.rawLines(text, width, true))
|
|
677
|
+
if (elided)
|
|
678
|
+
lines.push(
|
|
679
|
+
truncateToWidth(th.fg("dim", truncationNote(elided)), width),
|
|
680
|
+
)
|
|
442
681
|
}
|
|
443
682
|
} else {
|
|
444
683
|
continue
|