Merge remote-tracking branch 'origin/develop' into work/T-669-subjektiv-memory-tools

This commit is contained in:
2026-10-02 07:39:25 +09:00
7 changed files with 598 additions and 72 deletions
@@ -0,0 +1,113 @@
// @vitest-environment happy-dom
import type { Event } from "$lib/generated/protocol";
import { cleanup, render, waitFor } from "@testing-library/svelte";
import { afterEach, expect, test } from "vitest";
import ConsoleLineItem from "./ConsoleLineItem.svelte";
import { createConsoleProjector } from "./model.ts";
afterEach(cleanup);
function reconnectSnapshot(text: string): Event {
return {
event: "snapshot",
data: {
session: {
entries: [],
pending_submissions: {
revision: 0,
head_id: null,
notification_count: 0,
submissions: [],
},
},
greeting: {
worker_name: "Worker",
cwd: "/repo",
provider: "provider",
model: "model",
scope_summary: "bounded",
tools: [],
context_window: 100,
context_tokens: 20,
},
state: {
last_command_id: 0,
state: { kind: "busy", state: { kind: "run", state: "running" } },
},
in_flight: {
blocks: [{ kind: "text", text, finished: false }],
},
},
};
}
test("reconnected assistant output keeps the normal streaming Markdown path", async () => {
const projector = createConsoleProjector();
let projection = projector.append([{
eventId: "snapshot",
event: reconnectSnapshot("**hel"),
}]);
const { container, rerender } = render(ConsoleLineItem, {
item: projection.lines[0],
});
expect(container.querySelector("li")?.classList.contains("assistant")).toBe(
true,
);
expect(container.querySelector(".message-heading")).toBeNull();
expect(container.querySelector(".console-plain-text")).toBeNull();
expect(container.textContent).not.toContain("in-flight");
projection = projector.append([{
eventId: "inline-suffix",
event: {
event: "text_delta",
data: { text: "lo** and [safe](https://exam" },
},
}]);
await rerender({ item: projection.lines[0] });
await waitFor(() => {
expect(container.querySelector("strong")?.textContent).toBe("hello");
expect(container.querySelector('a[href="https://example.com"]')).toBeNull();
expect(container.textContent).toContain("[safe](https://exam");
});
projection = projector.append([{
eventId: "fence-prefix",
event: {
event: "text_delta",
data: { text: "ple.com)\n\n```ts\nconst answer = 42;" },
},
}]);
await rerender({ item: projection.lines[0] });
await waitFor(() => {
expect(container.querySelector('a[href="https://example.com"]')).not
.toBeNull();
expect(container.querySelector("pre code")?.textContent).toContain(
"const answer = 42;",
);
});
projection = projector.append([{
eventId: "fence-suffix",
event: { event: "text_delta", data: { text: "\n```" } },
}]);
await rerender({ item: projection.lines[0] });
await waitFor(() => {
expect(container.querySelector("strong")?.textContent).toBe("hello");
expect(container.querySelector('a[href="https://example.com"]')).not
.toBeNull();
expect(container.querySelector("pre code")?.textContent).toContain(
"const answer = 42;",
);
});
expect(projection.lines).toHaveLength(1);
expect(projection.lines[0]).toMatchObject({
kind: "assistant",
streaming: true,
body:
"**hello** and [safe](https://example.com)\n\n```ts\nconst answer = 42;\n```",
});
});
@@ -346,6 +346,36 @@ Deno.test("line previews include a current turn outside the retained five-turn p
); );
}); });
Deno.test("line previews track a snapshot-restored assistant as its body streams", () => {
const lines = [{
id: "user",
entryId: "user-entry",
kind: "user" as const,
title: "User",
body: "question",
source: "event" as const,
}, {
id: "restored-assistant",
kind: "assistant" as const,
title: "assistant streaming",
body: "**hel",
source: "event" as const,
streaming: true,
}];
assert(
conversationTurnPreviewsFromLines(lines)[0].assistant.join("|") ===
"**hel",
"restored assistant prefix must use the ordinary assistant preview path",
);
const continued = [lines[0], { ...lines[1], body: "**hello**" }];
assert(
conversationTurnPreviewsFromLines(continued)[0].assistant.join("|") ===
"**hello**",
"live suffixes must update the same turn preview",
);
});
Deno.test("turn preview uses one user line and final non-empty assistant up to three lines", () => { Deno.test("turn preview uses one user line and final non-empty assistant up to three lines", () => {
const preview = conversationTurnPreviews([turn(4)])[0]; const preview = conversationTurnPreviews([turn(4)])[0];
assert(preview.user === "question 4", "user preview must use its first line"); assert(preview.user === "question 4", "user preview must use its first line");
@@ -203,8 +203,7 @@ export function conversationTurnPreviewsFromLines(
assistants: [], assistants: [],
}; };
} else if ( } else if (
(line.kind === "assistant" || line.kind === "assistant" &&
(line.kind === "in_flight" && line.title === "in-flight assistant text")) &&
current && line.body.trim().length > 0 current && line.body.trim().length > 0
) { ) {
current.assistants.push(line.body); current.assistants.push(line.body);
@@ -1,5 +1,6 @@
import type { import type {
Event, Event,
InFlightBlock,
SessionSnapshotEntry, SessionSnapshotEntry,
WorkerStateSnapshot, WorkerStateSnapshot,
WorkerStatus, WorkerStatus,
@@ -176,6 +177,39 @@ function snapshotEvent(cwd: string, entries: unknown[] = []): Event {
}; };
} }
type SnapshotEvent = Extract<Event, { event: "snapshot" }>;
function snapshotWithInFlight(
blocks: InFlightBlock[],
entries: unknown[] = [],
): SnapshotEvent {
const snapshot = snapshotEvent("/repo", entries) as SnapshotEvent;
snapshot.data.in_flight = { blocks };
return snapshot;
}
function semanticOutput(lines: readonly ConsoleLine[]) {
return lines.map((line) => ({
kind: line.kind,
title: line.title,
body: line.body,
streaming: line.streaming,
entryId: line.entryId,
toolCall: line.toolCall
? {
id: line.toolCall.id,
name: line.toolCall.name,
argsStream: line.toolCall.argsStream,
arguments: line.toolCall.arguments,
state: line.toolCall.state,
summary: line.toolCall.summary,
output: line.toolCall.output,
isError: line.toolCall.isError,
}
: undefined,
}));
}
Deno.test("large paste segments project compact artifact metadata", () => { Deno.test("large paste segments project compact artifact metadata", () => {
const body = "secret pasted body"; const body = "secret pasted body";
const text = segmentsToText([{ const text = segmentsToText([{
@@ -2334,11 +2368,273 @@ Deno.test("projectConsole renders snapshot entries and in-flight output", () =>
"user:new user:false", "user:new user:false",
"assistant:assistant reply:false", "assistant:assistant reply:false",
"tool:Read(1 file)\n /tmp/a.md:false", "tool:Read(1 file)\n /tmp/a.md:false",
"in_flight:partial:true", "assistant:partial:true",
], ],
); );
}); });
Deno.test("snapshot text blocks match live prefixes, suffixes, and ordered commits", () => {
const committedPrefix = [{
kind: "user_input",
segments: [{ kind: "text", content: "question" }],
}];
const livePrefix: ConsoleEventInput[] = [
{ eventId: "live-base", event: snapshotEvent("/repo", committedPrefix) },
{
eventId: "live-first-delta",
event: { event: "text_delta", data: { text: "first draft" } },
},
{
eventId: "live-first-done",
event: { event: "text_done", data: { text: "first draft" } },
},
{
eventId: "live-second-delta",
event: { event: "text_delta", data: { text: "**hel" } },
},
];
const restoredPrefix: ConsoleEventInput[] = [{
eventId: "restored-base",
event: snapshotWithInFlight([
{ kind: "text", text: "first draft", finished: true },
{ kind: "text", text: "**hel", finished: false },
], committedPrefix),
}];
assertEquals(
semanticOutput(projectConsole(restoredPrefix).lines),
semanticOutput(projectConsole(livePrefix).lines),
);
const firstCommit: SessionSnapshotEntry = {
entry_id: "assistant-first",
timestamp: 2,
provenance: "model_output",
kind: "message",
role: "assistant",
content: [{ kind: "text", text: "first canonical" }],
};
const secondCommit: SessionSnapshotEntry = {
entry_id: "assistant-second",
timestamp: 3,
provenance: "model_output",
kind: "message",
role: "assistant",
content: [{ kind: "text", text: "**hello**" }],
};
const suffix: ConsoleEventInput[] = [
{
eventId: "commit-first",
event: {
event: "session_entry_committed",
data: { entry: firstCommit },
},
},
{
eventId: "second-suffix",
event: { event: "text_delta", data: { text: "lo**" } },
},
{
eventId: "second-done",
event: { event: "text_done", data: { text: "**hello**" } },
},
{
eventId: "commit-second",
event: {
event: "session_entry_committed",
data: { entry: secondCommit },
},
},
];
const liveFinal = projectConsole([...livePrefix, ...suffix]);
const restoredFinal = projectConsole([...restoredPrefix, ...suffix]);
assertEquals(
semanticOutput(restoredFinal.lines),
semanticOutput(liveFinal.lines),
);
assertEquals(
restoredFinal.lines.filter((line) => line.kind === "assistant").map(
(line) => [line.body, line.entryId, line.streaming],
),
[
["first canonical", "assistant-first", false],
["**hello**", "assistant-second", false],
],
);
});
Deno.test("snapshot thinking blocks preserve boundaries and live continuation", () => {
const livePrefix: ConsoleEventInput[] = [
{ eventId: "base", event: snapshotEvent("/repo") },
{ eventId: "thinking-1-start", event: { event: "thinking_start" } },
{
eventId: "thinking-1-delta",
event: { event: "thinking_delta", data: { text: "finished thought" } },
},
{
eventId: "thinking-1-done",
event: { event: "thinking_done", data: { text: "finished thought" } },
},
{ eventId: "thinking-2-start", event: { event: "thinking_start" } },
{
eventId: "thinking-2-delta",
event: { event: "thinking_delta", data: { text: "next tho" } },
},
];
const restoredPrefix: ConsoleEventInput[] = [{
eventId: "restored",
event: snapshotWithInFlight([
{ kind: "thinking", text: "finished thought", finished: true },
{ kind: "thinking", text: "next tho", finished: false },
]),
}];
assertEquals(
semanticOutput(projectConsole(restoredPrefix).lines),
semanticOutput(projectConsole(livePrefix).lines),
);
const suffix: ConsoleEventInput[] = [
{
eventId: "thinking-2-suffix",
event: { event: "thinking_delta", data: { text: "ught" } },
},
{
eventId: "thinking-2-done",
event: { event: "thinking_done", data: { text: "next thought" } },
},
];
const restored = projectConsole([...restoredPrefix, ...suffix]);
assertEquals(
semanticOutput(restored.lines),
semanticOutput(projectConsole([...livePrefix, ...suffix]).lines),
);
assertEquals(
restored.lines.map((line) => [line.kind, line.body, line.streaming]),
[
["thinking", "finished thought", false],
["thinking", "next thought", false],
],
);
assertEquals(projectOverviewLines(restored.lines), []);
});
Deno.test("snapshot tool calls retain identity and state through live suffixes", () => {
const livePrefix: ConsoleEventInput[] = [
{ eventId: "base", event: snapshotEvent("/repo") },
{
eventId: "pending-start",
event: {
event: "tool_call_start",
data: { id: "pending", name: "Read" },
},
},
{
eventId: "streaming-start",
event: {
event: "tool_call_start",
data: { id: "streaming", name: "Bash" },
},
},
{
eventId: "streaming-args",
event: {
event: "tool_call_args_delta",
data: { id: "streaming", json: '{"command":"ec' },
},
},
{
eventId: "done-start",
event: { event: "tool_call_start", data: { id: "done", name: "Glob" } },
},
{
eventId: "done-args",
event: {
event: "tool_call_args_delta",
data: { id: "done", json: '{"pattern":"*.ts"}' },
},
},
{
eventId: "done-call",
event: {
event: "tool_call_done",
data: { id: "done", name: "Glob", arguments: '{"pattern":"*.ts"}' },
},
},
];
const restoredPrefix: ConsoleEventInput[] = [{
eventId: "restored",
event: snapshotWithInFlight([
{
kind: "tool_call",
id: "pending",
name: "Read",
args: "",
state: "pending",
},
{
kind: "tool_call",
id: "streaming",
name: "Bash",
args: '{"command":"ec',
state: "streaming_args",
},
{
kind: "tool_call",
id: "done",
name: "Glob",
args: '{"pattern":"*.ts"}',
state: "done",
},
]),
}];
assertEquals(
semanticOutput(projectConsole(restoredPrefix).lines),
semanticOutput(projectConsole(livePrefix).lines),
);
const suffix: ConsoleEventInput[] = [
{
eventId: "pending-done",
event: {
event: "tool_call_done",
data: { id: "pending", name: "Read", arguments: '{"file_path":"a"}' },
},
},
{
eventId: "streaming-suffix",
event: {
event: "tool_call_args_delta",
data: { id: "streaming", json: 'ho hi"}' },
},
},
{
eventId: "streaming-done",
event: {
event: "tool_call_done",
data: {
id: "streaming",
name: "Bash",
arguments: '{"command":"echo hi"}',
},
},
},
...["pending", "streaming", "done"].map((id): ConsoleEventInput => ({
eventId: `${id}-result`,
event: {
event: "tool_result",
data: { id, summary: `${id} result`, output: id, is_error: false },
},
})),
];
assertEquals(
semanticOutput(projectConsole([...restoredPrefix, ...suffix]).lines),
semanticOutput(projectConsole([...livePrefix, ...suffix]).lines),
);
});
Deno.test("projectConsole restores system items from snapshot entries", () => { Deno.test("projectConsole restores system items from snapshot entries", () => {
const projection = projectConsole([{ const projection = projectConsole([{
eventId: "system-snapshot", eventId: "system-snapshot",
@@ -2666,6 +2962,65 @@ Deno.test("Internal Worker output stays separate and revision-fenced", () => {
assertEquals(resolveConsoleWorkerView(projection, "missing").sessionId, null); assertEquals(resolveConsoleWorkerView(projection, "missing").sessionId, null);
}); });
Deno.test("Internal Worker snapshot output continues without entering the parent view", () => {
const worker = {
session_id: "child-reconnect",
name: "research",
parent_session_id: "parent-session",
kind: "sub_worker" as const,
};
const snapshot = snapshotEvent("/repo") as SnapshotEvent;
snapshot.data.in_flight = {
blocks: [{ kind: "text", text: "parent", finished: false }],
};
snapshot.data.internal_workers = [{
worker,
revision: 4,
session: canonicalSession([]),
status: "running",
in_flight: {
blocks: [{ kind: "text", text: "**chi", finished: false }],
},
internal_workers: [],
}];
const projector = createConsoleProjector();
let projection = projector.append([{ eventId: "snapshot", event: snapshot }]);
assertEquals(projection.lines.map((line) => line.body), ["parent"]);
assertEquals(
projection.internalWorkers[0].console.lines.map((line) => line.body),
["**chi"],
);
projection = projector.append([
{
eventId: "parent-suffix",
event: { event: "text_delta", data: { text: " only" } },
},
{
eventId: "child-suffix",
event: {
event: "internal_worker",
data: {
worker,
revision: 5,
event: { event: "text_delta", data: { text: "ld**" } },
},
},
},
]);
assertEquals(projection.lines.map((line) => line.body), ["parent only"]);
assertEquals(
projection.internalWorkers[0].console.lines.map((line) => [
line.kind,
line.body,
line.streaming,
]),
[["assistant", "**child**", true]],
);
});
Deno.test("console Worker views expose only direct Internal Workers", () => { Deno.test("console Worker views expose only direct Internal Workers", () => {
const projector = createConsoleProjector(); const projector = createConsoleProjector();
projector.append([{ projector.append([{
@@ -3032,10 +3387,7 @@ Deno.test("overview hides thinking and aggregates uninterrupted tool activity",
Deno.test("overview keeps tool failure counts without marking the activity as an error", () => { Deno.test("overview keeps tool failure counts without marking the activity as an error", () => {
const overview = projectOverviewLines([ const overview = projectOverviewLines([
{ consoleLine("thinking-active", "thinking"),
...consoleLine("thinking-in-flight", "in_flight"),
title: "in-flight thinking",
},
{ {
...consoleLine("failed-read", "tool"), ...consoleLine("failed-read", "tool"),
error: true, error: true,
@@ -3398,7 +3750,8 @@ Deno.test("committed history inserts current live rows after their last stable a
historyUser.entryId = "user-1"; historyUser.entryId = "user-1";
const currentUser = consoleLine("current-user", "user"); const currentUser = consoleLine("current-user", "user");
currentUser.entryId = "user-1"; currentUser.entryId = "user-1";
const live = consoleLine("live-assistant", "in_flight"); const live = consoleLine("live-assistant", "assistant");
live.streaming = true;
assertEquals( assertEquals(
mergeCommittedHistoryLines([historyUser], [currentUser, live]).map((line) => mergeCommittedHistoryLines([historyUser], [currentUser, live]).map((line) =>
@@ -40,7 +40,6 @@ export type ConsoleLineKind =
| "status" | "status"
| "error" | "error"
| "usage" | "usage"
| "in_flight"
| "system"; | "system";
type ToolCallState = type ToolCallState =
@@ -361,8 +360,7 @@ function projectVisibleConsole(
} }
function isOverviewThinkingLine(line: ConsoleLine): boolean { function isOverviewThinkingLine(line: ConsoleLine): boolean {
return line.kind === "thinking" || return line.kind === "thinking";
(line.kind === "in_flight" && line.title === "in-flight thinking");
} }
function representedToolCallCount(line: ConsoleLine): number { function representedToolCallCount(line: ConsoleLine): number {
@@ -559,12 +557,58 @@ function appendSnapshotInFlightLines(
eventId: string, eventId: string,
cwd: string | null, cwd: string | null,
): void { ): void {
const lineIds = new Set(projection.lines.map((line) => line.id));
blocks.forEach((block, index) => { blocks.forEach((block, index) => {
const pending = inFlightLine(`${eventId}:${index}`, block, cwd); const blockEventId = `${eventId}:${index}`;
if (lineIds.has(pending.id)) return; switch (block.kind) {
projection.lines.push(pending); case "text":
lineIds.add(pending.id); appendStreaming(
projection,
blockEventId,
"assistant",
"assistant streaming",
block.text,
);
if (block.finished) {
finalizeStreaming(
projection,
"assistant",
blockEventId,
"assistant",
block.text,
);
}
break;
case "thinking":
startStreaming(projection, blockEventId, "thinking", "Thinking...");
if (block.text) {
appendStreaming(
projection,
blockEventId,
"thinking",
"Thinking...",
block.text,
);
}
if (block.finished) {
finalizeStreaming(
projection,
"thinking",
blockEventId,
"Thought",
block.text,
);
}
break;
case "tool_call":
upsertToolCall(projection, blockEventId, block.id, {
name: block.name,
argsStream: block.args,
arguments: block.state === "done" ? block.args : undefined,
state: inFlightToolState(block.state),
cwd,
});
break;
}
}); });
} }
@@ -924,10 +968,23 @@ export function applyProtocolEvent(
entryId: event.data.entry_id ?? undefined, entryId: event.data.entry_id ?? undefined,
}); });
break; break;
case "session_entry_committed": case "session_entry_committed": {
reconcileCommittedSessionEntry(next, event.data.entry); const replacementIndex = reconcileCommittedSessionEntry(
next,
event.data.entry,
);
applySessionEntry(next, envelope.eventId, event.data.entry); applySessionEntry(next, envelope.eventId, event.data.entry);
if (replacementIndex !== undefined) {
const committedIndex = next.lines.findIndex((line) =>
line.entryId === event.data.entry.entry_id
);
if (committedIndex >= 0) {
const [committed] = next.lines.splice(committedIndex, 1);
next.lines.splice(replacementIndex, 0, committed);
}
}
break; break;
}
case "system_item": case "system_item":
next.lines.push(systemItemLine(envelope.eventId, event.data.item)); next.lines.push(systemItemLine(envelope.eventId, event.data.item));
applyTaskSystemItem(next, event.data.item); applyTaskSystemItem(next, event.data.item);
@@ -951,9 +1008,7 @@ export function applyProtocolEvent(
); );
break; break;
case "thinking_start": case "thinking_start":
next.lines.push( startStreaming(next, envelope.eventId, "thinking", "Thinking...");
line(envelope.eventId, "thinking", "Thinking...", "", undefined, true),
);
break; break;
case "thinking_delta": case "thinking_delta":
appendStreaming( appendStreaming(
@@ -1296,7 +1351,8 @@ function appendDurableRunFailure(
) return; ) return;
projection.lines = projection.lines.filter((line) => { projection.lines = projection.lines.filter((line) => {
if (isDurableRunFailure(line)) return true; if (isDurableRunFailure(line)) return true;
return !(line.kind === "error" && runFailureMessagesMatch(line.body, message)); return !(line.kind === "error" &&
runFailureMessagesMatch(line.body, message));
}); });
projection.lines.push({ projection.lines.push({
...line( ...line(
@@ -1344,6 +1400,15 @@ function findLastLineIndex(
return -1; return -1;
} }
function startStreaming(
projection: ConsoleProjection,
eventId: string,
kind: "assistant" | "thinking",
title: string,
): void {
projection.lines.push(line(eventId, kind, title, "", undefined, true));
}
function appendStreaming( function appendStreaming(
projection: ConsoleProjection, projection: ConsoleProjection,
eventId: string, eventId: string,
@@ -2131,16 +2196,22 @@ function snapshotProjectionFromSession(
function reconcileCommittedSessionEntry( function reconcileCommittedSessionEntry(
projection: ConsoleProjection, projection: ConsoleProjection,
entry: SessionSnapshotEntry, entry: SessionSnapshotEntry,
): void { ): number | undefined {
if (entry.kind !== "message" || entry.role !== "assistant") return; if (entry.kind !== "message" || entry.role !== "assistant") return;
for (let index = projection.lines.length - 1; index >= 0; index -= 1) { const turnStart = findLastLineIndex(
projection.lines,
(line) => line.kind === "user",
);
for (let index = turnStart + 1; index < projection.lines.length; index += 1) {
const line = projection.lines[index]; const line = projection.lines[index];
if (line.entryId) continue; if (
if (line.kind === "assistant" || line.kind === "in_flight") { line.kind === "assistant" &&
!line.entryId &&
line.streaming !== true
) {
projection.lines.splice(index, 1); projection.lines.splice(index, 1);
return; return index;
} }
if (line.kind === "user") return;
} }
} }
@@ -2358,42 +2429,6 @@ function loggedContentText(parts: unknown[]): string {
.join("\n"); .join("\n");
} }
function inFlightLine(
eventId: string,
block: InFlightBlock,
cwd: string | null,
): ConsoleLine {
switch (block.kind) {
case "text":
return line(
eventId,
"in_flight",
"in-flight assistant text",
block.text,
undefined,
!block.finished,
);
case "thinking":
return line(
eventId,
"in_flight",
"in-flight thinking",
block.text,
undefined,
!block.finished,
);
case "tool_call":
return toolLine(eventId, {
id: block.id,
name: block.name,
argsStream: block.args,
arguments: block.state === "done" ? block.args : undefined,
state: inFlightToolState(block.state),
cwd,
});
}
}
function inFlightToolState( function inFlightToolState(
state: InFlightToolCallState | undefined, state: InFlightToolCallState | undefined,
): ToolCallState { ): ToolCallState {
@@ -54,11 +54,11 @@ Deno.test("streaming updates change the preview without creating another turn",
equal(answer.body, "Hi"); equal(answer.body, "Hi");
}); });
Deno.test("snapshot in-flight assistant text is previewed but in-flight thinking is not", () => { Deno.test("snapshot-restored assistant text is previewed but thinking is not", () => {
equal(consoleTurns([ equal(consoleTurns([
line("u", "user", "Question"), line("u", "user", "Question"),
{ ...line("reasoning", "in_flight", "Private"), title: "in-flight thinking" }, line("reasoning", "thinking", "Private"),
{ ...line("text", "in_flight", "Answer in progress"), title: "in-flight assistant text" }, line("text", "assistant", "Answer in progress"),
]), [{ id: "u", user: "Question", assistant: "Answer in progress" }]); ]), [{ id: "u", user: "Question", assistant: "Answer in progress" }]);
}); });
@@ -23,11 +23,7 @@ export function consoleTurns(lines: readonly ConsoleLine[]): ConsoleTurn[] {
if (line.kind === "user") { if (line.kind === "user") {
current = { id: line.id, user: previewText(line.body), assistant: "" }; current = { id: line.id, user: previewText(line.body), assistant: "" };
turns.push(current); turns.push(current);
} else if ( } else if (current && line.kind === "assistant") {
current &&
(line.kind === "assistant" ||
(line.kind === "in_flight" && line.title === "in-flight assistant text"))
) {
const assistant = previewText(line.body); const assistant = previewText(line.body);
if (assistant) current.assistant = assistant; if (assistant) current.assistant = assistant;
} }