story-kit: merge 117_story_show_startup_reconciliation_progress_in_ui
This commit is contained in:
@@ -18,6 +18,11 @@ type WsHandlers = {
|
||||
onUpdate: (history: Message[]) => void;
|
||||
onSessionId: (sessionId: string) => void;
|
||||
onError: (message: string) => void;
|
||||
onReconciliationProgress: (
|
||||
storyId: string,
|
||||
status: string,
|
||||
message: string,
|
||||
) => void;
|
||||
};
|
||||
let capturedWsHandlers: WsHandlers | null = null;
|
||||
|
||||
@@ -310,3 +315,81 @@ describe("Chat input Shift+Enter behavior", () => {
|
||||
expect((input as HTMLTextAreaElement).value).toBe("Hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Chat reconciliation banner", () => {
|
||||
beforeEach(() => {
|
||||
capturedWsHandlers = null;
|
||||
setupMocks();
|
||||
});
|
||||
|
||||
it("shows banner when a non-done reconciliation event is received", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
act(() => {
|
||||
capturedWsHandlers?.onReconciliationProgress(
|
||||
"42_story_test",
|
||||
"checking",
|
||||
"Checking for committed work in 2_current/",
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByTestId("reconciliation-banner"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText("Reconciling startup state..."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows event message in the banner", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
act(() => {
|
||||
capturedWsHandlers?.onReconciliationProgress(
|
||||
"42_story_test",
|
||||
"gates_running",
|
||||
"Running acceptance gates…",
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Running acceptance gates/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("dismisses banner when done event is received", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
act(() => {
|
||||
capturedWsHandlers?.onReconciliationProgress(
|
||||
"42_story_test",
|
||||
"checking",
|
||||
"Checking for committed work",
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByTestId("reconciliation-banner"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
capturedWsHandlers?.onReconciliationProgress(
|
||||
"",
|
||||
"done",
|
||||
"Startup reconciliation complete.",
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("reconciliation-banner"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,11 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
const [isNarrowScreen, setIsNarrowScreen] = useState(
|
||||
window.innerWidth < NARROW_BREAKPOINT,
|
||||
);
|
||||
const [reconciliationActive, setReconciliationActive] = useState(false);
|
||||
const [reconciliationEvents, setReconciliationEvents] = useState<
|
||||
{ id: string; storyId: string; status: string; message: string }[]
|
||||
>([]);
|
||||
const reconciliationEventIdRef = useRef(0);
|
||||
|
||||
const wsRef = useRef<ChatWebSocket | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -197,6 +202,19 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
onActivity: (toolName) => {
|
||||
setActivityStatus(formatToolActivity(toolName));
|
||||
},
|
||||
onReconciliationProgress: (storyId, status, message) => {
|
||||
if (status === "done") {
|
||||
setReconciliationActive(false);
|
||||
} else {
|
||||
setReconciliationActive(true);
|
||||
setReconciliationEvents((prev) => {
|
||||
const id = String(reconciliationEventIdRef.current++);
|
||||
const next = [...prev, { id, storyId, status, message }];
|
||||
// Keep only the last 8 events to avoid the banner growing too tall.
|
||||
return next.slice(-8);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -679,6 +697,52 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Startup reconciliation progress banner */}
|
||||
{reconciliationActive && (
|
||||
<div
|
||||
data-testid="reconciliation-banner"
|
||||
style={{
|
||||
padding: "6px 24px",
|
||||
background: "#1c2a1c",
|
||||
borderTop: "1px solid #2d4a2d",
|
||||
fontSize: "0.8em",
|
||||
color: "#7ec87e",
|
||||
maxHeight: "100px",
|
||||
overflowY: "auto",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
marginBottom: "2px",
|
||||
color: "#a0d4a0",
|
||||
}}
|
||||
>
|
||||
Reconciling startup state...
|
||||
</div>
|
||||
{reconciliationEvents.map((evt) => (
|
||||
<div
|
||||
key={evt.id}
|
||||
style={{
|
||||
color:
|
||||
evt.status === "failed"
|
||||
? "#d07070"
|
||||
: evt.status === "advanced"
|
||||
? "#80c880"
|
||||
: "#666",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{evt.storyId ? `[${evt.storyId}] ` : ""}
|
||||
{evt.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chat input pinned at bottom of left column */}
|
||||
<div
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user