story-kit: merge 300_story_show_token_cost_badge_on_pipeline_board_work_items

This commit is contained in:
Dave
2026-03-19 11:00:10 +00:00
parent b6f99ce7a2
commit 36535b639f
4 changed files with 137 additions and 0 deletions

View File

@@ -202,6 +202,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [agentConfigVersion, setAgentConfigVersion] = useState(0);
const [agentStateVersion, setAgentStateVersion] = useState(0);
const [pipelineVersion, setPipelineVersion] = useState(0);
const [storyTokenCosts, setStoryTokenCosts] = useState<Map<string, number>>(
new Map(),
);
const [needsOnboarding, setNeedsOnboarding] = useState(false);
const onboardingTriggeredRef = useRef(false);
const [selectedWorkItemId, setSelectedWorkItemId] = useState<string | null>(
@@ -363,6 +366,29 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
onPipelineState: (state) => {
setPipeline(state);
setPipelineVersion((v) => v + 1);
const allItems = [
...state.backlog,
...state.current,
...state.qa,
...state.merge,
...state.done,
];
for (const item of allItems) {
api
.getTokenCost(item.story_id)
.then((cost) => {
if (cost.total_cost_usd > 0) {
setStoryTokenCosts((prev) => {
const next = new Map(prev);
next.set(item.story_id, cost.total_cost_usd);
return next;
});
}
})
.catch(() => {
// Silently ignore — cost data may not exist yet.
});
}
},
onPermissionRequest: (requestId, toolName, toolInput) => {
setPermissionQueue((prev) => [
@@ -1005,26 +1031,31 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
<StagePanel
title="Done"
items={pipeline.done ?? []}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="To Merge"
items={pipeline.merge}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="QA"
items={pipeline.qa}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="Current"
items={pipeline.current}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="Backlog"
items={pipeline.backlog}
costs={storyTokenCosts}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<ServerLogsPanel logs={serverLogs} />

View File

@@ -42,6 +42,8 @@ interface StagePanelProps {
items: PipelineStageItem[];
emptyMessage?: string;
onItemClick?: (item: PipelineStageItem) => void;
/** Map of story_id → total_cost_usd for displaying cost badges. */
costs?: Map<string, number>;
}
function AgentLozenge({
@@ -128,6 +130,7 @@ export function StagePanel({
items,
emptyMessage = "Empty.",
onItemClick,
costs,
}: StagePanelProps) {
return (
<div
@@ -240,6 +243,19 @@ export function StagePanel({
{typeLabel}
</span>
)}
{costs?.has(item.story_id) && (
<span
data-testid={`cost-badge-${item.story_id}`}
style={{
fontSize: "0.65em",
fontWeight: 600,
color: "#e3b341",
marginRight: "8px",
}}
>
${costs.get(item.story_id)?.toFixed(2)}
</span>
)}
{item.name ?? item.story_id}
</div>
{item.error && (