@agimon-ai/doompi-ui 0.0.1-alpha.10 → 0.0.1-alpha.11

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,2 +1,2 @@
1
- const e=require("./rendering.cjs");let t=require("@earendil-works/pi-tui");const n=` · `,r=` `,i=`text`,a=`accent`,o=`warning`,s=`mdHeading`,c=[`mdCode`,`toolDiffAdded`,`warning`,`muted`];var l=class{theme;snapshot;context;footerData;modes;constructor(e,t,n,r,i=[]){this.theme=e,this.snapshot=t,this.context=n,this.footerData=r,this.modes=i}invalidate(){}render(t){if(t<=0||!this.snapshot.active)return[];if(t<4)return[this.theme.fg(`borderAccent`,`─`.repeat(t))];let n=Math.max(1,t-4),r=n=>this.theme.bg(`userMessageBg`,e.padLine(` ${n}`,t)),a=this.theme.inverse(this.theme.bold(this.theme.fg(s,` LEADER `))),o=`${this.snapshot.prefix.join(` › `)} / ${this.snapshot.label}`,c=r(e.alignLine(`${a} ${this.theme.bold(this.theme.fg(i,o))}`,this.theme.fg(`dim`,`esc cancel · backspace up`),n)),l=this.snapshot.rootOptions?.map(e=>`${e.key} ${e.label}`).join(` · `),u=t>=100&&this.snapshot.prefix.length>1&&l?[r(this.theme.fg(`dim`,`root ${l}`))]:[],d=Math.max(1,14-u.length),f=this.renderOptionGrid(n).slice(0,d).map(r),p=Math.max(0,12-u.length-f.length),m=this.snapshot.prefix.length===1?this.renderDiagnostics(n).slice(0,p).map(r):[];return[...[r(``),c,r(``),...f,...u,...m].slice(0,15),r(``)]}renderOptionGrid(n){if(this.snapshot.options.length===0)return[];let o=this.snapshot.options.map(e=>({heading:`${this.theme.inverse(this.theme.bold(this.theme.fg(a,` ${e.key} `)))} ${this.theme.bold(this.theme.fg(i,e.label))}`,headingWidth:4+(0,t.visibleWidth)(e.label),detail:e.detail?`${r}${this.theme.fg(`dim`,e.detail)}`:``,detailWidth:e.detail?(0,t.visibleWidth)(r)+(0,t.visibleWidth)(e.detail):0})),s=Math.max(...o.map(e=>Math.max(e.headingWidth,e.detailWidth))),c=Math.min(n,s),l=Math.max(1,Math.floor((n+2)/(c+2))),u=` `.repeat(2),d=[];for(let t=0;t<o.length;t+=l){let r=o.slice(t,t+l);d.push(e.fitLine(r.map(t=>e.padLine(t.heading,c)).join(u),n)),r.some(e=>e.detail)&&d.push(e.fitLine(r.map(t=>e.padLine(t.detail,c)).join(u),n))}return d}renderDiagnostics(e){let t=this.sessionDiagnostics(),n=this.extensionDiagnostics(e),r=this.modeDiagnostics();if(!t&&!r&&n.length===0)return[];let i=[this.theme.fg(`borderMuted`,`─`.repeat(e))];r&&i.push(this.diagnosticLine(`MODES`,r,o)),t&&i.push(this.diagnosticLine(`SESSION`,t,s));for(let[e,t]of n.entries()){let n=e===0;i.push(this.diagnosticLine(n?`EXTENSIONS`:`↳`,t,n?a:`dim`))}return i}diagnosticLine(e,t,n){return`${this.theme.bold(this.theme.fg(n,e.padEnd(11)))}${t}`}modeDiagnostics(){if(this.modes.length!==0)return this.modes.map(e=>{let t=this.theme.fg(o,e.label.toLowerCase());return e.detail?`${t}${this.theme.fg(`dim`,` (${e.detail})`)}`:t}).join(this.theme.fg(`dim`,n))}sessionDiagnostics(){if(!this.context)return;let t=0,r=0,i=0,a=0,s=0;for(let e of this.context.sessionManager.getBranch())e.type===`message`&&e.message.role===`assistant`&&(t+=e.message.usage.input,r+=e.message.usage.output,i+=e.message.usage.cacheRead,a+=e.message.usage.cacheWrite,s+=e.message.usage.cost.total);let c=t+i+a,l=c===0?0:i/c*100,u=this.context.getContextUsage(),d=u?.percent===null||u?.percent===void 0?`ctx ?`:`ctx ${u.percent.toFixed(1)}% / ${e.formatTokens(u.contextWindow)}`;return`${this.theme.fg(`muted`,`↑${e.formatTokens(t)} ↓${e.formatTokens(r)} R${e.formatTokens(i)} W${e.formatTokens(a)} CH${l.toFixed(1)}%`)} ${this.theme.fg(o,`$${s.toFixed(3)}`)}${this.theme.fg(`dim`,`${n}${d}`)}`}extensionDiagnostics(t){if(!this.footerData)return[];let r=[...this.footerData.getExtensionStatuses().values()],i=this.footerData.getAvailableProviderCount(),a=[...r];i>0&&a.push(`${i} provider${i===1?``:`s`}`);let o=a.map((e,t)=>this.theme.fg(c[t%c.length],e)),s=Math.max(1,t-15);return e.packSegments(o,s,this.theme.fg(`dim`,n)).slice(0,2)}};exports.LeaderHints=l;
1
+ const e=require("./rendering.cjs");let t=require("@earendil-works/pi-tui");const n=` · `,r=` `,i=`text`,a=`accent`,o=`warning`,s=`mdHeading`,c=[`mdCode`,`toolDiffAdded`,`warning`,`muted`];var l=class{theme;snapshot;context;footerData;modes;constructor(e,t,n,r,i=[]){this.theme=e,this.snapshot=t,this.context=n,this.footerData=r,this.modes=i}invalidate(){}render(t){if(t<=0||!this.snapshot.active)return[];if(t<4)return[this.theme.fg(`borderAccent`,`─`.repeat(t))];let n=Math.max(1,t-4),r=n=>this.theme.bg(`userMessageBg`,e.padLine(` ${n}`,t)),a=this.theme.inverse(this.theme.bold(this.theme.fg(s,` LEADER `))),o=`${this.snapshot.prefix.join(` › `)} / ${this.snapshot.label}`,c=r(e.alignLine(`${a} ${this.theme.bold(this.theme.fg(i,o))}`,this.theme.fg(`dim`,`esc cancel · backspace up`),n)),l=this.snapshot.rootOptions?.map(e=>`${e.key} ${e.label}`).join(` · `),u=t>=100&&this.snapshot.prefix.length>1&&l?[r(this.theme.fg(`dim`,`root ${l}`))]:[],d=Math.max(1,19-u.length),f=this.renderOptionGrid(n).slice(0,d).map(r),p=Math.max(0,17-u.length-f.length),m=this.snapshot.prefix.length===1?this.renderDiagnostics(n).slice(0,p).map(r):[];return[...[r(``),c,r(``),...f,...u,...m].slice(0,20),r(``)]}renderOptionGrid(n){if(this.snapshot.options.length===0)return[];let o=this.snapshot.options.map(e=>({heading:`${this.theme.inverse(this.theme.bold(this.theme.fg(a,` ${e.key} `)))} ${this.theme.bold(this.theme.fg(i,e.label))}`,headingWidth:4+(0,t.visibleWidth)(e.label),detail:e.detail?`${r}${this.theme.fg(`dim`,e.detail)}`:``,detailWidth:e.detail?(0,t.visibleWidth)(r)+(0,t.visibleWidth)(e.detail):0})),s=Math.max(...o.map(e=>Math.max(e.headingWidth,e.detailWidth))),c=Math.min(n,s),l=Math.max(1,Math.floor((n+2)/(c+2))),u=` `.repeat(2),d=[];for(let t=0;t<o.length;t+=l){let r=o.slice(t,t+l);d.push(e.fitLine(r.map(t=>e.padLine(t.heading,c)).join(u),n)),d.push(e.fitLine(r.map(t=>e.padLine(t.detail,c)).join(u),n))}return d}renderDiagnostics(e){let t=this.sessionDiagnostics(),n=this.extensionDiagnostics(e),r=this.modeDiagnostics();if(!t&&!r&&n.length===0)return[];let i=[this.theme.fg(`borderMuted`,`─`.repeat(e))];r&&i.push(this.diagnosticLine(`MODES`,r,o)),t&&i.push(this.diagnosticLine(`SESSION`,t,s));for(let[e,t]of n.entries()){let n=e===0;i.push(this.diagnosticLine(n?`EXTENSIONS`:`↳`,t,n?a:`dim`))}return i}diagnosticLine(e,t,n){return`${this.theme.bold(this.theme.fg(n,e.padEnd(11)))}${t}`}modeDiagnostics(){if(this.modes.length!==0)return this.modes.map(e=>{let t=this.theme.fg(o,e.label.toLowerCase());return e.detail?`${t}${this.theme.fg(`dim`,` (${e.detail})`)}`:t}).join(this.theme.fg(`dim`,n))}sessionDiagnostics(){if(!this.context)return;let t=0,r=0,i=0,a=0,s=0;for(let e of this.context.sessionManager.getBranch())e.type===`message`&&e.message.role===`assistant`&&(t+=e.message.usage.input,r+=e.message.usage.output,i+=e.message.usage.cacheRead,a+=e.message.usage.cacheWrite,s+=e.message.usage.cost.total);let c=t+i+a,l=c===0?0:i/c*100,u=this.context.getContextUsage(),d=u?.percent===null||u?.percent===void 0?`ctx ?`:`ctx ${u.percent.toFixed(1)}% / ${e.formatTokens(u.contextWindow)}`;return`${this.theme.fg(`muted`,`↑${e.formatTokens(t)} ↓${e.formatTokens(r)} R${e.formatTokens(i)} W${e.formatTokens(a)} CH${l.toFixed(1)}%`)} ${this.theme.fg(o,`$${s.toFixed(3)}`)}${this.theme.fg(`dim`,`${n}${d}`)}`}extensionDiagnostics(t){if(!this.footerData)return[];let r=[...this.footerData.getExtensionStatuses().values()],i=this.footerData.getAvailableProviderCount(),a=[...r];i>0&&a.push(`${i} provider${i===1?``:`s`}`);let o=a.map((e,t)=>this.theme.fg(c[t%c.length],e)),s=Math.max(1,t-15);return e.packSegments(o,s,this.theme.fg(`dim`,n)).slice(0,2)}};exports.LeaderHints=l;
2
2
  //# sourceMappingURL=leaderHints.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"leaderHints.cjs","names":["padLine","alignLine","visibleWidth","fitLine","formatTokens","packSegments"],"sources":["../../src/tui/leaderHints.ts"],"sourcesContent":["import type { ExtensionContext, ReadonlyFooterDataProvider, Theme, ThemeColor } from '@earendil-works/pi-coding-agent';\n\nimport { type Component, visibleWidth } from '@earendil-works/pi-tui';\nimport type { DoomModeStatus } from '../mode.ts';\nimport type { LeaderSnapshot } from '../services/state/uiState.ts';\nimport { alignLine, fitLine, formatTokens, packSegments, padLine } from './rendering.ts';\n\n/** The background palette is not exported by name, so take it from the method. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst WIDE_HINT_WIDTH = 100;\n/**\n * No frame rows to pay for, so the whole budget is content. Two-line option\n * cards need roughly double the grid rows a single-line list did, plus a blank\n * row of breathing space at the top and bottom.\n */\nconst MAX_WIDGET_LINES = 16;\n/** Leading pad inside the shaded block, mirrored by padLine on the right. */\nconst PANEL_INSET = ' ';\nconst MIN_RENDER_WIDTH = 4;\nconst CONTENT_INSET = 4;\nconst GRID_GAP = 2;\nconst DIAGNOSTIC_LABEL_WIDTH = 11;\nconst DIAGNOSTIC_VALUE_INSET = 15;\nconst MAX_EXTENSION_LINES = 2;\nconst STATUS_SEPARATOR = ' · ';\nconst SEGMENT_GAP = ' ';\n/** The inverse key badge is ` k `, three columns wide. */\nconst KEY_BADGE_WIDTH = 3;\n/** Puts the description under the label rather than under the badge. */\nconst DETAIL_INDENT = ' ';\nconst HORIZONTAL_BORDER = '─';\nconst CONTINUATION_MARKER = '↳';\nconst DIM_COLOR: ThemeColor = 'dim';\nconst TEXT_COLOR: ThemeColor = 'text';\nconst MUTED_COLOR: ThemeColor = 'muted';\nconst ACCENT_COLOR: ThemeColor = 'accent';\nconst WARNING_COLOR: ThemeColor = 'warning';\nconst HEADING_COLOR: ThemeColor = 'mdHeading';\nconst BORDER_COLOR: ThemeColor = 'borderAccent';\nconst MUTED_BORDER_COLOR: ThemeColor = 'borderMuted';\n/** Raised surface tone: one step above the terminal background, no frame needed. */\nconst PANEL_BACKGROUND: ThemeBg = 'userMessageBg';\n// The mockup colors each extension status differently so adjacent values stay\n// separable at a glance; cycling this palette reproduces that without hardcoding\n// a color per status key.\nconst STATUS_COLORS: ThemeColor[] = ['mdCode', 'toolDiffAdded', 'warning', 'muted'];\n\nexport class LeaderHints implements Component {\n constructor(\n private readonly theme: Theme,\n private readonly snapshot: LeaderSnapshot,\n private readonly context?: ExtensionContext,\n private readonly footerData?: ReadonlyFooterDataProvider,\n private readonly modes: readonly DoomModeStatus[] = [],\n ) {}\n\n invalidate(): void {}\n\n render(width: number): string[] {\n if (width <= 0 || !this.snapshot.active) return [];\n if (width < MIN_RENDER_WIDTH) return [this.theme.fg(BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n\n // A filled block rather than a framed one: the shade alone separates the\n // panel from the transcript, which buys back the two rows a frame costs.\n const contentWidth = Math.max(1, width - CONTENT_INSET);\n const shade = (content: string): string =>\n this.theme.bg(PANEL_BACKGROUND, padLine(`${PANEL_INSET}${content}`, width));\n const badge = this.theme.inverse(this.theme.bold(this.theme.fg(HEADING_COLOR, ' LEADER ')));\n const breadcrumb = `${this.snapshot.prefix.join(' › ')} / ${this.snapshot.label}`;\n const header = shade(\n alignLine(\n `${badge} ${this.theme.bold(this.theme.fg(TEXT_COLOR, breadcrumb))}`,\n this.theme.fg(DIM_COLOR, 'esc cancel · backspace up'),\n contentWidth,\n ),\n );\n const rootTrail = this.snapshot.rootOptions?.map((option) => `${option.key} ${option.label}`).join(' · ');\n const trail =\n width >= WIDE_HINT_WIDTH && this.snapshot.prefix.length > 1 && rootTrail\n ? [shade(this.theme.fg(DIM_COLOR, `root ${rootTrail}`))]\n : [];\n // Options are the interactive surface, so they claim space before the\n // diagnostics do; truncating them would hide a reachable key.\n const optionBudget = Math.max(1, MAX_WIDGET_LINES - 2 - trail.length);\n const optionLines = this.renderOptionGrid(contentWidth).slice(0, optionBudget).map(shade);\n // Four rows are spoken for before diagnostics get any: the blank, header and\n // blank above, and the closing blank below. Counting only two of them let\n // this budget overrun, and the final `slice` did the real cutting silently\n // from the end, which is how the EXTENSIONS row went missing when MODES\n // arrived. Take all four off here so what is dropped is decided in one place.\n const diagnosticsBudget = Math.max(0, MAX_WIDGET_LINES - 4 - trail.length - optionLines.length);\n // Session and extension diagnostics belong to the board opened directly by\n // SPC/^SPC. Sub-boards stay focused on their own actions and navigation.\n const diagnostics =\n this.snapshot.prefix.length === 1\n ? this.renderDiagnostics(contentWidth).slice(0, diagnosticsBudget).map(shade)\n : [];\n // Blank shaded rows top and bottom: the panel butts against the editor above\n // and the modeline below, so the shade needs room before its first glyph.\n const body = [shade(''), header, shade(''), ...optionLines, ...trail, ...diagnostics];\n\n return [...body.slice(0, MAX_WIDGET_LINES - 1), shade('')];\n }\n\n /**\n * Lays options out on a fixed cell grid rather than a packed run of segments,\n * so keys and labels stay column-aligned when the list wraps.\n */\n private renderOptionGrid(width: number): string[] {\n if (this.snapshot.options.length === 0) return [];\n\n // Two lines per option, as the mockup lays them out: the key badge and a\n // bold label, then the description indented under the label.\n const cells = this.snapshot.options.map((option) => {\n const key = this.theme.inverse(this.theme.bold(this.theme.fg(ACCENT_COLOR, ` ${option.key} `)));\n return {\n heading: `${key} ${this.theme.bold(this.theme.fg(TEXT_COLOR, option.label))}`,\n headingWidth: KEY_BADGE_WIDTH + 1 + visibleWidth(option.label),\n detail: option.detail ? `${DETAIL_INDENT}${this.theme.fg(DIM_COLOR, option.detail)}` : '',\n detailWidth: option.detail ? visibleWidth(DETAIL_INDENT) + visibleWidth(option.detail) : 0,\n };\n });\n const widest = Math.max(...cells.map((cell) => Math.max(cell.headingWidth, cell.detailWidth)));\n const cellWidth = Math.min(width, widest);\n const columns = Math.max(1, Math.floor((width + GRID_GAP) / (cellWidth + GRID_GAP)));\n const gap = ' '.repeat(GRID_GAP);\n const rows: string[] = [];\n for (let index = 0; index < cells.length; index += columns) {\n const group = cells.slice(index, index + columns);\n rows.push(fitLine(group.map((cell) => padLine(cell.heading, cellWidth)).join(gap), width));\n // A detail row only earns space when the group has detail to show. Root\n // boards contain many terse options; reserving an empty second row for\n // every group can consume the entire widget and hide session diagnostics.\n if (group.some((cell) => cell.detail)) {\n rows.push(fitLine(group.map((cell) => padLine(cell.detail, cellWidth)).join(gap), width));\n }\n }\n return rows;\n }\n\n /** Content lines only: the caller paints the shade and pads to full width. */\n private renderDiagnostics(width: number): string[] {\n const session = this.sessionDiagnostics();\n const extensions = this.extensionDiagnostics(width);\n const modes = this.modeDiagnostics();\n if (!session && !modes && extensions.length === 0) return [];\n\n const lines = [this.theme.fg(MUTED_BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n // Above SESSION because it is the more consequential fact: session counters\n // describe what this session has spent, a mode describes what it will do.\n //\n // No blank rows between these three. The diagnostics budget is what is left\n // after the option grid takes its share, which at a full grid is exactly one\n // row per label: a spacer here does not cost breathing room, it costs the\n // EXTENSIONS row entirely. Distinct labels in distinct colours separate them\n // well enough without one.\n if (modes) lines.push(this.diagnosticLine('MODES', modes, WARNING_COLOR));\n if (session) lines.push(this.diagnosticLine('SESSION', session, HEADING_COLOR));\n for (const [index, extension] of extensions.entries()) {\n const isFirst = index === 0;\n lines.push(\n this.diagnosticLine(\n isFirst ? 'EXTENSIONS' : CONTINUATION_MARKER,\n extension,\n isFirst ? ACCENT_COLOR : DIM_COLOR,\n ),\n );\n }\n return lines;\n }\n\n private diagnosticLine(label: string, value: string, color: ThemeColor): string {\n return `${this.theme.bold(this.theme.fg(color, label.padEnd(DIAGNOSTIC_LABEL_WIDTH)))}${value}`;\n }\n\n /**\n * The enabled modes with their detail, which is the long form of what the\n * editor's badge row shows as bare names.\n *\n * This panel is opened deliberately and has room, so it is the right place\n * for `plan (normal - read only)`: the badge row is glanced at mid-typing and\n * only has to answer which modes are on.\n */\n private modeDiagnostics(): string | undefined {\n if (this.modes.length === 0) return undefined;\n return this.modes\n .map((mode) => {\n const label = this.theme.fg(WARNING_COLOR, mode.label.toLowerCase());\n return mode.detail ? `${label}${this.theme.fg(DIM_COLOR, ` (${mode.detail})`)}` : label;\n })\n .join(this.theme.fg(DIM_COLOR, STATUS_SEPARATOR));\n }\n\n private sessionDiagnostics(): string | undefined {\n if (!this.context) return undefined;\n\n let input = 0;\n let output = 0;\n let cacheRead = 0;\n let cacheWrite = 0;\n let cost = 0;\n for (const entry of this.context.sessionManager.getBranch()) {\n if (entry.type !== 'message' || entry.message.role !== 'assistant') continue;\n input += entry.message.usage.input;\n output += entry.message.usage.output;\n cacheRead += entry.message.usage.cacheRead;\n cacheWrite += entry.message.usage.cacheWrite;\n cost += entry.message.usage.cost.total;\n }\n\n const cachedTotal = input + cacheRead + cacheWrite;\n const cacheHit = cachedTotal === 0 ? 0 : (cacheRead / cachedTotal) * 100;\n const usage = this.context.getContextUsage();\n const context =\n usage?.percent === null || usage?.percent === undefined\n ? 'ctx ?'\n : `ctx ${usage.percent.toFixed(1)}% / ${formatTokens(usage.contextWindow)}`;\n // Built as sibling segments rather than nested theme calls so each color\n // closes cleanly instead of resetting the one around it.\n const metrics = this.theme.fg(\n MUTED_COLOR,\n `↑${formatTokens(input)} ↓${formatTokens(output)} R${formatTokens(cacheRead)} W${formatTokens(\n cacheWrite,\n )} CH${cacheHit.toFixed(1)}%`,\n );\n const costText = this.theme.fg(WARNING_COLOR, `$${cost.toFixed(3)}`);\n return `${metrics}${SEGMENT_GAP}${costText}${this.theme.fg(DIM_COLOR, `${STATUS_SEPARATOR}${context}`)}`;\n }\n\n private extensionDiagnostics(width: number): string[] {\n if (!this.footerData) return [];\n\n const statuses = [...this.footerData.getExtensionStatuses().values()];\n const providers = this.footerData.getAvailableProviderCount();\n const values = [...statuses];\n if (providers > 0) values.push(`${providers} provider${providers === 1 ? '' : 's'}`);\n const colored = values.map((value, index) =>\n this.theme.fg(STATUS_COLORS[index % STATUS_COLORS.length] as ThemeColor, value),\n );\n const valueWidth = Math.max(1, width - DIAGNOSTIC_VALUE_INSET);\n return packSegments(colored, valueWidth, this.theme.fg(DIM_COLOR, STATUS_SEPARATOR)).slice(0, MAX_EXTENSION_LINES);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"leaderHints.cjs","names":["padLine","alignLine","visibleWidth","fitLine","formatTokens","packSegments"],"sources":["../../src/tui/leaderHints.ts"],"sourcesContent":["import type { ExtensionContext, ReadonlyFooterDataProvider, Theme, ThemeColor } from '@earendil-works/pi-coding-agent';\n\nimport { type Component, visibleWidth } from '@earendil-works/pi-tui';\nimport type { DoomModeStatus } from '../mode.ts';\nimport type { LeaderSnapshot } from '../services/state/uiState.ts';\nimport { alignLine, fitLine, formatTokens, packSegments, padLine } from './rendering.ts';\n\n/** The background palette is not exported by name, so take it from the method. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst WIDE_HINT_WIDTH = 100;\n/**\n * No frame rows to pay for, so the whole budget is content. Two-line option\n * cards need roughly double the grid rows a single-line list did, plus a blank\n * row of breathing space at the top and bottom.\n */\nconst MAX_WIDGET_LINES = 21;\n/** Leading pad inside the shaded block, mirrored by padLine on the right. */\nconst PANEL_INSET = ' ';\nconst MIN_RENDER_WIDTH = 4;\nconst CONTENT_INSET = 4;\nconst GRID_GAP = 2;\nconst DIAGNOSTIC_LABEL_WIDTH = 11;\nconst DIAGNOSTIC_VALUE_INSET = 15;\nconst MAX_EXTENSION_LINES = 2;\nconst STATUS_SEPARATOR = ' · ';\nconst SEGMENT_GAP = ' ';\n/** The inverse key badge is ` k `, three columns wide. */\nconst KEY_BADGE_WIDTH = 3;\n/** Puts the description under the label rather than under the badge. */\nconst DETAIL_INDENT = ' ';\nconst HORIZONTAL_BORDER = '─';\nconst CONTINUATION_MARKER = '↳';\nconst DIM_COLOR: ThemeColor = 'dim';\nconst TEXT_COLOR: ThemeColor = 'text';\nconst MUTED_COLOR: ThemeColor = 'muted';\nconst ACCENT_COLOR: ThemeColor = 'accent';\nconst WARNING_COLOR: ThemeColor = 'warning';\nconst HEADING_COLOR: ThemeColor = 'mdHeading';\nconst BORDER_COLOR: ThemeColor = 'borderAccent';\nconst MUTED_BORDER_COLOR: ThemeColor = 'borderMuted';\n/** Raised surface tone: one step above the terminal background, no frame needed. */\nconst PANEL_BACKGROUND: ThemeBg = 'userMessageBg';\n// The mockup colors each extension status differently so adjacent values stay\n// separable at a glance; cycling this palette reproduces that without hardcoding\n// a color per status key.\nconst STATUS_COLORS: ThemeColor[] = ['mdCode', 'toolDiffAdded', 'warning', 'muted'];\n\nexport class LeaderHints implements Component {\n constructor(\n private readonly theme: Theme,\n private readonly snapshot: LeaderSnapshot,\n private readonly context?: ExtensionContext,\n private readonly footerData?: ReadonlyFooterDataProvider,\n private readonly modes: readonly DoomModeStatus[] = [],\n ) {}\n\n invalidate(): void {}\n\n render(width: number): string[] {\n if (width <= 0 || !this.snapshot.active) return [];\n if (width < MIN_RENDER_WIDTH) return [this.theme.fg(BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n\n // A filled block rather than a framed one: the shade alone separates the\n // panel from the transcript, which buys back the two rows a frame costs.\n const contentWidth = Math.max(1, width - CONTENT_INSET);\n const shade = (content: string): string =>\n this.theme.bg(PANEL_BACKGROUND, padLine(`${PANEL_INSET}${content}`, width));\n const badge = this.theme.inverse(this.theme.bold(this.theme.fg(HEADING_COLOR, ' LEADER ')));\n const breadcrumb = `${this.snapshot.prefix.join(' › ')} / ${this.snapshot.label}`;\n const header = shade(\n alignLine(\n `${badge} ${this.theme.bold(this.theme.fg(TEXT_COLOR, breadcrumb))}`,\n this.theme.fg(DIM_COLOR, 'esc cancel · backspace up'),\n contentWidth,\n ),\n );\n const rootTrail = this.snapshot.rootOptions?.map((option) => `${option.key} ${option.label}`).join(' · ');\n const trail =\n width >= WIDE_HINT_WIDTH && this.snapshot.prefix.length > 1 && rootTrail\n ? [shade(this.theme.fg(DIM_COLOR, `root ${rootTrail}`))]\n : [];\n // Options are the interactive surface, so they claim space before the\n // diagnostics do; truncating them would hide a reachable key.\n const optionBudget = Math.max(1, MAX_WIDGET_LINES - 2 - trail.length);\n const optionLines = this.renderOptionGrid(contentWidth).slice(0, optionBudget).map(shade);\n // Four rows are spoken for before diagnostics get any: the blank, header and\n // blank above, and the closing blank below. Counting only two of them let\n // this budget overrun, and the final `slice` did the real cutting silently\n // from the end, which is how the EXTENSIONS row went missing when MODES\n // arrived. Take all four off here so what is dropped is decided in one place.\n const diagnosticsBudget = Math.max(0, MAX_WIDGET_LINES - 4 - trail.length - optionLines.length);\n // Session and extension diagnostics belong to the board opened directly by\n // SPC/^SPC. Sub-boards stay focused on their own actions and navigation.\n const diagnostics =\n this.snapshot.prefix.length === 1\n ? this.renderDiagnostics(contentWidth).slice(0, diagnosticsBudget).map(shade)\n : [];\n // Blank shaded rows top and bottom: the panel butts against the editor above\n // and the modeline below, so the shade needs room before its first glyph.\n const body = [shade(''), header, shade(''), ...optionLines, ...trail, ...diagnostics];\n\n return [...body.slice(0, MAX_WIDGET_LINES - 1), shade('')];\n }\n\n /**\n * Lays options out on a fixed cell grid rather than a packed run of segments,\n * so keys and labels stay column-aligned when the list wraps.\n */\n private renderOptionGrid(width: number): string[] {\n if (this.snapshot.options.length === 0) return [];\n\n // Two lines per option, as the mockup lays them out: the key badge and a\n // bold label, then the description indented under the label.\n const cells = this.snapshot.options.map((option) => {\n const key = this.theme.inverse(this.theme.bold(this.theme.fg(ACCENT_COLOR, ` ${option.key} `)));\n return {\n heading: `${key} ${this.theme.bold(this.theme.fg(TEXT_COLOR, option.label))}`,\n headingWidth: KEY_BADGE_WIDTH + 1 + visibleWidth(option.label),\n detail: option.detail ? `${DETAIL_INDENT}${this.theme.fg(DIM_COLOR, option.detail)}` : '',\n detailWidth: option.detail ? visibleWidth(DETAIL_INDENT) + visibleWidth(option.detail) : 0,\n };\n });\n const widest = Math.max(...cells.map((cell) => Math.max(cell.headingWidth, cell.detailWidth)));\n const cellWidth = Math.min(width, widest);\n const columns = Math.max(1, Math.floor((width + GRID_GAP) / (cellWidth + GRID_GAP)));\n const gap = ' '.repeat(GRID_GAP);\n const rows: string[] = [];\n for (let index = 0; index < cells.length; index += columns) {\n const group = cells.slice(index, index + columns);\n rows.push(fitLine(group.map((cell) => padLine(cell.heading, cellWidth)).join(gap), width));\n // Keep the second row even when every detail is empty. Each option card\n // then retains the same two-line rhythm instead of collapsing terse items\n // into a denser list than items with supporting text.\n rows.push(fitLine(group.map((cell) => padLine(cell.detail, cellWidth)).join(gap), width));\n }\n return rows;\n }\n\n /** Content lines only: the caller paints the shade and pads to full width. */\n private renderDiagnostics(width: number): string[] {\n const session = this.sessionDiagnostics();\n const extensions = this.extensionDiagnostics(width);\n const modes = this.modeDiagnostics();\n if (!session && !modes && extensions.length === 0) return [];\n\n const lines = [this.theme.fg(MUTED_BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n // Above SESSION because it is the more consequential fact: session counters\n // describe what this session has spent, a mode describes what it will do.\n //\n // No blank rows between these three. The diagnostics budget is what is left\n // after the option grid takes its share, which at a full grid is exactly one\n // row per label: a spacer here does not cost breathing room, it costs the\n // EXTENSIONS row entirely. Distinct labels in distinct colours separate them\n // well enough without one.\n if (modes) lines.push(this.diagnosticLine('MODES', modes, WARNING_COLOR));\n if (session) lines.push(this.diagnosticLine('SESSION', session, HEADING_COLOR));\n for (const [index, extension] of extensions.entries()) {\n const isFirst = index === 0;\n lines.push(\n this.diagnosticLine(\n isFirst ? 'EXTENSIONS' : CONTINUATION_MARKER,\n extension,\n isFirst ? ACCENT_COLOR : DIM_COLOR,\n ),\n );\n }\n return lines;\n }\n\n private diagnosticLine(label: string, value: string, color: ThemeColor): string {\n return `${this.theme.bold(this.theme.fg(color, label.padEnd(DIAGNOSTIC_LABEL_WIDTH)))}${value}`;\n }\n\n /**\n * The enabled modes with their detail, which is the long form of what the\n * editor's badge row shows as bare names.\n *\n * This panel is opened deliberately and has room, so it is the right place\n * for `plan (normal - read only)`: the badge row is glanced at mid-typing and\n * only has to answer which modes are on.\n */\n private modeDiagnostics(): string | undefined {\n if (this.modes.length === 0) return undefined;\n return this.modes\n .map((mode) => {\n const label = this.theme.fg(WARNING_COLOR, mode.label.toLowerCase());\n return mode.detail ? `${label}${this.theme.fg(DIM_COLOR, ` (${mode.detail})`)}` : label;\n })\n .join(this.theme.fg(DIM_COLOR, STATUS_SEPARATOR));\n }\n\n private sessionDiagnostics(): string | undefined {\n if (!this.context) return undefined;\n\n let input = 0;\n let output = 0;\n let cacheRead = 0;\n let cacheWrite = 0;\n let cost = 0;\n for (const entry of this.context.sessionManager.getBranch()) {\n if (entry.type !== 'message' || entry.message.role !== 'assistant') continue;\n input += entry.message.usage.input;\n output += entry.message.usage.output;\n cacheRead += entry.message.usage.cacheRead;\n cacheWrite += entry.message.usage.cacheWrite;\n cost += entry.message.usage.cost.total;\n }\n\n const cachedTotal = input + cacheRead + cacheWrite;\n const cacheHit = cachedTotal === 0 ? 0 : (cacheRead / cachedTotal) * 100;\n const usage = this.context.getContextUsage();\n const context =\n usage?.percent === null || usage?.percent === undefined\n ? 'ctx ?'\n : `ctx ${usage.percent.toFixed(1)}% / ${formatTokens(usage.contextWindow)}`;\n // Built as sibling segments rather than nested theme calls so each color\n // closes cleanly instead of resetting the one around it.\n const metrics = this.theme.fg(\n MUTED_COLOR,\n `↑${formatTokens(input)} ↓${formatTokens(output)} R${formatTokens(cacheRead)} W${formatTokens(\n cacheWrite,\n )} CH${cacheHit.toFixed(1)}%`,\n );\n const costText = this.theme.fg(WARNING_COLOR, `$${cost.toFixed(3)}`);\n return `${metrics}${SEGMENT_GAP}${costText}${this.theme.fg(DIM_COLOR, `${STATUS_SEPARATOR}${context}`)}`;\n }\n\n private extensionDiagnostics(width: number): string[] {\n if (!this.footerData) return [];\n\n const statuses = [...this.footerData.getExtensionStatuses().values()];\n const providers = this.footerData.getAvailableProviderCount();\n const values = [...statuses];\n if (providers > 0) values.push(`${providers} provider${providers === 1 ? '' : 's'}`);\n const colored = values.map((value, index) =>\n this.theme.fg(STATUS_COLORS[index % STATUS_COLORS.length] as ThemeColor, value),\n );\n const valueWidth = Math.max(1, width - DIAGNOSTIC_VALUE_INSET);\n return packSegments(colored, valueWidth, this.theme.fg(DIM_COLOR, STATUS_SEPARATOR)).slice(0, MAX_EXTENSION_LINES);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"leaderHints.d.cts","names":[],"sources":["../../src/tui/leaderHints.ts"],"mappings":";;;;;cAgDa,uBAAuB;mBAEf;mBACA;mBACA;mBACA;mBACA;EAJA,YAAA,OAAO,OACP,UAAU,gBACV,UAAU,8BACV,aAAa,wCACb,iBAAgB,eAAA;EAGnC;EAEA,OAAO;;;;;UAkDC;;UAiCA;UA8BA;;;;;;;;;UAYA;UAUA;UAoCA"}
