2026-09-05 17:30:19 +07:00
|
|
|
import { costOf, formatUsd } from '../pricing';
|
|
|
|
|
import type { Session } from '../session';
|
|
|
|
|
import { toolSetOf } from '../tools';
|
|
|
|
|
import { todoLines } from './transcript';
|
|
|
|
|
|
|
|
|
|
export type Panel = { title: string; hint?: string; body: string };
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The read-only panel bodies, as pure functions of session and hook state.
|
|
|
|
|
*
|
|
|
|
|
* These were inline inside App's submit switch, where each one added a `setPanel`
|
|
|
|
|
* call wrapped around string assembly and pushed the switch past the point a reader
|
|
|
|
|
* can follow it. Out here they are testable without mounting Ink, and the switch
|
|
|
|
|
* reads as routing rather than as formatting.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export function toolsPanel(session: Session): Panel {
|
|
|
|
|
const offered = session.activeTools().sort();
|
|
|
|
|
return {
|
|
|
|
|
title: 'tools',
|
|
|
|
|
hint: `${offered.length} offered this turn of ${Object.keys(session.tools).length} registered`,
|
|
|
|
|
body: offered
|
|
|
|
|
.map((t) => {
|
|
|
|
|
const set = toolSetOf(t);
|
|
|
|
|
return `- \`${t}\`${set ? ` ${set}` : ''}`;
|
|
|
|
|
})
|
|
|
|
|
.join('\n'),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function costPanel(
|
|
|
|
|
session: Session,
|
2026-09-07 19:59:58 +07:00
|
|
|
info: { sessionId: string; model: string; agent: string; thinking: string; subagentModel?: string },
|
2026-09-05 17:30:19 +07:00
|
|
|
): Panel {
|
|
|
|
|
const spend = costOf(info.model, session.inputTokens, session.outputTokens);
|
2026-09-07 19:59:58 +07:00
|
|
|
const lines = [
|
|
|
|
|
`- model: \`${info.model}\``,
|
|
|
|
|
`- billed: ${session.inputTokens} in / ${session.outputTokens} out`,
|
|
|
|
|
`- spend: ${spend === undefined ? 'unpriced model' : formatUsd(spend)}`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Subagent spend is priced against its own model id, which may be the cheaper
|
|
|
|
|
// one, so it is reported as its own line rather than folded into the parent's.
|
|
|
|
|
if (session.subagentInputTokens + session.subagentOutputTokens > 0) {
|
|
|
|
|
const subModel = info.subagentModel ?? info.model;
|
|
|
|
|
const subSpend = costOf(subModel, session.subagentInputTokens, session.subagentOutputTokens);
|
|
|
|
|
lines.push(
|
|
|
|
|
`- subagents: ${session.subagentInputTokens} in / ${session.subagentOutputTokens} out (\`${subModel}\`)${
|
|
|
|
|
subSpend === undefined ? '' : ` - ${formatUsd(subSpend)}`
|
|
|
|
|
}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ceiling = session.spend();
|
|
|
|
|
if (ceiling.ceiling !== undefined) {
|
|
|
|
|
lines.push(
|
|
|
|
|
`- ceiling: ${ceiling.usd === undefined ? 'unpriced' : formatUsd(ceiling.usd)} of ${formatUsd(ceiling.ceiling)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines.push(`- context: ~${session.estimatedTokens()} tokens`, `- agent: \`${info.agent}\` thinking \`${info.thinking}\``);
|
|
|
|
|
return { title: 'cost', hint: `session ${info.sessionId}`, body: lines.join('\n') };
|
2026-09-05 17:30:19 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function contextPanel(files: readonly string[]): Panel {
|
|
|
|
|
return {
|
|
|
|
|
title: 'project instructions',
|
|
|
|
|
body:
|
|
|
|
|
files.length > 0
|
|
|
|
|
? files.map((f) => `- \`${f}\``).join('\n')
|
|
|
|
|
: 'No `AGENTS.md`, `CLAUDE.md`, or `.shiro.md` found. Run `/init` to write one.',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const todosPanel = (session: Session): Panel => ({
|
|
|
|
|
title: 'task list',
|
|
|
|
|
body: todoLines(session.notebook.state().todos),
|
|
|
|
|
});
|