story-kit: merge 224_story_expand_work_item_to_full_screen_detail_view

This commit is contained in:
Dave
2026-02-27 11:21:35 +00:00
parent 3912f8ecc8
commit 101bfd78fe
6 changed files with 449 additions and 10 deletions

View File

@@ -13,6 +13,7 @@ import { ChatInput } from "./ChatInput";
import { LozengeFlyProvider } from "./LozengeFlyContext";
import { MessageItem } from "./MessageItem";
import { StagePanel } from "./StagePanel";
import { WorkItemDetailPanel } from "./WorkItemDetailPanel";
const { useCallback, useEffect, useMemo, useRef, useState } = React;
@@ -189,6 +190,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [agentStateVersion, setAgentStateVersion] = useState(0);
const [needsOnboarding, setNeedsOnboarding] = useState(false);
const onboardingTriggeredRef = useRef(false);
const [selectedWorkItemId, setSelectedWorkItemId] = useState<string | null>(
null,
);
const [queuedMessages, setQueuedMessages] = useState<
{ id: string; text: string }[]
>([]);
@@ -879,16 +883,45 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
}}
>
<LozengeFlyProvider pipeline={pipeline}>
<AgentPanel
configVersion={agentConfigVersion}
stateVersion={agentStateVersion}
/>
{selectedWorkItemId ? (
<WorkItemDetailPanel
storyId={selectedWorkItemId}
onClose={() => setSelectedWorkItemId(null)}
/>
) : (
<>
<AgentPanel
configVersion={agentConfigVersion}
stateVersion={agentStateVersion}
/>
<StagePanel title="Done" items={pipeline.done ?? []} />
<StagePanel title="To Merge" items={pipeline.merge} />
<StagePanel title="QA" items={pipeline.qa} />
<StagePanel title="Current" items={pipeline.current} />
<StagePanel title="Upcoming" items={pipeline.upcoming} />
<StagePanel
title="Done"
items={pipeline.done ?? []}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="To Merge"
items={pipeline.merge}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="QA"
items={pipeline.qa}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="Current"
items={pipeline.current}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
<StagePanel
title="Upcoming"
items={pipeline.upcoming}
onItemClick={(item) => setSelectedWorkItemId(item.story_id)}
/>
</>
)}
</LozengeFlyProvider>
</div>
</div>

View File

@@ -34,6 +34,7 @@ interface StagePanelProps {
title: string;
items: PipelineStageItem[];
emptyMessage?: string;
onItemClick?: (item: PipelineStageItem) => void;
}
function AgentLozenge({
@@ -119,6 +120,7 @@ export function StagePanel({
title,
items,
emptyMessage = "Empty.",
onItemClick,
}: StagePanelProps) {
return (
<div
@@ -189,6 +191,20 @@ export function StagePanel({
gap: "2px",
}}
>
const cardStyle = {
border: item.agent ? "1px solid #2a3a4a" : "1px solid #2a2a2a",
borderRadius: "8px",
padding: "8px 12px",
background: item.agent ? "#161e2a" : "#191919",
display: "flex",
flexDirection: "column" as const,
gap: "2px",
width: "100%",
textAlign: "left" as const,
cursor: onItemClick ? "pointer" : "default",
};
const cardInner = (
<>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
{hasMergeFailure && (
@@ -260,6 +276,25 @@ export function StagePanel({
{item.agent && (
<AgentLozenge agent={item.agent} storyId={item.story_id} />
)}
</>
);
return onItemClick ? (
<button
key={`${title}-${item.story_id}`}
type="button"
data-testid={`card-${item.story_id}`}
onClick={() => onItemClick(item)}
style={cardStyle}
>
{cardInner}
</button>
) : (
<div
key={`${title}-${item.story_id}`}
data-testid={`card-${item.story_id}`}
style={cardStyle}
>
{cardInner}
</div>
);
})}

View File

@@ -0,0 +1,217 @@
import * as React from "react";
import Markdown from "react-markdown";
import { api } from "../api/client";
const { useEffect, useRef, useState } = React;
const STAGE_LABELS: Record<string, string> = {
upcoming: "Upcoming",
current: "Current",
qa: "QA",
merge: "To Merge",
done: "Done",
archived: "Archived",
};
interface WorkItemDetailPanelProps {
storyId: string;
onClose: () => void;
}
export function WorkItemDetailPanel({
storyId,
onClose,
}: WorkItemDetailPanelProps) {
const [content, setContent] = useState<string | null>(null);
const [stage, setStage] = useState<string>("");
const [name, setName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const panelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setLoading(true);
setError(null);
api
.getWorkItemContent(storyId)
.then((data) => {
setContent(data.content);
setStage(data.stage);
setName(data.name);
})
.catch((err: unknown) => {
setError(err instanceof Error ? err.message : "Failed to load content");
})
.finally(() => {
setLoading(false);
});
}, [storyId]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
const stageLabel = STAGE_LABELS[stage] ?? stage;
return (
<div
data-testid="work-item-detail-panel"
ref={panelRef}
style={{
display: "flex",
flexDirection: "column",
height: "100%",
overflow: "hidden",
background: "#1a1a1a",
borderRadius: "8px",
border: "1px solid #333",
}}
>
{/* Header */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
borderBottom: "1px solid #333",
flexShrink: 0,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "2px",
minWidth: 0,
}}
>
<div
data-testid="detail-panel-title"
style={{
fontWeight: 600,
fontSize: "0.95em",
color: "#ececec",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{name ?? storyId}
</div>
{stage && (
<div
data-testid="detail-panel-stage"
style={{ fontSize: "0.75em", color: "#888" }}
>
{stageLabel}
</div>
)}
</div>
<button
type="button"
data-testid="detail-panel-close"
onClick={onClose}
style={{
background: "none",
border: "1px solid #444",
borderRadius: "6px",
color: "#aaa",
cursor: "pointer",
padding: "4px 10px",
fontSize: "0.8em",
flexShrink: 0,
}}
>
Close
</button>
</div>
{/* Scrollable content area */}
<div
style={{
flex: 1,
overflowY: "auto",
padding: "16px",
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
{loading && (
<div
data-testid="detail-panel-loading"
style={{ color: "#666", fontSize: "0.85em" }}
>
Loading...
</div>
)}
{error && (
<div
data-testid="detail-panel-error"
style={{ color: "#ff7b72", fontSize: "0.85em" }}
>
{error}
</div>
)}
{!loading && !error && content !== null && (
<div
data-testid="detail-panel-content"
className="markdown-body"
style={{ fontSize: "0.9em", lineHeight: 1.6 }}
>
<Markdown>{content}</Markdown>
</div>
)}
{/* Placeholder sections for future content */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
{(
[
{ id: "agent-logs", label: "Agent Logs" },
{ id: "test-output", label: "Test Output" },
{ id: "coverage", label: "Coverage" },
] as { id: string; label: string }[]
).map(({ id, label }) => (
<div
key={id}
data-testid={`placeholder-${id}`}
style={{
border: "1px solid #2a2a2a",
borderRadius: "8px",
padding: "10px 12px",
background: "#161616",
}}
>
<div
style={{
fontWeight: 600,
fontSize: "0.8em",
color: "#555",
marginBottom: "4px",
}}
>
{label}
</div>
<div style={{ fontSize: "0.75em", color: "#444" }}>
Coming soon
</div>
</div>
))}
</div>
</div>
</div>
);
}