1
+ {"version":3,"file":"leaderHints.d.cts","names":[],"sources":["../../src/tui/leaderHints.ts"],"mappings":";;;;;cAgDa,uBAAuB;mBAEf;mBACA;mBACA;mBACA;mBACA;EAJA,YAAA,OAAO,OACP,UAAU,gBACV,UAAU,8BACV,aAAa,wCACb,iBAAgB,eAAA;EAGnC;EAEA,OAAO;;;;;UAkDC;;UA+BA;UA8BA;;;;;;;;;UAYA;UAUA;UAoCA"}
@@ -1 +1 @@
1
- {"version":3,"file":"leaderHints.d.mts","names":[],"sources":["../../src/tui/leaderHints.ts"],"mappings":";;;;;cAgDa,uBAAuB;mBAEf;mBACA;mBACA;mBACA;mBACA;EAJA,YAAA,OAAO,OACP,UAAU,gBACV,UAAU,8BACV,aAAa,wCACb,iBAAgB,eAAA;EAGnC;EAEA,OAAO;;;;;UAkDC;;UAiCA;UA8BA;;;;;;;;;UAYA;UAUA;UAoCA"}
1
+ {"version":3,"file":"leaderHints.d.mts","names":[],"sources":["../../src/tui/leaderHints.ts"],"mappings":";;;;;cAgDa,uBAAuB;mBAEf;mBACA;mBACA;mBACA;mBACA;EAJA,YAAA,OAAO,OACP,UAAU,gBACV,UAAU,8BACV,aAAa,wCACb,iBAAgB,eAAA;EAGnC;EAEA,OAAO;;;;;UAkDC;;UA+BA;UA8BA;;;;;;;;;UAYA;UAUA;UAoCA"}
@@ -1,2 +1,2 @@
1
- import{alignLine as e,fitLine as t,formatTokens as n,packSegments as r,padLine as i}from"./rendering.mjs";import{visibleWidth as a}from"@earendil-works/pi-tui";const o=` · `,s=` `,c=`text`,l=`accent`,u=`warning`,d=`mdHeading`,f=[`mdCode`,`toolDiffAdded`,`warning`,`muted`];var p=class{theme;snapshot;context;footerData;modes;constructor(e,t,n,r,i=[]){this.theme=e,this.snapshot=t,this.context=n,this.footerData=r,this.modes=i}invalidate(){}render(t){if(t<=0||!this.snapshot.active)return[];if(t<4)return[this.theme.fg(`borderAccent`,`─`.repeat(t))];let n=Math.max(1,t-4),r=e=>this.theme.bg(`userMessageBg`,i(` ${e}`,t)),a=this.theme.inverse(this.theme.bold(this.theme.fg(d,` LEADER `))),o=`${this.snapshot.prefix.join(` › `)} / ${this.snapshot.label}`,s=r(e(`${a} ${this.theme.bold(this.theme.fg(c,o))}`,this.theme.fg(`dim`,`esc cancel · backspace up`),n)),l=this.snapshot.rootOptions?.map(e=>`${e.key} ${e.label}`).join(` · `),u=t>=100&&this.snapshot.prefix.length>1&&l?[r(this.theme.fg(`dim`,`root ${l}`))]:[],f=Math.max(1,14-u.length),p=this.renderOptionGrid(n).slice(0,f).map(r),m=Math.max(0,12-u.length-p.length),h=this.snapshot.prefix.length===1?this.renderDiagnostics(n).slice(0,m).map(r):[];return[...[r(``),s,r(``),...p,...u,...h].slice(0,15),r(``)]}renderOptionGrid(e){if(this.snapshot.options.length===0)return[];let n=this.snapshot.options.map(e=>({heading:`${this.theme.inverse(this.theme.bold(this.theme.fg(l,` ${e.key} `)))} ${this.theme.bold(this.theme.fg(c,e.label))}`,headingWidth:4+a(e.label),detail:e.detail?`${s}${this.theme.fg(`dim`,e.detail)}`:``,detailWidth:e.detail?a(s)+a(e.detail):0})),r=Math.max(...n.map(e=>Math.max(e.headingWidth,e.detailWidth))),o=Math.min(e,r),u=Math.max(1,Math.floor((e+2)/(o+2))),d=` `.repeat(2),f=[];for(let r=0;r<n.length;r+=u){let a=n.slice(r,r+u);f.push(t(a.map(e=>i(e.heading,o)).join(d),e)),a.some(e=>e.detail)&&f.push(t(a.map(e=>i(e.detail,o)).join(d),e))}return f}renderDiagnostics(e){let t=this.sessionDiagnostics(),n=this.extensionDiagnostics(e),r=this.modeDiagnostics();if(!t&&!r&&n.length===0)return[];let i=[this.theme.fg(`borderMuted`,`─`.repeat(e))];r&&i.push(this.diagnosticLine(`MODES`,r,u)),t&&i.push(this.diagnosticLine(`SESSION`,t,d));for(let[e,t]of n.entries()){let n=e===0;i.push(this.diagnosticLine(n?`EXTENSIONS`:`↳`,t,n?l:`dim`))}return i}diagnosticLine(e,t,n){return`${this.theme.bold(this.theme.fg(n,e.padEnd(11)))}${t}`}modeDiagnostics(){if(this.modes.length!==0)return this.modes.map(e=>{let t=this.theme.fg(u,e.label.toLowerCase());return e.detail?`${t}${this.theme.fg(`dim`,` (${e.detail})`)}`:t}).join(this.theme.fg(`dim`,o))}sessionDiagnostics(){if(!this.context)return;let e=0,t=0,r=0,i=0,a=0;for(let n of this.context.sessionManager.getBranch())n.type===`message`&&n.message.role===`assistant`&&(e+=n.message.usage.input,t+=n.message.usage.output,r+=n.message.usage.cacheRead,i+=n.message.usage.cacheWrite,a+=n.message.usage.cost.total);let s=e+r+i,c=s===0?0:r/s*100,l=this.context.getContextUsage(),d=l?.percent===null||l?.percent===void 0?`ctx ?`:`ctx ${l.percent.toFixed(1)}% / ${n(l.contextWindow)}`;return`${this.theme.fg(`muted`,`↑${n(e)} ↓${n(t)} R${n(r)} W${n(i)} CH${c.toFixed(1)}%`)} ${this.theme.fg(u,`$${a.toFixed(3)}`)}${this.theme.fg(`dim`,`${o}${d}`)}`}extensionDiagnostics(e){if(!this.footerData)return[];let t=[...this.footerData.getExtensionStatuses().values()],n=this.footerData.getAvailableProviderCount(),i=[...t];n>0&&i.push(`${n} provider${n===1?``:`s`}`);let a=i.map((e,t)=>this.theme.fg(f[t%f.length],e)),s=Math.max(1,e-15);return r(a,s,this.theme.fg(`dim`,o)).slice(0,2)}};export{p as LeaderHints};
1
+ import{alignLine as e,fitLine as t,formatTokens as n,packSegments as r,padLine as i}from"./rendering.mjs";import{visibleWidth as a}from"@earendil-works/pi-tui";const o=` · `,s=` `,c=`text`,l=`accent`,u=`warning`,d=`mdHeading`,f=[`mdCode`,`toolDiffAdded`,`warning`,`muted`];var p=class{theme;snapshot;context;footerData;modes;constructor(e,t,n,r,i=[]){this.theme=e,this.snapshot=t,this.context=n,this.footerData=r,this.modes=i}invalidate(){}render(t){if(t<=0||!this.snapshot.active)return[];if(t<4)return[this.theme.fg(`borderAccent`,`─`.repeat(t))];let n=Math.max(1,t-4),r=e=>this.theme.bg(`userMessageBg`,i(` ${e}`,t)),a=this.theme.inverse(this.theme.bold(this.theme.fg(d,` LEADER `))),o=`${this.snapshot.prefix.join(` › `)} / ${this.snapshot.label}`,s=r(e(`${a} ${this.theme.bold(this.theme.fg(c,o))}`,this.theme.fg(`dim`,`esc cancel · backspace up`),n)),l=this.snapshot.rootOptions?.map(e=>`${e.key} ${e.label}`).join(` · `),u=t>=100&&this.snapshot.prefix.length>1&&l?[r(this.theme.fg(`dim`,`root ${l}`))]:[],f=Math.max(1,19-u.length),p=this.renderOptionGrid(n).slice(0,f).map(r),m=Math.max(0,17-u.length-p.length),h=this.snapshot.prefix.length===1?this.renderDiagnostics(n).slice(0,m).map(r):[];return[...[r(``),s,r(``),...p,...u,...h].slice(0,20),r(``)]}renderOptionGrid(e){if(this.snapshot.options.length===0)return[];let n=this.snapshot.options.map(e=>({heading:`${this.theme.inverse(this.theme.bold(this.theme.fg(l,` ${e.key} `)))} ${this.theme.bold(this.theme.fg(c,e.label))}`,headingWidth:4+a(e.label),detail:e.detail?`${s}${this.theme.fg(`dim`,e.detail)}`:``,detailWidth:e.detail?a(s)+a(e.detail):0})),r=Math.max(...n.map(e=>Math.max(e.headingWidth,e.detailWidth))),o=Math.min(e,r),u=Math.max(1,Math.floor((e+2)/(o+2))),d=` `.repeat(2),f=[];for(let r=0;r<n.length;r+=u){let a=n.slice(r,r+u);f.push(t(a.map(e=>i(e.heading,o)).join(d),e)),f.push(t(a.map(e=>i(e.detail,o)).join(d),e))}return f}renderDiagnostics(e){let t=this.sessionDiagnostics(),n=this.extensionDiagnostics(e),r=this.modeDiagnostics();if(!t&&!r&&n.length===0)return[];let i=[this.theme.fg(`borderMuted`,`─`.repeat(e))];r&&i.push(this.diagnosticLine(`MODES`,r,u)),t&&i.push(this.diagnosticLine(`SESSION`,t,d));for(let[e,t]of n.entries()){let n=e===0;i.push(this.diagnosticLine(n?`EXTENSIONS`:`↳`,t,n?l:`dim`))}return i}diagnosticLine(e,t,n){return`${this.theme.bold(this.theme.fg(n,e.padEnd(11)))}${t}`}modeDiagnostics(){if(this.modes.length!==0)return this.modes.map(e=>{let t=this.theme.fg(u,e.label.toLowerCase());return e.detail?`${t}${this.theme.fg(`dim`,` (${e.detail})`)}`:t}).join(this.theme.fg(`dim`,o))}sessionDiagnostics(){if(!this.context)return;let e=0,t=0,r=0,i=0,a=0;for(let n of this.context.sessionManager.getBranch())n.type===`message`&&n.message.role===`assistant`&&(e+=n.message.usage.input,t+=n.message.usage.output,r+=n.message.usage.cacheRead,i+=n.message.usage.cacheWrite,a+=n.message.usage.cost.total);let s=e+r+i,c=s===0?0:r/s*100,l=this.context.getContextUsage(),d=l?.percent===null||l?.percent===void 0?`ctx ?`:`ctx ${l.percent.toFixed(1)}% / ${n(l.contextWindow)}`;return`${this.theme.fg(`muted`,`↑${n(e)} ↓${n(t)} R${n(r)} W${n(i)} CH${c.toFixed(1)}%`)} ${this.theme.fg(u,`$${a.toFixed(3)}`)}${this.theme.fg(`dim`,`${o}${d}`)}`}extensionDiagnostics(e){if(!this.footerData)return[];let t=[...this.footerData.getExtensionStatuses().values()],n=this.footerData.getAvailableProviderCount(),i=[...t];n>0&&i.push(`${n} provider${n===1?``:`s`}`);let a=i.map((e,t)=>this.theme.fg(f[t%f.length],e)),s=Math.max(1,e-15);return r(a,s,this.theme.fg(`dim`,o)).slice(0,2)}};export{p as LeaderHints};
2
2
  //# sourceMappingURL=leaderHints.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"leaderHints.mjs","names":[],"sources":["../../src/tui/leaderHints.ts"],"sourcesContent":["import type { ExtensionContext, ReadonlyFooterDataProvider, Theme, ThemeColor } from '@earendil-works/pi-coding-agent';\n\nimport { type Component, visibleWidth } from '@earendil-works/pi-tui';\nimport type { DoomModeStatus } from '../mode.ts';\nimport type { LeaderSnapshot } from '../services/state/uiState.ts';\nimport { alignLine, fitLine, formatTokens, packSegments, padLine } from './rendering.ts';\n\n/** The background palette is not exported by name, so take it from the method. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst WIDE_HINT_WIDTH = 100;\n/**\n * No frame rows to pay for, so the whole budget is content. Two-line option\n * cards need roughly double the grid rows a single-line list did, plus a blank\n * row of breathing space at the top and bottom.\n */\nconst MAX_WIDGET_LINES = 16;\n/** Leading pad inside the shaded block, mirrored by padLine on the right. */\nconst PANEL_INSET = ' ';\nconst MIN_RENDER_WIDTH = 4;\nconst CONTENT_INSET = 4;\nconst GRID_GAP = 2;\nconst DIAGNOSTIC_LABEL_WIDTH = 11;\nconst DIAGNOSTIC_VALUE_INSET = 15;\nconst MAX_EXTENSION_LINES = 2;\nconst STATUS_SEPARATOR = ' · ';\nconst SEGMENT_GAP = ' ';\n/** The inverse key badge is ` k `, three columns wide. */\nconst KEY_BADGE_WIDTH = 3;\n/** Puts the description under the label rather than under the badge. */\nconst DETAIL_INDENT = ' ';\nconst HORIZONTAL_BORDER = '─';\nconst CONTINUATION_MARKER = '↳';\nconst DIM_COLOR: ThemeColor = 'dim';\nconst TEXT_COLOR: ThemeColor = 'text';\nconst MUTED_COLOR: ThemeColor = 'muted';\nconst ACCENT_COLOR: ThemeColor = 'accent';\nconst WARNING_COLOR: ThemeColor = 'warning';\nconst HEADING_COLOR: ThemeColor = 'mdHeading';\nconst BORDER_COLOR: ThemeColor = 'borderAccent';\nconst MUTED_BORDER_COLOR: ThemeColor = 'borderMuted';\n/** Raised surface tone: one step above the terminal background, no frame needed. */\nconst PANEL_BACKGROUND: ThemeBg = 'userMessageBg';\n// The mockup colors each extension status differently so adjacent values stay\n// separable at a glance; cycling this palette reproduces that without hardcoding\n// a color per status key.\nconst STATUS_COLORS: ThemeColor[] = ['mdCode', 'toolDiffAdded', 'warning', 'muted'];\n\nexport class LeaderHints implements Component {\n constructor(\n private readonly theme: Theme,\n private readonly snapshot: LeaderSnapshot,\n private readonly context?: ExtensionContext,\n private readonly footerData?: ReadonlyFooterDataProvider,\n private readonly modes: readonly DoomModeStatus[] = [],\n ) {}\n\n invalidate(): void {}\n\n render(width: number): string[] {\n if (width <= 0 || !this.snapshot.active) return [];\n if (width < MIN_RENDER_WIDTH) return [this.theme.fg(BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n\n // A filled block rather than a framed one: the shade alone separates the\n // panel from the transcript, which buys back the two rows a frame costs.\n const contentWidth = Math.max(1, width - CONTENT_INSET);\n const shade = (content: string): string =>\n this.theme.bg(PANEL_BACKGROUND, padLine(`${PANEL_INSET}${content}`, width));\n const badge = this.theme.inverse(this.theme.bold(this.theme.fg(HEADING_COLOR, ' LEADER ')));\n const breadcrumb = `${this.snapshot.prefix.join(' › ')} / ${this.snapshot.label}`;\n const header = shade(\n alignLine(\n `${badge} ${this.theme.bold(this.theme.fg(TEXT_COLOR, breadcrumb))}`,\n this.theme.fg(DIM_COLOR, 'esc cancel · backspace up'),\n contentWidth,\n ),\n );\n const rootTrail = this.snapshot.rootOptions?.map((option) => `${option.key} ${option.label}`).join(' · ');\n const trail =\n width >= WIDE_HINT_WIDTH && this.snapshot.prefix.length > 1 && rootTrail\n ? [shade(this.theme.fg(DIM_COLOR, `root ${rootTrail}`))]\n : [];\n // Options are the interactive surface, so they claim space before the\n // diagnostics do; truncating them would hide a reachable key.\n const optionBudget = Math.max(1, MAX_WIDGET_LINES - 2 - trail.length);\n const optionLines = this.renderOptionGrid(contentWidth).slice(0, optionBudget).map(shade);\n // Four rows are spoken for before diagnostics get any: the blank, header and\n // blank above, and the closing blank below. Counting only two of them let\n // this budget overrun, and the final `slice` did the real cutting silently\n // from the end, which is how the EXTENSIONS row went missing when MODES\n // arrived. Take all four off here so what is dropped is decided in one place.\n const diagnosticsBudget = Math.max(0, MAX_WIDGET_LINES - 4 - trail.length - optionLines.length);\n // Session and extension diagnostics belong to the board opened directly by\n // SPC/^SPC. Sub-boards stay focused on their own actions and navigation.\n const diagnostics =\n this.snapshot.prefix.length === 1\n ? this.renderDiagnostics(contentWidth).slice(0, diagnosticsBudget).map(shade)\n : [];\n // Blank shaded rows top and bottom: the panel butts against the editor above\n // and the modeline below, so the shade needs room before its first glyph.\n const body = [shade(''), header, shade(''), ...optionLines, ...trail, ...diagnostics];\n\n return [...body.slice(0, MAX_WIDGET_LINES - 1), shade('')];\n }\n\n /**\n * Lays options out on a fixed cell grid rather than a packed run of segments,\n * so keys and labels stay column-aligned when the list wraps.\n */\n private renderOptionGrid(width: number): string[] {\n if (this.snapshot.options.length === 0) return [];\n\n // Two lines per option, as the mockup lays them out: the key badge and a\n // bold label, then the description indented under the label.\n const cells = this.snapshot.options.map((option) => {\n const key = this.theme.inverse(this.theme.bold(this.theme.fg(ACCENT_COLOR, ` ${option.key} `)));\n return {\n heading: `${key} ${this.theme.bold(this.theme.fg(TEXT_COLOR, option.label))}`,\n headingWidth: KEY_BADGE_WIDTH + 1 + visibleWidth(option.label),\n detail: option.detail ? `${DETAIL_INDENT}${this.theme.fg(DIM_COLOR, option.detail)}` : '',\n detailWidth: option.detail ? visibleWidth(DETAIL_INDENT) + visibleWidth(option.detail) : 0,\n };\n });\n const widest = Math.max(...cells.map((cell) => Math.max(cell.headingWidth, cell.detailWidth)));\n const cellWidth = Math.min(width, widest);\n const columns = Math.max(1, Math.floor((width + GRID_GAP) / (cellWidth + GRID_GAP)));\n const gap = ' '.repeat(GRID_GAP);\n const rows: string[] = [];\n for (let index = 0; index < cells.length; index += columns) {\n const group = cells.slice(index, index + columns);\n rows.push(fitLine(group.map((cell) => padLine(cell.heading, cellWidth)).join(gap), width));\n // A detail row only earns space when the group has detail to show. Root\n // boards contain many terse options; reserving an empty second row for\n // every group can consume the entire widget and hide session diagnostics.\n if (group.some((cell) => cell.detail)) {\n rows.push(fitLine(group.map((cell) => padLine(cell.detail, cellWidth)).join(gap), width));\n }\n }\n return rows;\n }\n\n /** Content lines only: the caller paints the shade and pads to full width. */\n private renderDiagnostics(width: number): string[] {\n const session = this.sessionDiagnostics();\n const extensions = this.extensionDiagnostics(width);\n const modes = this.modeDiagnostics();\n if (!session && !modes && extensions.length === 0) return [];\n\n const lines = [this.theme.fg(MUTED_BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n // Above SESSION because it is the more consequential fact: session counters\n // describe what this session has spent, a mode describes what it will do.\n //\n // No blank rows between these three. The diagnostics budget is what is left\n // after the option grid takes its share, which at a full grid is exactly one\n // row per label: a spacer here does not cost breathing room, it costs the\n // EXTENSIONS row entirely. Distinct labels in distinct colours separate them\n // well enough without one.\n if (modes) lines.push(this.diagnosticLine('MODES', modes, WARNING_COLOR));\n if (session) lines.push(this.diagnosticLine('SESSION', session, HEADING_COLOR));\n for (const [index, extension] of extensions.entries()) {\n const isFirst = index === 0;\n lines.push(\n this.diagnosticLine(\n isFirst ? 'EXTENSIONS' : CONTINUATION_MARKER,\n extension,\n isFirst ? ACCENT_COLOR : DIM_COLOR,\n ),\n );\n }\n return lines;\n }\n\n private diagnosticLine(label: string, value: string, color: ThemeColor): string {\n return `${this.theme.bold(this.theme.fg(color, label.padEnd(DIAGNOSTIC_LABEL_WIDTH)))}${value}`;\n }\n\n /**\n * The enabled modes with their detail, which is the long form of what the\n * editor's badge row shows as bare names.\n *\n * This panel is opened deliberately and has room, so it is the right place\n * for `plan (normal - read only)`: the badge row is glanced at mid-typing and\n * only has to answer which modes are on.\n */\n private modeDiagnostics(): string | undefined {\n if (this.modes.length === 0) return undefined;\n return this.modes\n .map((mode) => {\n const label = this.theme.fg(WARNING_COLOR, mode.label.toLowerCase());\n return mode.detail ? `${label}${this.theme.fg(DIM_COLOR, ` (${mode.detail})`)}` : label;\n })\n .join(this.theme.fg(DIM_COLOR, STATUS_SEPARATOR));\n }\n\n private sessionDiagnostics(): string | undefined {\n if (!this.context) return undefined;\n\n let input = 0;\n let output = 0;\n let cacheRead = 0;\n let cacheWrite = 0;\n let cost = 0;\n for (const entry of this.context.sessionManager.getBranch()) {\n if (entry.type !== 'message' || entry.message.role !== 'assistant') continue;\n input += entry.message.usage.input;\n output += entry.message.usage.output;\n cacheRead += entry.message.usage.cacheRead;\n cacheWrite += entry.message.usage.cacheWrite;\n cost += entry.message.usage.cost.total;\n }\n\n const cachedTotal = input + cacheRead + cacheWrite;\n const cacheHit = cachedTotal === 0 ? 0 : (cacheRead / cachedTotal) * 100;\n const usage = this.context.getContextUsage();\n const context =\n usage?.percent === null || usage?.percent === undefined\n ? 'ctx ?'\n : `ctx ${usage.percent.toFixed(1)}% / ${formatTokens(usage.contextWindow)}`;\n // Built as sibling segments rather than nested theme calls so each color\n // closes cleanly instead of resetting the one around it.\n const metrics = this.theme.fg(\n MUTED_COLOR,\n `↑${formatTokens(input)} ↓${formatTokens(output)} R${formatTokens(cacheRead)} W${formatTokens(\n cacheWrite,\n )} CH${cacheHit.toFixed(1)}%`,\n );\n const costText = this.theme.fg(WARNING_COLOR, `$${cost.toFixed(3)}`);\n return `${metrics}${SEGMENT_GAP}${costText}${this.theme.fg(DIM_COLOR, `${STATUS_SEPARATOR}${context}`)}`;\n }\n\n private extensionDiagnostics(width: number): string[] {\n if (!this.footerData) return [];\n\n const statuses = [...this.footerData.getExtensionStatuses().values()];\n const providers = this.footerData.getAvailableProviderCount();\n const values = [...statuses];\n if (providers > 0) values.push(`${providers} provider${providers === 1 ? '' : 's'}`);\n const colored = values.map((value, index) =>\n this.theme.fg(STATUS_COLORS[index % STATUS_COLORS.length] as ThemeColor, value),\n );\n const valueWidth = Math.max(1, width - DIAGNOSTIC_VALUE_INSET);\n return packSegments(colored, valueWidth, this.theme.fg(DIM_COLOR, STATUS_SEPARATOR)).slice(0, MAX_EXTENSION_LINES);\n }\n}\n"],"mappings":"gKAUA,MAeM,EAAmB,QAKnB,EAAgB,OAIhB,EAAyB,OAEzB,EAA2B,SAC3B,EAA4B,UAC5B,EAA4B,YAQ5B,EAA8B,CAAC,SAAU,gBAAiB,UAAW,OAAO,EAElF,IAAa,EAAb,KAA8C,CAEzB,MACA,SACA,QACA,WACA,MALnB,YACE,EACA,EACA,EACA,EACA,EAAoD,CAAC,EACrD,CALiB,KAAA,MAAA,EACA,KAAA,SAAA,EACA,KAAA,QAAA,EACA,KAAA,WAAA,EACA,KAAA,MAAA,CAChB,CAEH,YAAmB,CAAC,CAEpB,OAAO,EAAyB,CAC9B,GAAI,GAAS,GAAK,CAAC,KAAK,SAAS,OAAQ,MAAO,CAAC,EACjD,GAAI,EAAQ,EAAkB,MAAO,CAAC,KAAK,MAAM,GAAG,eAAc,IAAkB,OAAO,CAAK,CAAC,CAAC,EAIlG,IAAM,EAAe,KAAK,IAAI,EAAG,EAAQ,CAAa,EAChD,EAAS,GACb,KAAK,MAAM,GAAG,gBAAkB,EAAQ,IAAiB,IAAW,CAAK,CAAC,EACtE,EAAQ,KAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAe,UAAU,CAAC,CAAC,EACpF,EAAa,GAAG,KAAK,SAAS,OAAO,KAAK,KAAK,EAAE,KAAK,KAAK,SAAS,QACpE,EAAS,EACb,EACE,GAAG,EAAM,GAAG,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAY,CAAU,CAAC,IACjE,KAAK,MAAM,GAAG,MAAW,2BAA2B,EACpD,CACF,CACF,EACM,EAAY,KAAK,SAAS,aAAa,IAAK,GAAW,GAAG,EAAO,IAAI,GAAG,EAAO,OAAO,CAAC,CAAC,KAAK,KAAK,EAClG,EACJ,GAAS,KAAmB,KAAK,SAAS,OAAO,OAAS,GAAK,EAC3D,CAAC,EAAM,KAAK,MAAM,GAAG,MAAW,SAAS,GAAW,CAAC,CAAC,EACtD,CAAC,EAGD,EAAe,KAAK,IAAI,EAAG,GAAuB,EAAM,MAAM,EAC9D,EAAc,KAAK,iBAAiB,CAAY,CAAC,CAAC,MAAM,EAAG,CAAY,CAAC,CAAC,IAAI,CAAK,EAMlF,EAAoB,KAAK,IAAI,EAAG,GAAuB,EAAM,OAAS,EAAY,MAAM,EAGxF,EACJ,KAAK,SAAS,OAAO,SAAW,EAC5B,KAAK,kBAAkB,CAAY,CAAC,CAAC,MAAM,EAAG,CAAiB,CAAC,CAAC,IAAI,CAAK,EAC1E,CAAC,EAKP,MAAO,CAAC,GAAG,CAFG,EAAM,EAAE,EAAG,EAAQ,EAAM,EAAE,EAAG,GAAG,EAAa,GAAG,EAAO,GAAG,CAE3D,CAAC,CAAC,MAAM,EAAG,EAAoB,EAAG,EAAM,EAAE,CAAC,CAC3D,CAMA,iBAAyB,EAAyB,CAChD,GAAI,KAAK,SAAS,QAAQ,SAAW,EAAG,MAAO,CAAC,EAIhD,IAAM,EAAQ,KAAK,SAAS,QAAQ,IAAK,IAEhC,CACL,QAAS,GAFC,KAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAc,IAAI,EAAO,IAAI,EAAE,CAAC,CAE7E,EAAE,GAAG,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAY,EAAO,KAAK,CAAC,IAC1E,aAAc,EAAsB,EAAa,EAAO,KAAK,EAC7D,OAAQ,EAAO,OAAS,GAAG,IAAgB,KAAK,MAAM,GAAG,MAAW,EAAO,MAAM,IAAM,GACvF,YAAa,EAAO,OAAS,EAAa,CAAa,EAAI,EAAa,EAAO,MAAM,EAAI,CAC3F,EACD,EACK,EAAS,KAAK,IAAI,GAAG,EAAM,IAAK,GAAS,KAAK,IAAI,EAAK,aAAc,EAAK,WAAW,CAAC,CAAC,EACvF,EAAY,KAAK,IAAI,EAAO,CAAM,EAClC,EAAU,KAAK,IAAI,EAAG,KAAK,OAAO,EAAQ,IAAa,EAAY,EAAS,CAAC,EAC7E,EAAM,IAAI,OAAO,CAAQ,EACzB,EAAiB,CAAC,EACxB,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAM,OAAQ,GAAS,EAAS,CAC1D,IAAM,EAAQ,EAAM,MAAM,EAAO,EAAQ,CAAO,EAChD,EAAK,KAAK,EAAQ,EAAM,IAAK,GAAS,EAAQ,EAAK,QAAS,CAAS,CAAC,CAAC,CAAC,KAAK,CAAG,EAAG,CAAK,CAAC,EAIrF,EAAM,KAAM,GAAS,EAAK,MAAM,GAClC,EAAK,KAAK,EAAQ,EAAM,IAAK,GAAS,EAAQ,EAAK,OAAQ,CAAS,CAAC,CAAC,CAAC,KAAK,CAAG,EAAG,CAAK,CAAC,CAE5F,CACA,OAAO,CACT,CAGA,kBAA0B,EAAyB,CACjD,IAAM,EAAU,KAAK,mBAAmB,EAClC,EAAa,KAAK,qBAAqB,CAAK,EAC5C,EAAQ,KAAK,gBAAgB,EACnC,GAAI,CAAC,GAAW,CAAC,GAAS,EAAW,SAAW,EAAG,MAAO,CAAC,EAE3D,IAAM,EAAQ,CAAC,KAAK,MAAM,GAAG,cAAoB,IAAkB,OAAO,CAAK,CAAC,CAAC,EAS7E,GAAO,EAAM,KAAK,KAAK,eAAe,QAAS,EAAO,CAAa,CAAC,EACpE,GAAS,EAAM,KAAK,KAAK,eAAe,UAAW,EAAS,CAAa,CAAC,EAC9E,IAAK,GAAM,CAAC,EAAO,KAAc,EAAW,QAAQ,EAAG,CACrD,IAAM,EAAU,IAAU,EAC1B,EAAM,KACJ,KAAK,eACH,EAAU,aAAe,IACzB,EACA,EAAU,EAAe,KAC3B,CACF,CACF,CACA,OAAO,CACT,CAEA,eAAuB,EAAe,EAAe,EAA2B,CAC9E,MAAO,GAAG,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAO,EAAM,OAAO,EAAsB,CAAC,CAAC,IAAI,GAC1F,CAUA,iBAA8C,CACxC,QAAK,MAAM,SAAW,EAC1B,OAAO,KAAK,MACT,IAAK,GAAS,CACb,IAAM,EAAQ,KAAK,MAAM,GAAG,EAAe,EAAK,MAAM,YAAY,CAAC,EACnE,OAAO,EAAK,OAAS,GAAG,IAAQ,KAAK,MAAM,GAAG,MAAW,KAAK,EAAK,OAAO,EAAE,IAAM,CACpF,CAAC,CAAC,CACD,KAAK,KAAK,MAAM,GAAG,MAAW,CAAgB,CAAC,CACpD,CAEA,oBAAiD,CAC/C,GAAI,CAAC,KAAK,QAAS,OAEnB,IAAI,EAAQ,EACR,EAAS,EACT,EAAY,EACZ,EAAa,EACb,EAAO,EACX,IAAK,IAAM,KAAS,KAAK,QAAQ,eAAe,UAAU,EACpD,EAAM,OAAS,WAAa,EAAM,QAAQ,OAAS,cACvD,GAAS,EAAM,QAAQ,MAAM,MAC7B,GAAU,EAAM,QAAQ,MAAM,OAC9B,GAAa,EAAM,QAAQ,MAAM,UACjC,GAAc,EAAM,QAAQ,MAAM,WAClC,GAAQ,EAAM,QAAQ,MAAM,KAAK,OAGnC,IAAM,EAAc,EAAQ,EAAY,EAClC,EAAW,IAAgB,EAAI,EAAK,EAAY,EAAe,IAC/D,EAAQ,KAAK,QAAQ,gBAAgB,EACrC,EACJ,GAAO,UAAY,MAAQ,GAAO,UAAY,IAAA,GAC1C,QACA,OAAO,EAAM,QAAQ,QAAQ,CAAC,EAAE,MAAM,EAAa,EAAM,aAAa,IAU5E,MAAO,GAPS,KAAK,MAAM,GACzB,QACA,IAAI,EAAa,CAAK,EAAE,KAAK,EAAa,CAAM,EAAE,KAAK,EAAa,CAAS,EAAE,KAAK,EAClF,CACF,EAAE,MAAM,EAAS,QAAQ,CAAC,EAAE,EAGpB,MADO,KAAK,MAAM,GAAG,EAAe,IAAI,EAAK,QAAQ,CAAC,GAC9B,IAAW,KAAK,MAAM,GAAG,MAAW,GAAG,IAAmB,GAAS,GACvG,CAEA,qBAA6B,EAAyB,CACpD,GAAI,CAAC,KAAK,WAAY,MAAO,CAAC,EAE9B,IAAM,EAAW,CAAC,GAAG,KAAK,WAAW,qBAAqB,CAAC,CAAC,OAAO,CAAC,EAC9D,EAAY,KAAK,WAAW,0BAA0B,EACtD,EAAS,CAAC,GAAG,CAAQ,EACvB,EAAY,GAAG,EAAO,KAAK,GAAG,EAAU,WAAW,IAAc,EAAI,GAAK,KAAK,EACnF,IAAM,EAAU,EAAO,KAAK,EAAO,IACjC,KAAK,MAAM,GAAG,EAAc,EAAQ,EAAc,QAAuB,CAAK,CAChF,EACM,EAAa,KAAK,IAAI,EAAG,EAAQ,EAAsB,EAC7D,OAAO,EAAa,EAAS,EAAY,KAAK,MAAM,GAAG,MAAW,CAAgB,CAAC,CAAC,CAAC,MAAM,EAAG,CAAmB,CACnH,CACF"}
