@agimon-ai/doompi-runner 0.0.1-alpha.11 → 0.0.1-alpha.12

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.
@@ -1,5 +1,5 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../commands/bash/responseEnvelope.cjs");let n=require("node:os");n=e.__toESM(n,1);let r=require("@earendil-works/pi-coding-agent"),i=require("@earendil-works/pi-tui");var a=class{lines;layout;constructor(e,t){this.lines=e,this.layout=t}render(e){if(e<=0)return[];let t=Math.max(1,e-2),n=this.layout.wrap?this.lines.flatMap(e=>(0,i.wrapTextWithAnsi)(e,t)):this.lines.map(e=>(0,i.truncateToWidth)(e,t,`…`));this.layout.separator&&n.unshift(this.layout.separator(`─`.repeat(t)));let r=n.map(e=>` ${e}`);return this.layout.trailingBlank&&r.push(``),r}invalidate(){}};function o(e,t,n){return new a(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const s=/\[[0-9;]*m/;function c(e,t,n){return s.test(e)?e:n.fg(t,e)}function l(e,t=n.default.homedir()){return t.length>0?e.split(t).join(`~`):e}function u(e){let t=e.split(`
2
- `).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function d(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function f(e){return l(u(e))}function p(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.alarm!==void 0&&t.push(`alarm ${e.alarm}s`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function m(e,t,n=r.highlightCode){let i=n(f(e.command),`bash`).join(` `),o=t.fg(`toolTitle`,t.bold(`[bash] `))+i,s=p(e);return s.length>0&&(o+=` ${t.fg(`muted`,`· ${s.join(` · `)}`)}`),new a([o],{wrap:!0})}function h(e){let n=[];return e.lines!==void 0&&n.push(`${e.lines.toLocaleString(`en-US`)} lines`),e.fileSize!==void 0&&n.push(t.formatSize(e.fileSize)),e.runner!==void 0&&n.push(e.runner),n.join(` · `)}function g(e,t){let n=(e.tail??``).split(`
3
- `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return t||n.length<=12?n:n.slice(-12)}function _(e){return(e.content??[]).map(e=>e.text??``).join(``)}function v(e,t){let n=e.split(`
4
- `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function y(e,t,n){let r=e.details??{};if(t.isPartial){let t=v(_(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return o([...t.map(e=>c(e,`toolOutput`,n)),r],!1,n)}if(e.isError===!0){let i=v(_(e),t.expanded?2**53-1:12),a=h(r),s=a.length>0?` ${a}`:``;return o([...i.map(e=>c(e,`toolOutput`,n)),n.fg(`error`,`✗`)+n.fg(`dim`,s)],t.expanded,n)}if(r.promoted===!0){let e=r.runner??r.id??`runner`,t=r.id===void 0?``:` · ${r.id}`;return o([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=g(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,s=r.timedOut===!0||a&&r.exitCode!==0,l=s?n.fg(`error`,`✗`):n.fg(`success`,`✓`),u=[];r.timedOut===!0?u.push(`timed out`):s&&u.push(`exit ${r.exitCode}`);let d=h(r);d.length>0&&u.push(d),!t.expanded&&(r.tailLines??0)>i.length&&u.push(`ctrl+o`);let f=i.map(e=>c(e,`toolOutput`,n));return f.push(l+n.fg(`dim`,u.length>0?` ${u.join(` · `)}`:``)),o(f,t.expanded,n)}exports.abbreviateHome=l,exports.collapseCommand=u,exports.formatBashCommand=f,exports.formatBashFlags=p,exports.formatResultSummary=h,exports.renderBashCall=m,exports.renderBashResult=y,exports.truncateMiddle=d;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../commands/bash/responseEnvelope.cjs");let n=require("node:os");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-ui/toolChrome"),i=require("@earendil-works/pi-coding-agent"),a=require("@earendil-works/pi-tui");var o=class{lines;layout;constructor(e,t){this.lines=e,this.layout=t}render(e){if(e<=0)return[];let t=Math.max(1,e-2),n=this.layout.wrap?this.lines.flatMap(e=>(0,a.wrapTextWithAnsi)(e,t)):this.lines.map(e=>(0,a.truncateToWidth)(e,t,`…`));this.layout.separator&&n.unshift(this.layout.separator(`─`.repeat(t)));let r=n.map(e=>` ${e}`);return this.layout.trailingBlank&&r.push(``),r}invalidate(){}};function s(e,t,n){return new o(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const c=/\u001b\[[0-9;]*m/;function l(e,t,n){return c.test(e)?e:n.fg(t,e)}function u(e,t=n.default.homedir()){return t.length>0?e.split(t).join(`~`):e}function d(e){let t=e.split(`
2
+ `).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function f(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function p(e){return u(d(e))}function m(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.alarm!==void 0&&t.push(`alarm ${e.alarm}s`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function h(e,t,n=i.highlightCode){let a=n(p(e.command),`bash`).join(` `),o=`${(0,r.renderToolBadge)(`bash`,t)} ${a}`,s=m(e);return s.length>0&&(o+=` ${t.fg(`muted`,`· ${s.join(` · `)}`)}`),new r.DoomToolCall(o)}function g(e){let n=[];return e.lines!==void 0&&n.push(`${e.lines.toLocaleString(`en-US`)} lines`),e.fileSize!==void 0&&n.push(t.formatSize(e.fileSize)),e.runner!==void 0&&n.push(e.runner),n.join(` · `)}function _(e,t){let n=(e.tail??``).split(`
3
+ `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return t||n.length<=12?n:n.slice(-12)}function v(e){return(e.content??[]).map(e=>e.text??``).join(``)}function y(e,t){let n=e.split(`
4
+ `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function b(e,t,n){let r=e.details??{};if(t.isPartial){let t=y(v(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return s([...t.map(e=>l(e,`toolOutput`,n)),r],!1,n)}if(e.isError===!0){let i=y(v(e),t.expanded?2**53-1:12),a=g(r),o=a.length>0?` ${a}`:` failed`;return s([...i.map(e=>l(e,`toolOutput`,n)),n.fg(`error`,`✗`)+n.fg(`dim`,o)],t.expanded,n)}if(r.promoted===!0){let e=r.runner??r.id??`runner`,t=r.id===void 0?``:` · ${r.id}`;return s([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=_(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,o=r.timedOut===!0||a&&r.exitCode!==0,c=o?n.fg(`error`,`✗`):n.fg(`success`,`✓`),u=[];r.timedOut===!0?u.push(`timed out`):o&&u.push(`exit ${r.exitCode}`);let d=g(r);d.length>0&&u.push(d),!t.expanded&&(r.tailLines??0)>i.length&&u.push(`ctrl+o`);let f=i.map(e=>l(e,`toolOutput`,n)),p=u.length>0?` ${u.join(` · `)}`:o?` failed`:i.length===0?` done`:``;return p.length>0&&f.push(c+n.fg(`dim`,p)),s(f,t.expanded,n)}exports.abbreviateHome=u,exports.collapseCommand=d,exports.formatBashCommand=p,exports.formatBashFlags=m,exports.formatResultSummary=g,exports.renderBashCall=h,exports.renderBashResult=b,exports.truncateMiddle=f;
5
5
  //# sourceMappingURL=bashRender.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.cjs","names":["wrapTextWithAnsi","truncateToWidth","os","highlightCode","formatSize"],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\nconst SGR_PATTERN = /\u001b\\[[0-9;]*m/;\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = theme.fg('toolTitle', theme.bold('[bash] ')) + command;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new BashToolText([text], { wrap: true });\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },\n options: { expanded: boolean; isPartial: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (result.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : '';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n lines.push(glyph + theme.fg('dim', meta.length > 0 ? ` ${meta.join(' · ')}` : ''));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bashRender.cjs","names":["wrapTextWithAnsi","truncateToWidth","os","highlightCode","renderToolBadge","DoomToolCall","formatSize"],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },\n options: { expanded: boolean; isPartial: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (result.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.d.cts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAkBY,eAAe,cAAc;;UAkExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;EAAmB;GAC/E;EAAW;EAAmB;GAC9B,OAAO,QACN"}
1
+ {"version":3,"file":"bashRender.d.cts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAmBY,eAAe,cAAc;;UAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;EAAmB;GAC/E;EAAW;EAAmB;GAC9B,OAAO,QACN"}
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.d.mts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAkBY,eAAe,cAAc;;UAkExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;EAAmB;GAC/E;EAAW;EAAmB;GAC9B,OAAO,QACN"}
1
+ {"version":3,"file":"bashRender.d.mts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAmBY,eAAe,cAAc;;UAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;EAAmB;GAC/E;EAAW;EAAmB;GAC9B,OAAO,QACN"}
@@ -1,5 +1,5 @@
1
- import{formatSize as e}from"../commands/bash/responseEnvelope.mjs";import t from"node:os";import{highlightCode as n}from"@earendil-works/pi-coding-agent";import{truncateToWidth as r,wrapTextWithAnsi as i}from"@earendil-works/pi-tui";var a=class{lines;layout;constructor(e,t){this.lines=e,this.layout=t}render(e){if(e<=0)return[];let t=Math.max(1,e-2),n=this.layout.wrap?this.lines.flatMap(e=>i(e,t)):this.lines.map(e=>r(e,t,`…`));this.layout.separator&&n.unshift(this.layout.separator(`─`.repeat(t)));let a=n.map(e=>` ${e}`);return this.layout.trailingBlank&&a.push(``),a}invalidate(){}};function o(e,t,n){return new a(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const s=/\[[0-9;]*m/;function c(e,t,n){return s.test(e)?e:n.fg(t,e)}function l(e,n=t.homedir()){return n.length>0?e.split(n).join(`~`):e}function u(e){let t=e.split(`
2
- `).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function d(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function f(e){return l(u(e))}function p(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.alarm!==void 0&&t.push(`alarm ${e.alarm}s`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function m(e,t,r=n){let i=r(f(e.command),`bash`).join(` `),o=t.fg(`toolTitle`,t.bold(`[bash] `))+i,s=p(e);return s.length>0&&(o+=` ${t.fg(`muted`,`· ${s.join(` · `)}`)}`),new a([o],{wrap:!0})}function h(t){let n=[];return t.lines!==void 0&&n.push(`${t.lines.toLocaleString(`en-US`)} lines`),t.fileSize!==void 0&&n.push(e(t.fileSize)),t.runner!==void 0&&n.push(t.runner),n.join(` · `)}function g(e,t){let n=(e.tail??``).split(`
3
- `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return t||n.length<=12?n:n.slice(-12)}function _(e){return(e.content??[]).map(e=>e.text??``).join(``)}function v(e,t){let n=e.split(`
4
- `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function y(e,t,n){let r=e.details??{};if(t.isPartial){let t=v(_(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return o([...t.map(e=>c(e,`toolOutput`,n)),r],!1,n)}if(e.isError===!0){let i=v(_(e),t.expanded?2**53-1:12),a=h(r),s=a.length>0?` ${a}`:``;return o([...i.map(e=>c(e,`toolOutput`,n)),n.fg(`error`,`✗`)+n.fg(`dim`,s)],t.expanded,n)}if(r.promoted===!0){let e=r.runner??r.id??`runner`,t=r.id===void 0?``:` · ${r.id}`;return o([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=g(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,s=r.timedOut===!0||a&&r.exitCode!==0,l=s?n.fg(`error`,`✗`):n.fg(`success`,`✓`),u=[];r.timedOut===!0?u.push(`timed out`):s&&u.push(`exit ${r.exitCode}`);let d=h(r);d.length>0&&u.push(d),!t.expanded&&(r.tailLines??0)>i.length&&u.push(`ctrl+o`);let f=i.map(e=>c(e,`toolOutput`,n));return f.push(l+n.fg(`dim`,u.length>0?` ${u.join(` · `)}`:``)),o(f,t.expanded,n)}export{l as abbreviateHome,u as collapseCommand,f as formatBashCommand,p as formatBashFlags,h as formatResultSummary,m as renderBashCall,y as renderBashResult,d as truncateMiddle};
1
+ import{formatSize as e}from"../commands/bash/responseEnvelope.mjs";import t from"node:os";import{DoomToolCall as n,renderToolBadge as r}from"@agimon-ai/doompi-ui/toolChrome";import{highlightCode as i}from"@earendil-works/pi-coding-agent";import{truncateToWidth as a,wrapTextWithAnsi as o}from"@earendil-works/pi-tui";var s=class{lines;layout;constructor(e,t){this.lines=e,this.layout=t}render(e){if(e<=0)return[];let t=Math.max(1,e-2),n=this.layout.wrap?this.lines.flatMap(e=>o(e,t)):this.lines.map(e=>a(e,t,`…`));this.layout.separator&&n.unshift(this.layout.separator(`─`.repeat(t)));let r=n.map(e=>` ${e}`);return this.layout.trailingBlank&&r.push(``),r}invalidate(){}};function c(e,t,n){return new s(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const l=/\u001b\[[0-9;]*m/;function u(e,t,n){return l.test(e)?e:n.fg(t,e)}function d(e,n=t.homedir()){return n.length>0?e.split(n).join(`~`):e}function f(e){let t=e.split(`
2
+ `).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function p(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function m(e){return d(f(e))}function h(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.alarm!==void 0&&t.push(`alarm ${e.alarm}s`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function g(e,t,a=i){let o=a(m(e.command),`bash`).join(` `),s=`${r(`bash`,t)} ${o}`,c=h(e);return c.length>0&&(s+=` ${t.fg(`muted`,`· ${c.join(` · `)}`)}`),new n(s)}function _(t){let n=[];return t.lines!==void 0&&n.push(`${t.lines.toLocaleString(`en-US`)} lines`),t.fileSize!==void 0&&n.push(e(t.fileSize)),t.runner!==void 0&&n.push(t.runner),n.join(` · `)}function v(e,t){let n=(e.tail??``).split(`
3
+ `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return t||n.length<=12?n:n.slice(-12)}function y(e){return(e.content??[]).map(e=>e.text??``).join(``)}function b(e,t){let n=e.split(`
4
+ `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function x(e,t,n){let r=e.details??{};if(t.isPartial){let t=b(y(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return c([...t.map(e=>u(e,`toolOutput`,n)),r],!1,n)}if(e.isError===!0){let i=b(y(e),t.expanded?2**53-1:12),a=_(r),o=a.length>0?` ${a}`:` failed`;return c([...i.map(e=>u(e,`toolOutput`,n)),n.fg(`error`,`✗`)+n.fg(`dim`,o)],t.expanded,n)}if(r.promoted===!0){let e=r.runner??r.id??`runner`,t=r.id===void 0?``:` · ${r.id}`;return c([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=v(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,o=r.timedOut===!0||a&&r.exitCode!==0,s=o?n.fg(`error`,`✗`):n.fg(`success`,`✓`),l=[];r.timedOut===!0?l.push(`timed out`):o&&l.push(`exit ${r.exitCode}`);let d=_(r);d.length>0&&l.push(d),!t.expanded&&(r.tailLines??0)>i.length&&l.push(`ctrl+o`);let f=i.map(e=>u(e,`toolOutput`,n)),p=l.length>0?` ${l.join(` · `)}`:o?` failed`:i.length===0?` done`:``;return p.length>0&&f.push(s+n.fg(`dim`,p)),c(f,t.expanded,n)}export{d as abbreviateHome,f as collapseCommand,m as formatBashCommand,h as formatBashFlags,_ as formatResultSummary,g as renderBashCall,x as renderBashResult,p as truncateMiddle};
5
5
  //# sourceMappingURL=bashRender.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.mjs","names":[],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\nconst SGR_PATTERN = /\u001b\\[[0-9;]*m/;\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = theme.fg('toolTitle', theme.bold('[bash] ')) + command;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new BashToolText([text], { wrap: true });\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },\n options: { expanded: boolean; isPartial: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (result.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : '';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n lines.push(glyph + theme.fg('dim', meta.length > 0 ? ` ${meta.join(' · ')}` : ''));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bashRender.mjs","names":[],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },\n options: { expanded: boolean; isPartial: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (result.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./format.cjs");let t=require("@earendil-works/pi-tui"),n=require("@agimon-ai/doompi-ui/components/doomOverlay");const r=n.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;var i=class extends n.DoomOverlay{options;done;selected=0;attached;viewedLog;unsubscribe;logRefresh;notice;stopReason;constructor(e,t,n,r){super(e,t),this.options=n,this.done=r}handleInput(e){if(this.stopReason){this.handleStopReasonInput(e);return}if(this.attached){this.handleAttachedInput(e);return}if(this.viewedLog){(0,t.matchesKey)(e,`escape`)&&this.detach();return}this.handleListInput(e)}getChrome(){let e=this.options.getRunners();return{title:`RUNNER SPACE`,accent:n.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › r / runners › r / runner space`,headerRight:this.attached?`attached · ${this.attached.name}`:this.viewedLog?`logs · ${this.viewedLog.name}`:`${e.length} runners · this worktree`,footer:this.attached?`esc back · everything else goes to the runner`:this.viewedLog?`esc back · live stdout/stderr`:`↑↓/jk select · enter open · s stop · r reason · esc close`,footerHints:this.attached||this.viewedLog?void 0:[[`↑↓/jk`,`select`],[`enter`,`open`],[`s`,`stop`],[`r`,`reason`],[`esc`,`close`]],footerRight:this.attached?`live tty`:this.viewedLog?`live log`:e.length>0?`${Math.min(this.selected,e.length-1)+1}/${e.length}`:`empty`}}renderBody(e,n){let r=n=>(0,t.truncateToWidth)(n,e,`…`);return this.attached?this.renderAttached(r,n):this.viewedLog?this.renderLog(r,n):this.renderList(r,n)}handleAttachedInput(e){if((0,t.matchesKey)(e,`escape`)){this.detach();return}this.attached?.write(e)}handleListInput(e){let n=this.options.getRunners();if((0,t.matchesKey)(e,`escape`)||(0,t.matchesKey)(e,`ctrl+c`)){this.close();return}(0,t.matchesKey)(e,`up`)||e===`k`?this.move(-1,n.length):(0,t.matchesKey)(e,`down`)||e===`j`?this.move(1,n.length):e===`\r`||e===`
2
- `?this.attach(n):e===`s`?this.stop(n):e===`r`&&this.promptStopReason(n)}move(e,t){t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.notice=void 0,this.tui.requestRender())}current(e){if(e.length!==0)return e[Math.min(this.selected,e.length-1)]}attach(e){let t=this.current(e);if(!t)return;let n=t.interactive?this.options.getPtyRun(t.name):void 0;this.notice=void 0,n?(this.attached=n,this.unsubscribe=n.onData(()=>this.tui.requestRender())):(this.viewedLog=t,this.logRefresh=setInterval(()=>this.tui.requestRender(!0),250),this.logRefresh.unref?.()),this.tui.requestRender(!0)}detach(){this.unsubscribe?.(),this.unsubscribe=void 0,this.logRefresh&&clearInterval(this.logRefresh),this.logRefresh=void 0,this.attached=void 0,this.viewedLog=void 0,this.tui.requestRender(!0)}stop(e){let t=this.current(e);t&&this.options.stopRunner(t.id).then(()=>{this.notice=`Stopped ${t.name}`,this.tui.requestRender(!0)}).catch(e=>{this.notice=e instanceof Error?e.message:String(e),this.tui.requestRender()})}promptStopReason(e){let t=this.current(e);t&&(this.stopReason={runner:t,text:``},this.tui.requestRender())}handleStopReasonInput(e){let n=this.stopReason;if(n){if((0,t.matchesKey)(e,`escape`)||(0,t.matchesKey)(e,`ctrl+c`)){this.stopReason=void 0,this.tui.requestRender();return}if(e===`\r`||e===`
3
- `){this.stopReason=void 0,this.options.stopRunner(n.runner.id,n.text.trim()||void 0).then(()=>{this.notice=`Stopped ${n.runner.name}`,this.tui.requestRender(!0)}).catch(e=>{this.notice=e instanceof Error?e.message:String(e),this.tui.requestRender()});return}(0,t.matchesKey)(e,`backspace`)?n.text=n.text.slice(0,-1):e.includes(`\x1B`)||(n.text+=e),this.tui.requestRender()}}close(){this.detach(),this.done(void 0)}renderList(t,n){let r=this.options.getRunners(),i=e.toRunnerRows(r,Date.now()),a=[];if(this.stopReason)return[t(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];if(i.length===0)a.push(t(this.theme.fg(`dim`,`No background runners. bash backgrounds anything that outlives the threshold.`)));else{let e=this.notice?2:0,r=Math.max(1,n-e),o=Math.max(0,i.length-r),s=Math.min(o,Math.max(0,this.selected-r+1));i.slice(s,s+r).forEach((e,n)=>{let r=s+n===Math.min(this.selected,i.length-1)?`›`:` `,o=e.interactive?` [tty]`:``;a.push(t(`${r} ${e.name}${o} ${this.theme.fg(`dim`,e.detail)}`))})}return this.notice&&a.push(``,t(this.theme.fg(`warning`,this.notice))),a.slice(0,n)}renderAttached(e,t){let n=this.attached;return n?n.screen().split(`
2
+ `?this.attach(n):e===`s`?this.stop(n):e===`r`&&this.promptStopReason(n)}move(e,t){t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.notice=void 0,this.tui.requestRender())}current(e){if(e.length!==0)return e[Math.min(this.selected,e.length-1)]}attach(e){let t=this.current(e);if(!t)return;let n=t.interactive?this.options.getPtyRun(t.name):void 0;this.notice=void 0,n?(this.attached=n,this.unsubscribe=n.onData(()=>this.tui.requestRender())):(this.viewedLog=t,this.logRefresh=setInterval(()=>this.tui.requestRender(!0),250),this.logRefresh.unref?.()),this.tui.requestRender(!0)}detach(){this.unsubscribe?.(),this.unsubscribe=void 0,this.logRefresh&&clearInterval(this.logRefresh),this.logRefresh=void 0,this.attached=void 0,this.viewedLog=void 0,this.tui.requestRender(!0)}stop(e){let t=this.current(e);t&&this.options.stopRunner(t.id).then(()=>{this.notice={text:`Stopped ${t.name}`,color:`success`},this.tui.requestRender(!0)}).catch(e=>{this.notice={text:e instanceof Error?e.message:String(e),color:`error`},this.tui.requestRender()})}promptStopReason(e){let t=this.current(e);t&&(this.stopReason={runner:t,text:``},this.tui.requestRender())}handleStopReasonInput(e){let n=this.stopReason;if(n){if((0,t.matchesKey)(e,`escape`)||(0,t.matchesKey)(e,`ctrl+c`)){this.stopReason=void 0,this.tui.requestRender();return}if(e===`\r`||e===`
3
+ `){this.stopReason=void 0,this.options.stopRunner(n.runner.id,n.text.trim()||void 0).then(()=>{this.notice={text:`Stopped ${n.runner.name}`,color:`success`},this.tui.requestRender(!0)}).catch(e=>{this.notice={text:e instanceof Error?e.message:String(e),color:`error`},this.tui.requestRender()});return}(0,t.matchesKey)(e,`backspace`)?n.text=n.text.slice(0,-1):e.includes(`\x1B`)||(n.text+=e),this.tui.requestRender()}}close(){this.detach(),this.done(void 0)}renderList(t,n){let r=this.options.getRunners(),i=e.toRunnerRows(r,Date.now()),a=[];if(this.stopReason)return[t(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];if(i.length===0)a.push(t(this.theme.fg(`dim`,`No background runners. bash backgrounds anything that outlives the threshold.`)));else{let e=this.notice?2:0,r=Math.max(1,n-e),o=Math.max(0,i.length-r),s=Math.min(o,Math.max(0,this.selected-r+1));i.slice(s,s+r).forEach((e,n)=>{let r=s+n===Math.min(this.selected,i.length-1)?`›`:` `,o=e.interactive?` [tty]`:``;a.push(t(`${r} ${e.name}${o} ${this.theme.fg(`dim`,e.detail)}`))})}return this.notice&&a.push(``,t(this.theme.fg(this.notice.color,this.notice.text))),a.slice(0,n)}renderAttached(e,t){let n=this.attached;return n?n.screen().split(`
4
4
  `).slice(-t).map(e):[]}renderLog(e,t){let n=this.viewedLog;if(!n)return[];let r=this.options.readLog(n.logPath);return r?r.split(`
5
5
  `).slice(-t).map(e):[e(this.theme.fg(`dim`,`Waiting for output…`))]}};async function a(e,t){await e.ui.custom((e,n,r,a)=>new i(e,n,t,a),n.DOOM_FULLSCREEN_UI_OPTIONS)}exports.RUNNER_SPACE_OVERLAY_OPTIONS=r,exports.RunnerSpaceComponent=i,exports.openRunnerSpace=a;
6
6
  //# sourceMappingURL=runnerSpace.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"runnerSpace.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","DoomOverlay","matchesKey","DOOM_OVERLAY_ACCENT","truncateToWidth","toRunnerRows"],"sources":["../../src/tui/runnerSpace.ts"],"sourcesContent":["/**\n * Runner Space: the `SPC r r` overlay.\n *\n * Two modes behind one component. The list shows every runner in this worktree\n * and can stop them; attaching to an interactive runner renders its live\n * terminal and forwards keystrokes to it, which is the whole reason PTY runs\n * exist. Rendering is pure, so the surface can be asserted as text.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { PtyRun } from '../services/PtyHost/index.ts';\nimport type { RunnerRecord } from '../services/RunnerRegistry/index.ts';\nimport { toRunnerRows } from './format.ts';\n\nexport const RUNNER_SPACE_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst TITLE = 'RUNNER SPACE';\nconst EMPTY_MESSAGE = 'No background runners. bash backgrounds anything that outlives the threshold.';\nconst LIST_FOOTER = '↑↓/jk select · enter open · s stop · r reason · esc close';\nconst ATTACH_FOOTER = 'esc back · everything else goes to the runner';\nconst LOG_FOOTER = 'esc back · live stdout/stderr';\nconst LOG_REFRESH_MS = 250;\nconst ELLIPSIS = '…';\n\nexport interface RunnerSpaceOptions {\n getRunners: () => readonly RunnerRecord[];\n /** The hosted terminal for a runner, when this session owns one. */\n getPtyRun: (name: string) => PtyRun | undefined;\n readLog: (logPath: string) => string;\n stopRunner: (id: string, reason?: string) => Promise<void>;\n}\n\nexport class RunnerSpaceComponent extends DoomOverlay {\n private selected = 0;\n private attached: PtyRun | undefined;\n private viewedLog: RunnerRecord | undefined;\n private unsubscribe: (() => void) | undefined;\n private logRefresh: ReturnType<typeof setInterval> | undefined;\n private notice: string | undefined;\n private stopReason: { runner: RunnerRecord; text: string } | undefined;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: RunnerSpaceOptions,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (this.stopReason) {\n this.handleStopReasonInput(data);\n return;\n }\n if (this.attached) {\n this.handleAttachedInput(data);\n return;\n }\n if (this.viewedLog) {\n if (matchesKey(data, 'escape')) this.detach();\n return;\n }\n this.handleListInput(data);\n }\n\n protected getChrome(): DoomOverlayChrome {\n const runners = this.options.getRunners();\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: 'SPC › r / runners › r / runner space',\n headerRight: this.attached\n ? `attached · ${this.attached.name}`\n : this.viewedLog\n ? `logs · ${this.viewedLog.name}`\n : `${runners.length} runners · this worktree`,\n footer: this.attached ? ATTACH_FOOTER : this.viewedLog ? LOG_FOOTER : LIST_FOOTER,\n footerHints:\n this.attached || this.viewedLog\n ? undefined\n : [\n ['↑↓/jk', 'select'],\n ['enter', 'open'],\n ['s', 'stop'],\n ['r', 'reason'],\n ['esc', 'close'],\n ],\n footerRight: this.attached\n ? 'live tty'\n : this.viewedLog\n ? 'live log'\n : runners.length > 0\n ? `${Math.min(this.selected, runners.length - 1) + 1}/${runners.length}`\n : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const truncate = (line: string): string => truncateToWidth(line, width, ELLIPSIS);\n if (this.attached) return this.renderAttached(truncate, height);\n if (this.viewedLog) return this.renderLog(truncate, height);\n return this.renderList(truncate, height);\n }\n\n private handleAttachedInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.detach();\n return;\n }\n this.attached?.write(data);\n }\n\n private handleListInput(data: string): void {\n const runners = this.options.getRunners();\n\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.close();\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') this.move(-1, runners.length);\n else if (matchesKey(data, 'down') || data === 'j') this.move(1, runners.length);\n else if (data === KEY_ENTER || data === KEY_NEWLINE) this.attach(runners);\n else if (data === 's') this.stop(runners);\n else if (data === 'r') this.promptStopReason(runners);\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.notice = undefined;\n this.tui.requestRender();\n }\n\n private current(runners: readonly RunnerRecord[]): RunnerRecord | undefined {\n if (runners.length === 0) return undefined;\n return runners[Math.min(this.selected, runners.length - 1)];\n }\n\n private attach(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n const run = record.interactive ? this.options.getPtyRun(record.name) : undefined;\n this.notice = undefined;\n if (run) {\n this.attached = run;\n this.unsubscribe = run.onData(() => this.tui.requestRender());\n } else {\n this.viewedLog = record;\n this.logRefresh = setInterval(() => this.tui.requestRender(true), LOG_REFRESH_MS);\n this.logRefresh.unref?.();\n }\n this.tui.requestRender(true);\n }\n\n private detach(): void {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n if (this.logRefresh) clearInterval(this.logRefresh);\n this.logRefresh = undefined;\n this.attached = undefined;\n this.viewedLog = undefined;\n this.tui.requestRender(true);\n }\n\n private stop(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n this.options\n .stopRunner(record.id)\n .then(() => {\n this.notice = `Stopped ${record.name}`;\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n // Shown rather than swallowed: a failed stop means the process is still\n // running and the operator needs to know.\n this.notice = error instanceof Error ? error.message : String(error);\n this.tui.requestRender();\n });\n }\n\n private promptStopReason(runners: readonly RunnerRecord[]): void {\n const runner = this.current(runners);\n if (!runner) return;\n this.stopReason = { runner, text: '' };\n this.tui.requestRender();\n }\n\n private handleStopReasonInput(data: string): void {\n const prompt = this.stopReason;\n if (!prompt) return;\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.stopReason = undefined;\n this.tui.requestRender();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.stopReason = undefined;\n this.options\n .stopRunner(prompt.runner.id, prompt.text.trim() || undefined)\n .then(() => {\n this.notice = `Stopped ${prompt.runner.name}`;\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n this.notice = error instanceof Error ? error.message : String(error);\n this.tui.requestRender();\n });\n return;\n }\n if (matchesKey(data, 'backspace')) prompt.text = prompt.text.slice(0, -1);\n else if (!data.includes('\\u001b')) prompt.text += data;\n this.tui.requestRender();\n }\n\n private close(): void {\n this.detach();\n this.done(undefined);\n }\n\n private renderList(truncate: (line: string) => string, height: number): string[] {\n const runners = this.options.getRunners();\n const rows = toRunnerRows(runners, Date.now());\n const lines: string[] = [];\n\n if (this.stopReason) {\n return [truncate(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];\n }\n\n if (rows.length === 0) lines.push(truncate(this.theme.fg('dim', EMPTY_MESSAGE)));\n else {\n const noticeRows = this.notice ? 2 : 0;\n const capacity = Math.max(1, height - noticeRows);\n const maxStart = Math.max(0, rows.length - capacity);\n const start = Math.min(maxStart, Math.max(0, this.selected - capacity + 1));\n rows.slice(start, start + capacity).forEach((row, offset) => {\n const index = start + offset;\n const marker = index === Math.min(this.selected, rows.length - 1) ? '›' : ' ';\n const tty = row.interactive ? ' [tty]' : '';\n lines.push(truncate(`${marker} ${row.name}${tty} ${this.theme.fg('dim', row.detail)}`));\n });\n }\n\n if (this.notice) lines.push('', truncate(this.theme.fg('warning', this.notice)));\n return lines.slice(0, height);\n }\n\n private renderAttached(truncate: (line: string) => string, height: number): string[] {\n const run = this.attached;\n if (!run) return [];\n\n return run.screen().split('\\n').slice(-height).map(truncate);\n }\n\n private renderLog(truncate: (line: string) => string, height: number): string[] {\n const record = this.viewedLog;\n if (!record) return [];\n\n const text = this.options.readLog(record.logPath);\n if (!text) return [truncate(this.theme.fg('dim', 'Waiting for output…'))];\n return text.split('\\n').slice(-height).map(truncate);\n }\n}\n\nexport async function openRunnerSpace(ctx: ExtensionContext, options: RunnerSpaceOptions): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new RunnerSpaceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runnerSpace.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","DoomOverlay","matchesKey","DOOM_OVERLAY_ACCENT","truncateToWidth","toRunnerRows"],"sources":["../../src/tui/runnerSpace.ts"],"sourcesContent":["/**\n * Runner Space: the `SPC r r` overlay.\n *\n * Two modes behind one component. The list shows every runner in this worktree\n * and can stop them; attaching to an interactive runner renders its live\n * terminal and forwards keystrokes to it, which is the whole reason PTY runs\n * exist. Rendering is pure, so the surface can be asserted as text.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { PtyRun } from '../services/PtyHost/index.ts';\nimport type { RunnerRecord } from '../services/RunnerRegistry/index.ts';\nimport { toRunnerRows } from './format.ts';\n\nexport const RUNNER_SPACE_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst TITLE = 'RUNNER SPACE';\nconst EMPTY_MESSAGE = 'No background runners. bash backgrounds anything that outlives the threshold.';\nconst LIST_FOOTER = '↑↓/jk select · enter open · s stop · r reason · esc close';\nconst ATTACH_FOOTER = 'esc back · everything else goes to the runner';\nconst LOG_FOOTER = 'esc back · live stdout/stderr';\nconst LOG_REFRESH_MS = 250;\nconst ELLIPSIS = '…';\n\nexport interface RunnerSpaceOptions {\n getRunners: () => readonly RunnerRecord[];\n /** The hosted terminal for a runner, when this session owns one. */\n getPtyRun: (name: string) => PtyRun | undefined;\n readLog: (logPath: string) => string;\n stopRunner: (id: string, reason?: string) => Promise<void>;\n}\n\nexport class RunnerSpaceComponent extends DoomOverlay {\n private selected = 0;\n private attached: PtyRun | undefined;\n private viewedLog: RunnerRecord | undefined;\n private unsubscribe: (() => void) | undefined;\n private logRefresh: ReturnType<typeof setInterval> | undefined;\n private notice: { text: string; color: 'success' | 'error' } | undefined;\n private stopReason: { runner: RunnerRecord; text: string } | undefined;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: RunnerSpaceOptions,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (this.stopReason) {\n this.handleStopReasonInput(data);\n return;\n }\n if (this.attached) {\n this.handleAttachedInput(data);\n return;\n }\n if (this.viewedLog) {\n if (matchesKey(data, 'escape')) this.detach();\n return;\n }\n this.handleListInput(data);\n }\n\n protected getChrome(): DoomOverlayChrome {\n const runners = this.options.getRunners();\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: 'SPC › r / runners › r / runner space',\n headerRight: this.attached\n ? `attached · ${this.attached.name}`\n : this.viewedLog\n ? `logs · ${this.viewedLog.name}`\n : `${runners.length} runners · this worktree`,\n footer: this.attached ? ATTACH_FOOTER : this.viewedLog ? LOG_FOOTER : LIST_FOOTER,\n footerHints:\n this.attached || this.viewedLog\n ? undefined\n : [\n ['↑↓/jk', 'select'],\n ['enter', 'open'],\n ['s', 'stop'],\n ['r', 'reason'],\n ['esc', 'close'],\n ],\n footerRight: this.attached\n ? 'live tty'\n : this.viewedLog\n ? 'live log'\n : runners.length > 0\n ? `${Math.min(this.selected, runners.length - 1) + 1}/${runners.length}`\n : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const truncate = (line: string): string => truncateToWidth(line, width, ELLIPSIS);\n if (this.attached) return this.renderAttached(truncate, height);\n if (this.viewedLog) return this.renderLog(truncate, height);\n return this.renderList(truncate, height);\n }\n\n private handleAttachedInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.detach();\n return;\n }\n this.attached?.write(data);\n }\n\n private handleListInput(data: string): void {\n const runners = this.options.getRunners();\n\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.close();\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') this.move(-1, runners.length);\n else if (matchesKey(data, 'down') || data === 'j') this.move(1, runners.length);\n else if (data === KEY_ENTER || data === KEY_NEWLINE) this.attach(runners);\n else if (data === 's') this.stop(runners);\n else if (data === 'r') this.promptStopReason(runners);\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.notice = undefined;\n this.tui.requestRender();\n }\n\n private current(runners: readonly RunnerRecord[]): RunnerRecord | undefined {\n if (runners.length === 0) return undefined;\n return runners[Math.min(this.selected, runners.length - 1)];\n }\n\n private attach(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n const run = record.interactive ? this.options.getPtyRun(record.name) : undefined;\n this.notice = undefined;\n if (run) {\n this.attached = run;\n this.unsubscribe = run.onData(() => this.tui.requestRender());\n } else {\n this.viewedLog = record;\n this.logRefresh = setInterval(() => this.tui.requestRender(true), LOG_REFRESH_MS);\n this.logRefresh.unref?.();\n }\n this.tui.requestRender(true);\n }\n\n private detach(): void {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n if (this.logRefresh) clearInterval(this.logRefresh);\n this.logRefresh = undefined;\n this.attached = undefined;\n this.viewedLog = undefined;\n this.tui.requestRender(true);\n }\n\n private stop(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n this.options\n .stopRunner(record.id)\n .then(() => {\n this.notice = { text: `Stopped ${record.name}`, color: 'success' };\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n // Shown rather than swallowed: a failed stop means the process is still\n // running and the operator needs to know.\n this.notice = { text: error instanceof Error ? error.message : String(error), color: 'error' };\n this.tui.requestRender();\n });\n }\n\n private promptStopReason(runners: readonly RunnerRecord[]): void {\n const runner = this.current(runners);\n if (!runner) return;\n this.stopReason = { runner, text: '' };\n this.tui.requestRender();\n }\n\n private handleStopReasonInput(data: string): void {\n const prompt = this.stopReason;\n if (!prompt) return;\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.stopReason = undefined;\n this.tui.requestRender();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.stopReason = undefined;\n this.options\n .stopRunner(prompt.runner.id, prompt.text.trim() || undefined)\n .then(() => {\n this.notice = { text: `Stopped ${prompt.runner.name}`, color: 'success' };\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n this.notice = { text: error instanceof Error ? error.message : String(error), color: 'error' };\n this.tui.requestRender();\n });\n return;\n }\n if (matchesKey(data, 'backspace')) prompt.text = prompt.text.slice(0, -1);\n else if (!data.includes('\\u001b')) prompt.text += data;\n this.tui.requestRender();\n }\n\n private close(): void {\n this.detach();\n this.done(undefined);\n }\n\n private renderList(truncate: (line: string) => string, height: number): string[] {\n const runners = this.options.getRunners();\n const rows = toRunnerRows(runners, Date.now());\n const lines: string[] = [];\n\n if (this.stopReason) {\n return [truncate(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];\n }\n\n if (rows.length === 0) lines.push(truncate(this.theme.fg('dim', EMPTY_MESSAGE)));\n else {\n const noticeRows = this.notice ? 2 : 0;\n const capacity = Math.max(1, height - noticeRows);\n const maxStart = Math.max(0, rows.length - capacity);\n const start = Math.min(maxStart, Math.max(0, this.selected - capacity + 1));\n rows.slice(start, start + capacity).forEach((row, offset) => {\n const index = start + offset;\n const marker = index === Math.min(this.selected, rows.length - 1) ? '›' : ' ';\n const tty = row.interactive ? ' [tty]' : '';\n lines.push(truncate(`${marker} ${row.name}${tty} ${this.theme.fg('dim', row.detail)}`));\n });\n }\n\n if (this.notice) lines.push('', truncate(this.theme.fg(this.notice.color, this.notice.text)));\n return lines.slice(0, height);\n }\n\n private renderAttached(truncate: (line: string) => string, height: number): string[] {\n const run = this.attached;\n if (!run) return [];\n\n return run.screen().split('\\n').slice(-height).map(truncate);\n }\n\n private renderLog(truncate: (line: string) => string, height: number): string[] {\n const record = this.viewedLog;\n if (!record) return [];\n\n const text = this.options.readLog(record.logPath);\n if (!text) return [truncate(this.theme.fg('dim', 'Waiting for output…'))];\n return text.split('\\n').slice(-height).map(truncate);\n }\n}\n\nexport async function openRunnerSpace(ctx: ExtensionContext, options: RunnerSpaceOptions): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new RunnerSpaceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import{toRunnerRows as e}from"./format.mjs";import{matchesKey as t,truncateToWidth as n}from"@earendil-works/pi-tui";import{DOOM_FULLSCREEN_UI_OPTIONS as r,DOOM_OVERLAY_ACCENT as i,DoomOverlay as a}from"@agimon-ai/doompi-ui/components/doomOverlay";const o=r.overlayOptions;var s=class extends a{options;done;selected=0;attached;viewedLog;unsubscribe;logRefresh;notice;stopReason;constructor(e,t,n,r){super(e,t),this.options=n,this.done=r}handleInput(e){if(this.stopReason){this.handleStopReasonInput(e);return}if(this.attached){this.handleAttachedInput(e);return}if(this.viewedLog){t(e,`escape`)&&this.detach();return}this.handleListInput(e)}getChrome(){let e=this.options.getRunners();return{title:`RUNNER SPACE`,accent:i,breadcrumb:`SPC › r / runners › r / runner space`,headerRight:this.attached?`attached · ${this.attached.name}`:this.viewedLog?`logs · ${this.viewedLog.name}`:`${e.length} runners · this worktree`,footer:this.attached?`esc back · everything else goes to the runner`:this.viewedLog?`esc back · live stdout/stderr`:`↑↓/jk select · enter open · s stop · r reason · esc close`,footerHints:this.attached||this.viewedLog?void 0:[[`↑↓/jk`,`select`],[`enter`,`open`],[`s`,`stop`],[`r`,`reason`],[`esc`,`close`]],footerRight:this.attached?`live tty`:this.viewedLog?`live log`:e.length>0?`${Math.min(this.selected,e.length-1)+1}/${e.length}`:`empty`}}renderBody(e,t){let r=t=>n(t,e,`…`);return this.attached?this.renderAttached(r,t):this.viewedLog?this.renderLog(r,t):this.renderList(r,t)}handleAttachedInput(e){if(t(e,`escape`)){this.detach();return}this.attached?.write(e)}handleListInput(e){let n=this.options.getRunners();if(t(e,`escape`)||t(e,`ctrl+c`)){this.close();return}t(e,`up`)||e===`k`?this.move(-1,n.length):t(e,`down`)||e===`j`?this.move(1,n.length):e===`\r`||e===`
2
- `?this.attach(n):e===`s`?this.stop(n):e===`r`&&this.promptStopReason(n)}move(e,t){t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.notice=void 0,this.tui.requestRender())}current(e){if(e.length!==0)return e[Math.min(this.selected,e.length-1)]}attach(e){let t=this.current(e);if(!t)return;let n=t.interactive?this.options.getPtyRun(t.name):void 0;this.notice=void 0,n?(this.attached=n,this.unsubscribe=n.onData(()=>this.tui.requestRender())):(this.viewedLog=t,this.logRefresh=setInterval(()=>this.tui.requestRender(!0),250),this.logRefresh.unref?.()),this.tui.requestRender(!0)}detach(){this.unsubscribe?.(),this.unsubscribe=void 0,this.logRefresh&&clearInterval(this.logRefresh),this.logRefresh=void 0,this.attached=void 0,this.viewedLog=void 0,this.tui.requestRender(!0)}stop(e){let t=this.current(e);t&&this.options.stopRunner(t.id).then(()=>{this.notice=`Stopped ${t.name}`,this.tui.requestRender(!0)}).catch(e=>{this.notice=e instanceof Error?e.message:String(e),this.tui.requestRender()})}promptStopReason(e){let t=this.current(e);t&&(this.stopReason={runner:t,text:``},this.tui.requestRender())}handleStopReasonInput(e){let n=this.stopReason;if(n){if(t(e,`escape`)||t(e,`ctrl+c`)){this.stopReason=void 0,this.tui.requestRender();return}if(e===`\r`||e===`
3
- `){this.stopReason=void 0,this.options.stopRunner(n.runner.id,n.text.trim()||void 0).then(()=>{this.notice=`Stopped ${n.runner.name}`,this.tui.requestRender(!0)}).catch(e=>{this.notice=e instanceof Error?e.message:String(e),this.tui.requestRender()});return}t(e,`backspace`)?n.text=n.text.slice(0,-1):e.includes(`\x1B`)||(n.text+=e),this.tui.requestRender()}}close(){this.detach(),this.done(void 0)}renderList(t,n){let r=this.options.getRunners(),i=e(r,Date.now()),a=[];if(this.stopReason)return[t(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];if(i.length===0)a.push(t(this.theme.fg(`dim`,`No background runners. bash backgrounds anything that outlives the threshold.`)));else{let e=this.notice?2:0,r=Math.max(1,n-e),o=Math.max(0,i.length-r),s=Math.min(o,Math.max(0,this.selected-r+1));i.slice(s,s+r).forEach((e,n)=>{let r=s+n===Math.min(this.selected,i.length-1)?`›`:` `,o=e.interactive?` [tty]`:``;a.push(t(`${r} ${e.name}${o} ${this.theme.fg(`dim`,e.detail)}`))})}return this.notice&&a.push(``,t(this.theme.fg(`warning`,this.notice))),a.slice(0,n)}renderAttached(e,t){let n=this.attached;return n?n.screen().split(`
2
+ `?this.attach(n):e===`s`?this.stop(n):e===`r`&&this.promptStopReason(n)}move(e,t){t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.notice=void 0,this.tui.requestRender())}current(e){if(e.length!==0)return e[Math.min(this.selected,e.length-1)]}attach(e){let t=this.current(e);if(!t)return;let n=t.interactive?this.options.getPtyRun(t.name):void 0;this.notice=void 0,n?(this.attached=n,this.unsubscribe=n.onData(()=>this.tui.requestRender())):(this.viewedLog=t,this.logRefresh=setInterval(()=>this.tui.requestRender(!0),250),this.logRefresh.unref?.()),this.tui.requestRender(!0)}detach(){this.unsubscribe?.(),this.unsubscribe=void 0,this.logRefresh&&clearInterval(this.logRefresh),this.logRefresh=void 0,this.attached=void 0,this.viewedLog=void 0,this.tui.requestRender(!0)}stop(e){let t=this.current(e);t&&this.options.stopRunner(t.id).then(()=>{this.notice={text:`Stopped ${t.name}`,color:`success`},this.tui.requestRender(!0)}).catch(e=>{this.notice={text:e instanceof Error?e.message:String(e),color:`error`},this.tui.requestRender()})}promptStopReason(e){let t=this.current(e);t&&(this.stopReason={runner:t,text:``},this.tui.requestRender())}handleStopReasonInput(e){let n=this.stopReason;if(n){if(t(e,`escape`)||t(e,`ctrl+c`)){this.stopReason=void 0,this.tui.requestRender();return}if(e===`\r`||e===`
3
+ `){this.stopReason=void 0,this.options.stopRunner(n.runner.id,n.text.trim()||void 0).then(()=>{this.notice={text:`Stopped ${n.runner.name}`,color:`success`},this.tui.requestRender(!0)}).catch(e=>{this.notice={text:e instanceof Error?e.message:String(e),color:`error`},this.tui.requestRender()});return}t(e,`backspace`)?n.text=n.text.slice(0,-1):e.includes(`\x1B`)||(n.text+=e),this.tui.requestRender()}}close(){this.detach(),this.done(void 0)}renderList(t,n){let r=this.options.getRunners(),i=e(r,Date.now()),a=[];if(this.stopReason)return[t(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];if(i.length===0)a.push(t(this.theme.fg(`dim`,`No background runners. bash backgrounds anything that outlives the threshold.`)));else{let e=this.notice?2:0,r=Math.max(1,n-e),o=Math.max(0,i.length-r),s=Math.min(o,Math.max(0,this.selected-r+1));i.slice(s,s+r).forEach((e,n)=>{let r=s+n===Math.min(this.selected,i.length-1)?`›`:` `,o=e.interactive?` [tty]`:``;a.push(t(`${r} ${e.name}${o} ${this.theme.fg(`dim`,e.detail)}`))})}return this.notice&&a.push(``,t(this.theme.fg(this.notice.color,this.notice.text))),a.slice(0,n)}renderAttached(e,t){let n=this.attached;return n?n.screen().split(`
4
4
  `).slice(-t).map(e):[]}renderLog(e,t){let n=this.viewedLog;if(!n)return[];let r=this.options.readLog(n.logPath);return r?r.split(`
5
5
  `).slice(-t).map(e):[e(this.theme.fg(`dim`,`Waiting for output…`))]}};async function c(e,t){await e.ui.custom((e,n,r,i)=>new s(e,n,t,i),r)}export{o as RUNNER_SPACE_OVERLAY_OPTIONS,s as RunnerSpaceComponent,c as openRunnerSpace};
6
6
  //# sourceMappingURL=runnerSpace.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"runnerSpace.mjs","names":[],"sources":["../../src/tui/runnerSpace.ts"],"sourcesContent":["/**\n * Runner Space: the `SPC r r` overlay.\n *\n * Two modes behind one component. The list shows every runner in this worktree\n * and can stop them; attaching to an interactive runner renders its live\n * terminal and forwards keystrokes to it, which is the whole reason PTY runs\n * exist. Rendering is pure, so the surface can be asserted as text.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { PtyRun } from '../services/PtyHost/index.ts';\nimport type { RunnerRecord } from '../services/RunnerRegistry/index.ts';\nimport { toRunnerRows } from './format.ts';\n\nexport const RUNNER_SPACE_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst TITLE = 'RUNNER SPACE';\nconst EMPTY_MESSAGE = 'No background runners. bash backgrounds anything that outlives the threshold.';\nconst LIST_FOOTER = '↑↓/jk select · enter open · s stop · r reason · esc close';\nconst ATTACH_FOOTER = 'esc back · everything else goes to the runner';\nconst LOG_FOOTER = 'esc back · live stdout/stderr';\nconst LOG_REFRESH_MS = 250;\nconst ELLIPSIS = '…';\n\nexport interface RunnerSpaceOptions {\n getRunners: () => readonly RunnerRecord[];\n /** The hosted terminal for a runner, when this session owns one. */\n getPtyRun: (name: string) => PtyRun | undefined;\n readLog: (logPath: string) => string;\n stopRunner: (id: string, reason?: string) => Promise<void>;\n}\n\nexport class RunnerSpaceComponent extends DoomOverlay {\n private selected = 0;\n private attached: PtyRun | undefined;\n private viewedLog: RunnerRecord | undefined;\n private unsubscribe: (() => void) | undefined;\n private logRefresh: ReturnType<typeof setInterval> | undefined;\n private notice: string | undefined;\n private stopReason: { runner: RunnerRecord; text: string } | undefined;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: RunnerSpaceOptions,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (this.stopReason) {\n this.handleStopReasonInput(data);\n return;\n }\n if (this.attached) {\n this.handleAttachedInput(data);\n return;\n }\n if (this.viewedLog) {\n if (matchesKey(data, 'escape')) this.detach();\n return;\n }\n this.handleListInput(data);\n }\n\n protected getChrome(): DoomOverlayChrome {\n const runners = this.options.getRunners();\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: 'SPC › r / runners › r / runner space',\n headerRight: this.attached\n ? `attached · ${this.attached.name}`\n : this.viewedLog\n ? `logs · ${this.viewedLog.name}`\n : `${runners.length} runners · this worktree`,\n footer: this.attached ? ATTACH_FOOTER : this.viewedLog ? LOG_FOOTER : LIST_FOOTER,\n footerHints:\n this.attached || this.viewedLog\n ? undefined\n : [\n ['↑↓/jk', 'select'],\n ['enter', 'open'],\n ['s', 'stop'],\n ['r', 'reason'],\n ['esc', 'close'],\n ],\n footerRight: this.attached\n ? 'live tty'\n : this.viewedLog\n ? 'live log'\n : runners.length > 0\n ? `${Math.min(this.selected, runners.length - 1) + 1}/${runners.length}`\n : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const truncate = (line: string): string => truncateToWidth(line, width, ELLIPSIS);\n if (this.attached) return this.renderAttached(truncate, height);\n if (this.viewedLog) return this.renderLog(truncate, height);\n return this.renderList(truncate, height);\n }\n\n private handleAttachedInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.detach();\n return;\n }\n this.attached?.write(data);\n }\n\n private handleListInput(data: string): void {\n const runners = this.options.getRunners();\n\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.close();\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') this.move(-1, runners.length);\n else if (matchesKey(data, 'down') || data === 'j') this.move(1, runners.length);\n else if (data === KEY_ENTER || data === KEY_NEWLINE) this.attach(runners);\n else if (data === 's') this.stop(runners);\n else if (data === 'r') this.promptStopReason(runners);\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.notice = undefined;\n this.tui.requestRender();\n }\n\n private current(runners: readonly RunnerRecord[]): RunnerRecord | undefined {\n if (runners.length === 0) return undefined;\n return runners[Math.min(this.selected, runners.length - 1)];\n }\n\n private attach(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n const run = record.interactive ? this.options.getPtyRun(record.name) : undefined;\n this.notice = undefined;\n if (run) {\n this.attached = run;\n this.unsubscribe = run.onData(() => this.tui.requestRender());\n } else {\n this.viewedLog = record;\n this.logRefresh = setInterval(() => this.tui.requestRender(true), LOG_REFRESH_MS);\n this.logRefresh.unref?.();\n }\n this.tui.requestRender(true);\n }\n\n private detach(): void {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n if (this.logRefresh) clearInterval(this.logRefresh);\n this.logRefresh = undefined;\n this.attached = undefined;\n this.viewedLog = undefined;\n this.tui.requestRender(true);\n }\n\n private stop(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n this.options\n .stopRunner(record.id)\n .then(() => {\n this.notice = `Stopped ${record.name}`;\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n // Shown rather than swallowed: a failed stop means the process is still\n // running and the operator needs to know.\n this.notice = error instanceof Error ? error.message : String(error);\n this.tui.requestRender();\n });\n }\n\n private promptStopReason(runners: readonly RunnerRecord[]): void {\n const runner = this.current(runners);\n if (!runner) return;\n this.stopReason = { runner, text: '' };\n this.tui.requestRender();\n }\n\n private handleStopReasonInput(data: string): void {\n const prompt = this.stopReason;\n if (!prompt) return;\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.stopReason = undefined;\n this.tui.requestRender();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.stopReason = undefined;\n this.options\n .stopRunner(prompt.runner.id, prompt.text.trim() || undefined)\n .then(() => {\n this.notice = `Stopped ${prompt.runner.name}`;\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n this.notice = error instanceof Error ? error.message : String(error);\n this.tui.requestRender();\n });\n return;\n }\n if (matchesKey(data, 'backspace')) prompt.text = prompt.text.slice(0, -1);\n else if (!data.includes('\\u001b')) prompt.text += data;\n this.tui.requestRender();\n }\n\n private close(): void {\n this.detach();\n this.done(undefined);\n }\n\n private renderList(truncate: (line: string) => string, height: number): string[] {\n const runners = this.options.getRunners();\n const rows = toRunnerRows(runners, Date.now());\n const lines: string[] = [];\n\n if (this.stopReason) {\n return [truncate(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];\n }\n\n if (rows.length === 0) lines.push(truncate(this.theme.fg('dim', EMPTY_MESSAGE)));\n else {\n const noticeRows = this.notice ? 2 : 0;\n const capacity = Math.max(1, height - noticeRows);\n const maxStart = Math.max(0, rows.length - capacity);\n const start = Math.min(maxStart, Math.max(0, this.selected - capacity + 1));\n rows.slice(start, start + capacity).forEach((row, offset) => {\n const index = start + offset;\n const marker = index === Math.min(this.selected, rows.length - 1) ? '›' : ' ';\n const tty = row.interactive ? ' [tty]' : '';\n lines.push(truncate(`${marker} ${row.name}${tty} ${this.theme.fg('dim', row.detail)}`));\n });\n }\n\n if (this.notice) lines.push('', truncate(this.theme.fg('warning', this.notice)));\n return lines.slice(0, height);\n }\n\n private renderAttached(truncate: (line: string) => string, height: number): string[] {\n const run = this.attached;\n if (!run) return [];\n\n return run.screen().split('\\n').slice(-height).map(truncate);\n }\n\n private renderLog(truncate: (line: string) => string, height: number): string[] {\n const record = this.viewedLog;\n if (!record) return [];\n\n const text = this.options.readLog(record.logPath);\n if (!text) return [truncate(this.theme.fg('dim', 'Waiting for output…'))];\n return text.split('\\n').slice(-height).map(truncate);\n }\n}\n\nexport async function openRunnerSpace(ctx: ExtensionContext, options: RunnerSpaceOptions): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new RunnerSpaceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runnerSpace.mjs","names":[],"sources":["../../src/tui/runnerSpace.ts"],"sourcesContent":["/**\n * Runner Space: the `SPC r r` overlay.\n *\n * Two modes behind one component. The list shows every runner in this worktree\n * and can stop them; attaching to an interactive runner renders its live\n * terminal and forwards keystrokes to it, which is the whole reason PTY runs\n * exist. Rendering is pure, so the surface can be asserted as text.\n */\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { PtyRun } from '../services/PtyHost/index.ts';\nimport type { RunnerRecord } from '../services/RunnerRegistry/index.ts';\nimport { toRunnerRows } from './format.ts';\n\nexport const RUNNER_SPACE_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst TITLE = 'RUNNER SPACE';\nconst EMPTY_MESSAGE = 'No background runners. bash backgrounds anything that outlives the threshold.';\nconst LIST_FOOTER = '↑↓/jk select · enter open · s stop · r reason · esc close';\nconst ATTACH_FOOTER = 'esc back · everything else goes to the runner';\nconst LOG_FOOTER = 'esc back · live stdout/stderr';\nconst LOG_REFRESH_MS = 250;\nconst ELLIPSIS = '…';\n\nexport interface RunnerSpaceOptions {\n getRunners: () => readonly RunnerRecord[];\n /** The hosted terminal for a runner, when this session owns one. */\n getPtyRun: (name: string) => PtyRun | undefined;\n readLog: (logPath: string) => string;\n stopRunner: (id: string, reason?: string) => Promise<void>;\n}\n\nexport class RunnerSpaceComponent extends DoomOverlay {\n private selected = 0;\n private attached: PtyRun | undefined;\n private viewedLog: RunnerRecord | undefined;\n private unsubscribe: (() => void) | undefined;\n private logRefresh: ReturnType<typeof setInterval> | undefined;\n private notice: { text: string; color: 'success' | 'error' } | undefined;\n private stopReason: { runner: RunnerRecord; text: string } | undefined;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: RunnerSpaceOptions,\n private readonly done: (result: undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (this.stopReason) {\n this.handleStopReasonInput(data);\n return;\n }\n if (this.attached) {\n this.handleAttachedInput(data);\n return;\n }\n if (this.viewedLog) {\n if (matchesKey(data, 'escape')) this.detach();\n return;\n }\n this.handleListInput(data);\n }\n\n protected getChrome(): DoomOverlayChrome {\n const runners = this.options.getRunners();\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: 'SPC › r / runners › r / runner space',\n headerRight: this.attached\n ? `attached · ${this.attached.name}`\n : this.viewedLog\n ? `logs · ${this.viewedLog.name}`\n : `${runners.length} runners · this worktree`,\n footer: this.attached ? ATTACH_FOOTER : this.viewedLog ? LOG_FOOTER : LIST_FOOTER,\n footerHints:\n this.attached || this.viewedLog\n ? undefined\n : [\n ['↑↓/jk', 'select'],\n ['enter', 'open'],\n ['s', 'stop'],\n ['r', 'reason'],\n ['esc', 'close'],\n ],\n footerRight: this.attached\n ? 'live tty'\n : this.viewedLog\n ? 'live log'\n : runners.length > 0\n ? `${Math.min(this.selected, runners.length - 1) + 1}/${runners.length}`\n : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const truncate = (line: string): string => truncateToWidth(line, width, ELLIPSIS);\n if (this.attached) return this.renderAttached(truncate, height);\n if (this.viewedLog) return this.renderLog(truncate, height);\n return this.renderList(truncate, height);\n }\n\n private handleAttachedInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.detach();\n return;\n }\n this.attached?.write(data);\n }\n\n private handleListInput(data: string): void {\n const runners = this.options.getRunners();\n\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.close();\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') this.move(-1, runners.length);\n else if (matchesKey(data, 'down') || data === 'j') this.move(1, runners.length);\n else if (data === KEY_ENTER || data === KEY_NEWLINE) this.attach(runners);\n else if (data === 's') this.stop(runners);\n else if (data === 'r') this.promptStopReason(runners);\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.notice = undefined;\n this.tui.requestRender();\n }\n\n private current(runners: readonly RunnerRecord[]): RunnerRecord | undefined {\n if (runners.length === 0) return undefined;\n return runners[Math.min(this.selected, runners.length - 1)];\n }\n\n private attach(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n const run = record.interactive ? this.options.getPtyRun(record.name) : undefined;\n this.notice = undefined;\n if (run) {\n this.attached = run;\n this.unsubscribe = run.onData(() => this.tui.requestRender());\n } else {\n this.viewedLog = record;\n this.logRefresh = setInterval(() => this.tui.requestRender(true), LOG_REFRESH_MS);\n this.logRefresh.unref?.();\n }\n this.tui.requestRender(true);\n }\n\n private detach(): void {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n if (this.logRefresh) clearInterval(this.logRefresh);\n this.logRefresh = undefined;\n this.attached = undefined;\n this.viewedLog = undefined;\n this.tui.requestRender(true);\n }\n\n private stop(runners: readonly RunnerRecord[]): void {\n const record = this.current(runners);\n if (!record) return;\n\n this.options\n .stopRunner(record.id)\n .then(() => {\n this.notice = { text: `Stopped ${record.name}`, color: 'success' };\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n // Shown rather than swallowed: a failed stop means the process is still\n // running and the operator needs to know.\n this.notice = { text: error instanceof Error ? error.message : String(error), color: 'error' };\n this.tui.requestRender();\n });\n }\n\n private promptStopReason(runners: readonly RunnerRecord[]): void {\n const runner = this.current(runners);\n if (!runner) return;\n this.stopReason = { runner, text: '' };\n this.tui.requestRender();\n }\n\n private handleStopReasonInput(data: string): void {\n const prompt = this.stopReason;\n if (!prompt) return;\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.stopReason = undefined;\n this.tui.requestRender();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.stopReason = undefined;\n this.options\n .stopRunner(prompt.runner.id, prompt.text.trim() || undefined)\n .then(() => {\n this.notice = { text: `Stopped ${prompt.runner.name}`, color: 'success' };\n this.tui.requestRender(true);\n })\n .catch((error: unknown) => {\n this.notice = { text: error instanceof Error ? error.message : String(error), color: 'error' };\n this.tui.requestRender();\n });\n return;\n }\n if (matchesKey(data, 'backspace')) prompt.text = prompt.text.slice(0, -1);\n else if (!data.includes('\\u001b')) prompt.text += data;\n this.tui.requestRender();\n }\n\n private close(): void {\n this.detach();\n this.done(undefined);\n }\n\n private renderList(truncate: (line: string) => string, height: number): string[] {\n const runners = this.options.getRunners();\n const rows = toRunnerRows(runners, Date.now());\n const lines: string[] = [];\n\n if (this.stopReason) {\n return [truncate(`Stop reason for ${this.stopReason.runner.name}: ${this.stopReason.text}`)];\n }\n\n if (rows.length === 0) lines.push(truncate(this.theme.fg('dim', EMPTY_MESSAGE)));\n else {\n const noticeRows = this.notice ? 2 : 0;\n const capacity = Math.max(1, height - noticeRows);\n const maxStart = Math.max(0, rows.length - capacity);\n const start = Math.min(maxStart, Math.max(0, this.selected - capacity + 1));\n rows.slice(start, start + capacity).forEach((row, offset) => {\n const index = start + offset;\n const marker = index === Math.min(this.selected, rows.length - 1) ? '›' : ' ';\n const tty = row.interactive ? ' [tty]' : '';\n lines.push(truncate(`${marker} ${row.name}${tty} ${this.theme.fg('dim', row.detail)}`));\n });\n }\n\n if (this.notice) lines.push('', truncate(this.theme.fg(this.notice.color, this.notice.text)));\n return lines.slice(0, height);\n }\n\n private renderAttached(truncate: (line: string) => string, height: number): string[] {\n const run = this.attached;\n if (!run) return [];\n\n return run.screen().split('\\n').slice(-height).map(truncate);\n }\n\n private renderLog(truncate: (line: string) => string, height: number): string[] {\n const record = this.viewedLog;\n if (!record) return [];\n\n const text = this.options.readLog(record.logPath);\n if (!text) return [truncate(this.theme.fg('dim', 'Waiting for output…'))];\n return text.split('\\n').slice(-height).map(truncate);\n }\n}\n\nexport async function openRunnerSpace(ctx: ExtensionContext, options: RunnerSpaceOptions): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new RunnerSpaceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-runner",
3
- "version": "0.0.1-alpha.11",
3
+ "version": "0.0.1-alpha.12",
4
4
  "description": "Pi bash tool replacement with background process supervision",
5
5
  "homepage": "https://agimon.ai",
6
6
  "license": "MIT",
@@ -285,10 +285,10 @@
285
285
  "node-pty": "1.1.0",
286
286
  "reflect-metadata": "0.2.2",
287
287
  "typebox": "1.1.38",
288
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.11",
289
- "@agimon-ai/foundation-process-registry": "0.29.1",
290
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.11",
291
- "@agimon-ai/doompi-ui": "0.0.1-alpha.11"
288
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.12",
289
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.12",
290
+ "@agimon-ai/foundation-process-registry": "0.29.2",
291
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.12"
292
292
  },
293
293
  "devDependencies": {
294
294
  "@earendil-works/pi-coding-agent": "0.84.1",
@@ -312,10 +312,10 @@
312
312
  }
313
313
  },
314
314
  "optionalDependencies": {
315
- "@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.11",
316
- "@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.11",
317
- "@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.11",
318
- "@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.11"
315
+ "@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.12",
316
+ "@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.12",
317
+ "@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.12",
318
+ "@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.12"
319
319
  },
320
320
  "engines": {
321
321
  "node": ">=22.19.0"