fix: restore model activity reasoning stream
This commit is contained in:
@@ -58,6 +58,7 @@ export interface UiResponse {
|
||||
export type StreamEvent =
|
||||
| { type: "ui_request"; ui_request: WidgetDescriptor }
|
||||
| { type: "text_delta"; text: string }
|
||||
| { type: "activity_delta"; text: string }
|
||||
| { type: "info"; level?: "info" | "warning" | "error"; text: string }
|
||||
| { type: "system_event"; event: string; [k: string]: unknown };
|
||||
|
||||
|
||||
@@ -6,14 +6,22 @@ import { ModelActivityPanel } from "./ModelActivityPanel";
|
||||
|
||||
beforeEach(() => useSessionStore.getState().resetSession());
|
||||
|
||||
test("renders the streamed model transcript", () => {
|
||||
useSessionStore.getState().applyEvent({ type: "text_delta", text: "Promoting table dim_patient." });
|
||||
test("renders the streamed model activity", () => {
|
||||
useSessionStore.getState().applyEvent({ type: "activity_delta", text: "Promoting table dim_patient." });
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
expect(screen.getByText(/Promoting table dim_patient/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("does not treat final assistant text as model reasoning", () => {
|
||||
useSessionStore.getState().applyEvent({ type: "text_delta", text: "Final answer" });
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
|
||||
expect(screen.queryByText("Final answer")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("No activity yet.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders markdown emphasis instead of literal asterisks", () => {
|
||||
useSessionStore.getState().applyEvent({ type: "text_delta", text: "Chiarimento su **fibrillazione atriale**." });
|
||||
useSessionStore.getState().applyEvent({ type: "activity_delta", text: "Chiarimento su **fibrillazione atriale**." });
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
expect(screen.getByText("fibrillazione atriale").tagName).toBe("STRONG");
|
||||
});
|
||||
@@ -28,7 +36,7 @@ test("close button calls onClose", async () => {
|
||||
const EIGHT = ["para-01", "para-02", "para-03", "para-04", "para-05", "para-06", "para-07", "para-08"].join("\n\n");
|
||||
|
||||
test("collapsed shows only the last 5 paragraphs of the model-stream tail", () => {
|
||||
useSessionStore.getState().applyEvent({ type: "text_delta", text: EIGHT });
|
||||
useSessionStore.getState().applyEvent({ type: "activity_delta", text: EIGHT });
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
expect(screen.getByText("para-08")).toBeInTheDocument();
|
||||
expect(screen.getByText("para-04")).toBeInTheDocument();
|
||||
@@ -37,7 +45,7 @@ test("collapsed shows only the last 5 paragraphs of the model-stream tail", () =
|
||||
});
|
||||
|
||||
test("expanding reveals the full stream", async () => {
|
||||
useSessionStore.getState().applyEvent({ type: "text_delta", text: EIGHT });
|
||||
useSessionStore.getState().applyEvent({ type: "activity_delta", text: EIGHT });
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
await userEvent.click(screen.getByRole("button", { name: /show more/i }));
|
||||
expect(screen.getByText("para-01")).toBeInTheDocument();
|
||||
@@ -46,7 +54,7 @@ test("expanding reveals the full stream", async () => {
|
||||
test("paragraphs stay separated as distinct blocks", () => {
|
||||
useSessionStore
|
||||
.getState()
|
||||
.applyEvent({ type: "text_delta", text: "Primo paragrafo.\n\nSecondo paragrafo." });
|
||||
.applyEvent({ type: "activity_delta", text: "Primo paragrafo.\n\nSecondo paragrafo." });
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
const first = screen.getByText("Primo paragrafo.");
|
||||
const second = screen.getByText("Secondo paragrafo.");
|
||||
@@ -58,7 +66,7 @@ test("paragraphs stay separated as distinct blocks", () => {
|
||||
|
||||
test("separates activity updates concatenated after sentence punctuation", () => {
|
||||
useSessionStore.getState().applyEvent({
|
||||
type: "text_delta",
|
||||
type: "activity_delta",
|
||||
text: "Ambiguità principale risolta.Finestra temporale risolta.Terza ambiguità risolta.",
|
||||
});
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
|
||||
@@ -24,13 +24,13 @@ export function formatModelActivity(text: string): string {
|
||||
* (refreshing as the stream grows); the expand toggle reveals the full stream. Opened
|
||||
* on demand from the rotating activity icon. */
|
||||
export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
|
||||
const transcript = useSessionStore((s) => s.transcript);
|
||||
const activity = useSessionStore((s) => s.activity);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Blank lines are markdown's paragraph separator — split on them (not on every "\n",
|
||||
// which would also break single soft-wrapped lines apart) so each block below is a
|
||||
// real paragraph and the tail cut lands on paragraph boundaries.
|
||||
const paragraphs = transcript
|
||||
const paragraphs = activity
|
||||
.map((e) => e.text)
|
||||
.join("\n\n")
|
||||
.split(/\n{2,}/)
|
||||
|
||||
@@ -15,6 +15,15 @@ test("text_delta accumulates into transcript", () => {
|
||||
expect(useSessionStore.getState().transcript.at(-1)?.text).toBe("Analisi");
|
||||
});
|
||||
|
||||
test("activity_delta accumulates separately from the assistant transcript", () => {
|
||||
const s = useSessionStore.getState();
|
||||
s.applyEvent({ type: "activity_delta", text: "Valuto " });
|
||||
s.applyEvent({ type: "activity_delta", text: "le ambiguità" });
|
||||
|
||||
expect(useSessionStore.getState().activity.at(-1)?.text).toBe("Valuto le ambiguità");
|
||||
expect(useSessionStore.getState().transcript).toEqual([]);
|
||||
});
|
||||
|
||||
test("info appends to stepMessages", () => {
|
||||
useSessionStore.getState().applyEvent({ type: "info", level: "warning", text: "attenzione" });
|
||||
expect(useSessionStore.getState().stepMessages.at(-1)).toEqual({ level: "warning", text: "attenzione" });
|
||||
@@ -122,4 +131,5 @@ test("resetSession clears lastUserEntry and stepMessages", () => {
|
||||
st.resetSession();
|
||||
expect(useSessionStore.getState().lastUserEntry).toBeNull();
|
||||
expect(useSessionStore.getState().stepMessages).toEqual([]);
|
||||
expect(useSessionStore.getState().activity).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ interface Entry {
|
||||
interface SessionState {
|
||||
pendingWidget: WidgetDescriptor | null;
|
||||
transcript: Entry[];
|
||||
activity: Entry[];
|
||||
toasts: { level: string; text: string }[];
|
||||
stepMessages: { level: string; text: string }[];
|
||||
lastUserEntry: { kind: "input" | "choice"; text: string } | null;
|
||||
@@ -31,6 +32,7 @@ interface SessionState {
|
||||
const empty = {
|
||||
pendingWidget: null,
|
||||
transcript: [] as Entry[],
|
||||
activity: [] as Entry[],
|
||||
toasts: [] as { level: string; text: string }[],
|
||||
stepMessages: [] as { level: string; text: string }[],
|
||||
lastUserEntry: null as { kind: "input" | "choice"; text: string } | null,
|
||||
@@ -63,6 +65,13 @@ export const useSessionStore = create<SessionState>((set) => ({
|
||||
// Streamed text means the turn is alive (also covers reattaching mid-turn).
|
||||
return { transcript: t, agentActive: true };
|
||||
}
|
||||
if (e.type === "activity_delta") {
|
||||
const activity = [...st.activity];
|
||||
const last = activity.at(-1);
|
||||
if (last) activity[activity.length - 1] = { role: "assistant", text: last.text + e.text };
|
||||
else activity.push({ role: "assistant", text: e.text });
|
||||
return { activity, agentActive: true };
|
||||
}
|
||||
if (e.type === "info") {
|
||||
const stepMessages = [...st.stepMessages, { level: e.level ?? "info", text: e.text }];
|
||||
// An error during the active phase marks that phase red until the next gate.
|
||||
|
||||
Reference in New Issue
Block a user