1
+ {"version":3,"file":"leaderHints.mjs","names":[],"sources":["../../src/tui/leaderHints.ts"],"sourcesContent":["import type { ExtensionContext, ReadonlyFooterDataProvider, Theme, ThemeColor } from '@earendil-works/pi-coding-agent';\n\nimport { type Component, visibleWidth } from '@earendil-works/pi-tui';\nimport type { DoomModeStatus } from '../mode.ts';\nimport type { LeaderSnapshot } from '../services/state/uiState.ts';\nimport { alignLine, fitLine, formatTokens, packSegments, padLine } from './rendering.ts';\n\n/** The background palette is not exported by name, so take it from the method. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst WIDE_HINT_WIDTH = 100;\n/**\n * No frame rows to pay for, so the whole budget is content. Two-line option\n * cards need roughly double the grid rows a single-line list did, plus a blank\n * row of breathing space at the top and bottom.\n */\nconst MAX_WIDGET_LINES = 21;\n/** Leading pad inside the shaded block, mirrored by padLine on the right. */\nconst PANEL_INSET = ' ';\nconst MIN_RENDER_WIDTH = 4;\nconst CONTENT_INSET = 4;\nconst GRID_GAP = 2;\nconst DIAGNOSTIC_LABEL_WIDTH = 11;\nconst DIAGNOSTIC_VALUE_INSET = 15;\nconst MAX_EXTENSION_LINES = 2;\nconst STATUS_SEPARATOR = ' · ';\nconst SEGMENT_GAP = ' ';\n/** The inverse key badge is ` k `, three columns wide. */\nconst KEY_BADGE_WIDTH = 3;\n/** Puts the description under the label rather than under the badge. */\nconst DETAIL_INDENT = ' ';\nconst HORIZONTAL_BORDER = '─';\nconst CONTINUATION_MARKER = '↳';\nconst DIM_COLOR: ThemeColor = 'dim';\nconst TEXT_COLOR: ThemeColor = 'text';\nconst MUTED_COLOR: ThemeColor = 'muted';\nconst ACCENT_COLOR: ThemeColor = 'accent';\nconst WARNING_COLOR: ThemeColor = 'warning';\nconst HEADING_COLOR: ThemeColor = 'mdHeading';\nconst BORDER_COLOR: ThemeColor = 'borderAccent';\nconst MUTED_BORDER_COLOR: ThemeColor = 'borderMuted';\n/** Raised surface tone: one step above the terminal background, no frame needed. */\nconst PANEL_BACKGROUND: ThemeBg = 'userMessageBg';\n// The mockup colors each extension status differently so adjacent values stay\n// separable at a glance; cycling this palette reproduces that without hardcoding\n// a color per status key.\nconst STATUS_COLORS: ThemeColor[] = ['mdCode', 'toolDiffAdded', 'warning', 'muted'];\n\nexport class LeaderHints implements Component {\n constructor(\n private readonly theme: Theme,\n private readonly snapshot: LeaderSnapshot,\n private readonly context?: ExtensionContext,\n private readonly footerData?: ReadonlyFooterDataProvider,\n private readonly modes: readonly DoomModeStatus[] = [],\n ) {}\n\n invalidate(): void {}\n\n render(width: number): string[] {\n if (width <= 0 || !this.snapshot.active) return [];\n if (width < MIN_RENDER_WIDTH) return [this.theme.fg(BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n\n // A filled block rather than a framed one: the shade alone separates the\n // panel from the transcript, which buys back the two rows a frame costs.\n const contentWidth = Math.max(1, width - CONTENT_INSET);\n const shade = (content: string): string =>\n this.theme.bg(PANEL_BACKGROUND, padLine(`${PANEL_INSET}${content}`, width));\n const badge = this.theme.inverse(this.theme.bold(this.theme.fg(HEADING_COLOR, ' LEADER ')));\n const breadcrumb = `${this.snapshot.prefix.join(' › ')} / ${this.snapshot.label}`;\n const header = shade(\n alignLine(\n `${badge} ${this.theme.bold(this.theme.fg(TEXT_COLOR, breadcrumb))}`,\n this.theme.fg(DIM_COLOR, 'esc cancel · backspace up'),\n contentWidth,\n ),\n );\n const rootTrail = this.snapshot.rootOptions?.map((option) => `${option.key} ${option.label}`).join(' · ');\n const trail =\n width >= WIDE_HINT_WIDTH && this.snapshot.prefix.length > 1 && rootTrail\n ? [shade(this.theme.fg(DIM_COLOR, `root ${rootTrail}`))]\n : [];\n // Options are the interactive surface, so they claim space before the\n // diagnostics do; truncating them would hide a reachable key.\n const optionBudget = Math.max(1, MAX_WIDGET_LINES - 2 - trail.length);\n const optionLines = this.renderOptionGrid(contentWidth).slice(0, optionBudget).map(shade);\n // Four rows are spoken for before diagnostics get any: the blank, header and\n // blank above, and the closing blank below. Counting only two of them let\n // this budget overrun, and the final `slice` did the real cutting silently\n // from the end, which is how the EXTENSIONS row went missing when MODES\n // arrived. Take all four off here so what is dropped is decided in one place.\n const diagnosticsBudget = Math.max(0, MAX_WIDGET_LINES - 4 - trail.length - optionLines.length);\n // Session and extension diagnostics belong to the board opened directly by\n // SPC/^SPC. Sub-boards stay focused on their own actions and navigation.\n const diagnostics =\n this.snapshot.prefix.length === 1\n ? this.renderDiagnostics(contentWidth).slice(0, diagnosticsBudget).map(shade)\n : [];\n // Blank shaded rows top and bottom: the panel butts against the editor above\n // and the modeline below, so the shade needs room before its first glyph.\n const body = [shade(''), header, shade(''), ...optionLines, ...trail, ...diagnostics];\n\n return [...body.slice(0, MAX_WIDGET_LINES - 1), shade('')];\n }\n\n /**\n * Lays options out on a fixed cell grid rather than a packed run of segments,\n * so keys and labels stay column-aligned when the list wraps.\n */\n private renderOptionGrid(width: number): string[] {\n if (this.snapshot.options.length === 0) return [];\n\n // Two lines per option, as the mockup lays them out: the key badge and a\n // bold label, then the description indented under the label.\n const cells = this.snapshot.options.map((option) => {\n const key = this.theme.inverse(this.theme.bold(this.theme.fg(ACCENT_COLOR, ` ${option.key} `)));\n return {\n heading: `${key} ${this.theme.bold(this.theme.fg(TEXT_COLOR, option.label))}`,\n headingWidth: KEY_BADGE_WIDTH + 1 + visibleWidth(option.label),\n detail: option.detail ? `${DETAIL_INDENT}${this.theme.fg(DIM_COLOR, option.detail)}` : '',\n detailWidth: option.detail ? visibleWidth(DETAIL_INDENT) + visibleWidth(option.detail) : 0,\n };\n });\n const widest = Math.max(...cells.map((cell) => Math.max(cell.headingWidth, cell.detailWidth)));\n const cellWidth = Math.min(width, widest);\n const columns = Math.max(1, Math.floor((width + GRID_GAP) / (cellWidth + GRID_GAP)));\n const gap = ' '.repeat(GRID_GAP);\n const rows: string[] = [];\n for (let index = 0; index < cells.length; index += columns) {\n const group = cells.slice(index, index + columns);\n rows.push(fitLine(group.map((cell) => padLine(cell.heading, cellWidth)).join(gap), width));\n // Keep the second row even when every detail is empty. Each option card\n // then retains the same two-line rhythm instead of collapsing terse items\n // into a denser list than items with supporting text.\n rows.push(fitLine(group.map((cell) => padLine(cell.detail, cellWidth)).join(gap), width));\n }\n return rows;\n }\n\n /** Content lines only: the caller paints the shade and pads to full width. */\n private renderDiagnostics(width: number): string[] {\n const session = this.sessionDiagnostics();\n const extensions = this.extensionDiagnostics(width);\n const modes = this.modeDiagnostics();\n if (!session && !modes && extensions.length === 0) return [];\n\n const lines = [this.theme.fg(MUTED_BORDER_COLOR, HORIZONTAL_BORDER.repeat(width))];\n // Above SESSION because it is the more consequential fact: session counters\n // describe what this session has spent, a mode describes what it will do.\n //\n // No blank rows between these three. The diagnostics budget is what is left\n // after the option grid takes its share, which at a full grid is exactly one\n // row per label: a spacer here does not cost breathing room, it costs the\n // EXTENSIONS row entirely. Distinct labels in distinct colours separate them\n // well enough without one.\n if (modes) lines.push(this.diagnosticLine('MODES', modes, WARNING_COLOR));\n if (session) lines.push(this.diagnosticLine('SESSION', session, HEADING_COLOR));\n for (const [index, extension] of extensions.entries()) {\n const isFirst = index === 0;\n lines.push(\n this.diagnosticLine(\n isFirst ? 'EXTENSIONS' : CONTINUATION_MARKER,\n extension,\n isFirst ? ACCENT_COLOR : DIM_COLOR,\n ),\n );\n }\n return lines;\n }\n\n private diagnosticLine(label: string, value: string, color: ThemeColor): string {\n return `${this.theme.bold(this.theme.fg(color, label.padEnd(DIAGNOSTIC_LABEL_WIDTH)))}${value}`;\n }\n\n /**\n * The enabled modes with their detail, which is the long form of what the\n * editor's badge row shows as bare names.\n *\n * This panel is opened deliberately and has room, so it is the right place\n * for `plan (normal - read only)`: the badge row is glanced at mid-typing and\n * only has to answer which modes are on.\n */\n private modeDiagnostics(): string | undefined {\n if (this.modes.length === 0) return undefined;\n return this.modes\n .map((mode) => {\n const label = this.theme.fg(WARNING_COLOR, mode.label.toLowerCase());\n return mode.detail ? `${label}${this.theme.fg(DIM_COLOR, ` (${mode.detail})`)}` : label;\n })\n .join(this.theme.fg(DIM_COLOR, STATUS_SEPARATOR));\n }\n\n private sessionDiagnostics(): string | undefined {\n if (!this.context) return undefined;\n\n let input = 0;\n let output = 0;\n let cacheRead = 0;\n let cacheWrite = 0;\n let cost = 0;\n for (const entry of this.context.sessionManager.getBranch()) {\n if (entry.type !== 'message' || entry.message.role !== 'assistant') continue;\n input += entry.message.usage.input;\n output += entry.message.usage.output;\n cacheRead += entry.message.usage.cacheRead;\n cacheWrite += entry.message.usage.cacheWrite;\n cost += entry.message.usage.cost.total;\n }\n\n const cachedTotal = input + cacheRead + cacheWrite;\n const cacheHit = cachedTotal === 0 ? 0 : (cacheRead / cachedTotal) * 100;\n const usage = this.context.getContextUsage();\n const context =\n usage?.percent === null || usage?.percent === undefined\n ? 'ctx ?'\n : `ctx ${usage.percent.toFixed(1)}% / ${formatTokens(usage.contextWindow)}`;\n // Built as sibling segments rather than nested theme calls so each color\n // closes cleanly instead of resetting the one around it.\n const metrics = this.theme.fg(\n MUTED_COLOR,\n `↑${formatTokens(input)} ↓${formatTokens(output)} R${formatTokens(cacheRead)} W${formatTokens(\n cacheWrite,\n )} CH${cacheHit.toFixed(1)}%`,\n );\n const costText = this.theme.fg(WARNING_COLOR, `$${cost.toFixed(3)}`);\n return `${metrics}${SEGMENT_GAP}${costText}${this.theme.fg(DIM_COLOR, `${STATUS_SEPARATOR}${context}`)}`;\n }\n\n private extensionDiagnostics(width: number): string[] {\n if (!this.footerData) return [];\n\n const statuses = [...this.footerData.getExtensionStatuses().values()];\n const providers = this.footerData.getAvailableProviderCount();\n const values = [...statuses];\n if (providers > 0) values.push(`${providers} provider${providers === 1 ? '' : 's'}`);\n const colored = values.map((value, index) =>\n this.theme.fg(STATUS_COLORS[index % STATUS_COLORS.length] as ThemeColor, value),\n );\n const valueWidth = Math.max(1, width - DIAGNOSTIC_VALUE_INSET);\n return packSegments(colored, valueWidth, this.theme.fg(DIM_COLOR, STATUS_SEPARATOR)).slice(0, MAX_EXTENSION_LINES);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-ui",
3
- "version": "0.0.1-alpha.10",
3
+ "version": "0.0.1-alpha.11",
4
4
  "description": "Doom-inspired interactive UI extension for Pi",
5
5
  "homepage": "https://agimon.ai",
6
6
  "license": "MIT",
@@ -137,8 +137,8 @@
137
137
  },
138
138
  "dependencies": {
139
139
  "yaml": "2.9.0",
140
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.10",
141
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.10"
140
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.11",
141
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.11"
142
142
  },
143
143
  "devDependencies": {
144
144
  "@earendil-works/pi-coding-agent": "0.84.1",