Files
shiro-neko/src/ui/panel-bodies.ts
T
asepharyana de89457cf7
ci / check (macos-latest) (push) Canceled after 0s
ci / check (ubuntu-latest) (push) Canceled after 0s
ci / check (windows-latest) (push) Canceled after 0s
feat: add diagnostics tool for live command output monitoring
- Implemented diagnostics functionality to start, stop, and monitor background check commands.
- Created a DiagnosticsPanel for real-time output display in the UI.
- Added support for auto-detecting default diagnostics commands based on project configuration.
- Introduced run_checks tool to execute project verification commands and report results.
- Enhanced tools-extra with functions to parse AGENTS.md and package.json for check commands.
- Added diff review functionality to visualize changes made in the last turn.
- Implemented tests for diagnostics and run_checks functionalities to ensure reliability.
2026-09-11 22:09:19 +07:00

138 lines
5.8 KiB
TypeScript

import { renderDiffReview } from '../diff-review';
import { costOf, formatUsd, PRICING_VERIFIED_AT } 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,
info: { sessionId: string; model: string; agent: string; thinking: string; subagentModel?: string },
): Panel {
const spend = costOf(info.model, session.inputTokens, session.outputTokens);
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)}`,
);
}
const perTurn = session.maxSpendPerTurn();
if (perTurn !== undefined) {
lines.push(`- per-turn cap: ${formatUsd(perTurn)}`);
}
const cache = session.promptCacheStats();
if (cache.hits + cache.misses > 0) {
lines.push(`- prompt cache: ${cache.hits} hits / ${cache.misses} misses`);
}
lines.push(`- context: ~${session.estimatedTokens()} tokens (est.)`, `- agent: \`${info.agent}\` thinking \`${info.thinking}\``);
lines.push(`- pricing verified: ${PRICING_VERIFIED_AT} (est., verify before billing)`);
return { title: 'cost', hint: `session ${info.sessionId}`, body: lines.join('\n') };
}
export function contextPanel(files: readonly string[]): Panel {
const tracker = files.filter((f) => f.endsWith('TODO.md') || f.endsWith('ROADMAP.md'));
const instructions = files.filter((f) => !tracker.includes(f));
const body: string[] = [];
if (instructions.length > 0) body.push('instructions:', ...instructions.map((f) => `- \`${f}\``));
if (tracker.length > 0) body.push('trackers:', ...tracker.map((f) => `- \`${f}\``));
if (body.length === 0) {
body.push('No `AGENTS.md`, `CLAUDE.md`, or `.shiro.md` found. Run `/init` to write one.');
body.push('No `TODO.md` or `ROADMAP.md` found — no project tracker is loaded.');
}
return { title: 'project instructions & trackers', body: body.join('\n') };
}
export const todosPanel = (session: Session): Panel => ({
title: 'task list',
body: todoLines(session.notebook.state().todos),
});
const renderChangeList = (kind: string, paths: string[]): string =>
paths.length === 0 ? '' : `${kind}:\n${paths.map((p) => `- ${p}`).join('\n')}`;
/** What the last turn changed on disk — the file side of /undo, without undoing. */
export function changesPanel(session: Session): Panel {
const summary = session.lastTurnSummary();
if (!summary) return { title: 'changes', body: 'the last turn changed no files (bash effects are not tracked)' };
const body = [
renderChangeList('added', summary.added),
renderChangeList('modified', summary.modified),
renderChangeList('deleted', summary.deleted),
]
.filter(Boolean)
.join('\n');
return { title: 'changes', body };
}
/** Project workflow state: what the repo tracks and how the session behaved. */
export function workflowPanel(session: Session): Panel {
const w = session.workflowStatus();
const yes = (b: boolean): string => (b ? 'yes' : 'no');
const rows = [
['workflow', w.enabled ? 'on' : 'off'],
['TODO.md', `${yes(w.hasTodo)}${w.hasTodo ? ` (${w.todoLines} lines)` : ''}`],
['ROADMAP.md', `${yes(w.hasRoadmap)}${w.hasRoadmap ? ` (${w.roadmapLines} lines)` : ''}`],
['docs dir', `${yes(w.hasDocs)}${w.hasDocs ? ` (${w.docsFiles} files)` : ''}`],
['reminders sent', w.nudgeCount === 0 ? 'none' : `${w.nudgeCount}/3`],
];
const body = rows.map(([k, v]) => `${k}: ${v}`).join('\n');
return { title: 'workflow', body };
}
/** Raw unified diff of the last turn's file changes. */
export function diffPanel(session: Session): Panel {
const diff = session.diffLastTurn();
if (!diff) return { title: 'diff', body: 'the last turn changed no files (bash effects are not diffed)' };
return { title: 'diff', body: diff };
}
/** Structured per-hunk review of the last turn's file changes. */
export function diffReviewPanel(session: Session): Panel {
const diff = session.diffLastTurn();
if (!diff) return { title: 'diff review', body: 'the last turn changed no files (bash effects are not diffed)' };
return { title: 'diff review', hint: 'file:line anchors point to the new side', body: renderDiffReview(diff) };
}