story-kit: merge 300_story_show_token_cost_badge_on_pipeline_board_work_items
This commit is contained in:
@@ -143,6 +143,20 @@ export interface SearchResult {
|
||||
matches: number;
|
||||
}
|
||||
|
||||
export interface AgentCostEntry {
|
||||
agent_name: string;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_creation_input_tokens: number;
|
||||
cache_read_input_tokens: number;
|
||||
total_cost_usd: number;
|
||||
}
|
||||
|
||||
export interface TokenCostResponse {
|
||||
total_cost_usd: number;
|
||||
agents: AgentCostEntry[];
|
||||
}
|
||||
|
||||
export interface CommandOutput {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
@@ -316,6 +330,13 @@ export const api = {
|
||||
baseUrl,
|
||||
);
|
||||
},
|
||||
getTokenCost(storyId: string, baseUrl?: string) {
|
||||
return requestJson<TokenCostResponse>(
|
||||
`/work-items/${encodeURIComponent(storyId)}/token-cost`,
|
||||
{},
|
||||
baseUrl,
|
||||
);
|
||||
},
|
||||
/** Approve a story in QA, moving it to merge. */
|
||||
approveQa(storyId: string) {
|
||||
return callMcpTool("approve_qa", { story_id: storyId });
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user