refactor(app): remove launcher tabs from workspace flow

This commit is contained in:
Mohamed Boudra
2026-04-04 15:37:26 +07:00
parent 2ca7dd71e2
commit 5e1b24b217
24 changed files with 473 additions and 735 deletions
@@ -279,52 +279,52 @@ describe("processAgentStreamEvent", () => {
expect(result.cursorChanged).toBe(false);
});
it("appends committed live rows to tail and clears superseded provisional assistant state", () => {
const currentHead: StreamItem[] = [
{
kind: "assistant_message",
id: "head-assistant",
text: "partial",
timestamp: new Date(1000),
},
];
const currentCursor: TimelineCursor = { startSeq: 1, endSeq: 120 };
const result = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("finalized reply"),
seq: 121,
currentHead,
currentCursor,
});
expect(result.changedTail).toBe(true);
expect(result.changedHead).toBe(true);
expect(result.head).toEqual([]);
expect(result.cursorChanged).toBe(true);
expect(result.cursor).toEqual({
startSeq: 1,
endSeq: 121,
});
expect(result.tail[result.tail.length - 1]).toMatchObject({
kind: "assistant_message",
text: "finalized reply",
});
});
it("replaces provisional tool progress when the committed tool row arrives", () => {
const provisional = processAgentStreamEvent({
it("keeps tool call rows anchored in tail across live and committed updates", () => {
const running = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("running"),
seq: undefined,
});
expect(running.head).toEqual([]);
expect(running.tail).toHaveLength(1);
expect(running.tail[0]).toMatchObject({
kind: "tool_call",
payload: {
source: "agent",
data: {
callId: "call-1",
status: "running",
},
},
});
const completedLive = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("completed"),
seq: undefined,
currentHead: running.head,
currentTail: running.tail,
currentCursor: { startSeq: 1, endSeq: 7 },
});
expect(completedLive.head).toEqual([]);
expect(completedLive.tail).toHaveLength(1);
expect(completedLive.tail[0]).toMatchObject({
kind: "tool_call",
payload: {
source: "agent",
data: {
callId: "call-1",
status: "completed",
},
},
});
const committed = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("completed"),
seq: 8,
currentHead: provisional.head,
currentTail: provisional.tail,
currentHead: completedLive.head,
currentTail: completedLive.tail,
currentCursor: { startSeq: 1, endSeq: 7 },
});
@@ -342,6 +342,51 @@ describe("processAgentStreamEvent", () => {
});
});
it("preserves assistant/tool interleaving while a turn is streaming", () => {
const assistantBeforeTool = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("before"),
seq: undefined,
});
const runningTool = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("running"),
seq: undefined,
currentHead: assistantBeforeTool.head,
currentTail: assistantBeforeTool.tail,
});
const assistantAfterTool = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("after"),
seq: undefined,
currentHead: runningTool.head,
currentTail: runningTool.tail,
});
const completedTool = processAgentStreamEvent({
...baseStreamInput,
event: makeToolCallEvent("completed"),
seq: undefined,
currentHead: assistantAfterTool.head,
currentTail: assistantAfterTool.tail,
});
expect(completedTool.head).toEqual([]);
expect(completedTool.tail.map((item) => item.kind)).toEqual([
"assistant_message",
"tool_call",
"assistant_message",
]);
expect(
completedTool.tail[0]?.kind === "assistant_message" ? completedTool.tail[0].text : null,
).toBe("before");
expect(
completedTool.tail[2]?.kind === "assistant_message" ? completedTool.tail[2].text : null,
).toBe("after");
});
it("requests catch-up when a committed live row skips ahead", () => {
const result = processAgentStreamEvent({
...baseStreamInput,
@@ -359,27 +404,30 @@ describe("processAgentStreamEvent", () => {
});
});
it("clears provisional head on terminal turn events without committing it to tail", () => {
it("flushes provisional head into tail on terminal turn events", () => {
const withHead = processAgentStreamEvent({
...baseStreamInput,
event: makeTimelineEvent("streaming"),
seq: undefined,
});
const result = processAgentStreamEvent({
...baseStreamInput,
event: {
type: "turn_completed",
provider: "claude",
},
currentHead: [
{
kind: "thought",
id: "reasoning-1",
text: "thinking",
timestamp: new Date(1000),
status: "loading",
},
],
currentHead: withHead.head,
currentTail: withHead.tail,
});
expect(result.changedHead).toBe(true);
expect(result.changedTail).toBe(false);
expect(result.changedTail).toBe(true);
expect(result.head).toEqual([]);
expect(result.tail).toEqual([]);
expect(result.tail).toHaveLength(1);
expect(result.tail[0]).toMatchObject({
kind: "assistant_message",
text: "streaming",
});
});
});
@@ -1,7 +1,7 @@
import type { AgentStreamEventPayload } from "@server/shared/messages";
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
import type { StreamItem } from "@/types/stream";
import { hydrateStreamState, reduceStreamUpdate } from "@/types/stream";
import { applyStreamEvent, hydrateStreamState, reduceStreamUpdate } from "@/types/stream";
import {
classifySessionTimelineSeq,
type SessionTimelineSeqDecision,
@@ -289,10 +289,7 @@ export function processAgentStreamEvent(
): ProcessAgentStreamEventOutput {
const { event, seq, currentTail, currentHead, currentCursor, currentAgent, timestamp } = input;
let nextTail = currentTail;
let nextHead = currentHead;
let changedTail = false;
let changedHead = false;
let shouldApplyStreamEvent = true;
let nextTimelineCursor: TimelineCursor | null = null;
let cursorChanged = false;
const sideEffects: AgentStreamReducerSideEffect[] = [];
@@ -304,42 +301,39 @@ export function processAgentStreamEvent(
});
if (decision === "gap") {
shouldApplyStreamEvent = false;
if (currentCursor) {
sideEffects.push({
type: "catch_up",
cursor: { endSeq: currentCursor.endSeq },
});
}
} else if (decision !== "drop_stale") {
nextTail = reduceStreamUpdate(currentTail, event, timestamp, {
source: "canonical",
});
changedTail = nextTail !== currentTail;
nextHead = removeSupersededProvisionalItems(currentHead, event);
changedHead = nextHead !== currentHead;
} else if (decision === "drop_stale") {
shouldApplyStreamEvent = false;
} else {
nextTimelineCursor =
decision === "init"
? { startSeq: seq, endSeq: seq }
: { ...(currentCursor ?? { startSeq: seq, endSeq: seq }), endSeq: seq };
cursorChanged = !cursorsEqual(currentCursor, nextTimelineCursor);
}
} else if (event.type === "timeline") {
nextHead = reduceStreamUpdate(currentHead, event, timestamp, {
source: "live",
});
changedHead = nextHead !== currentHead;
} else if (
(event.type === "turn_completed" ||
event.type === "turn_canceled" ||
event.type === "turn_failed") &&
currentHead.length > 0
) {
nextHead = [];
changedHead = true;
}
const { tail, head, changedTail, changedHead } = shouldApplyStreamEvent
? applyStreamEvent({
tail: currentTail,
head: currentHead,
event,
timestamp,
source: "live",
})
: {
tail: currentTail,
head: currentHead,
changedTail: false,
changedHead: false,
};
let agentPatch: AgentPatch | null = null;
let agentChanged = false;
@@ -366,8 +360,8 @@ export function processAgentStreamEvent(
}
return {
tail: nextTail,
head: nextHead,
tail,
head,
changedTail,
changedHead,
cursor: nextTimelineCursor,