<script lang="ts">
// A session's transcript, in the group the reader is in.
//
// The transcript view reads the ONE session being read (`sessions`'
// `activeSessionId`): its messages, its live updates, the message scrolled
// to. So it is mounted once, in the focused group, and this is what keeps it
// fed — the messages of the session in front, the scroll a link or a search
// hit asked for.
import { untrack } from "svelte";
import SessionEditorView from "../content/SessionEditorView.svelte";
import { messages } from "../../stores/messages.svelte.js";
import { sessions } from "../../stores/sessions.svelte.js";
import { sync } from "../../stores/sync.svelte.js";
import { ui } from "../../stores/ui.svelte.js";
import type { DisplayItem } from "../../utils/display-items.js";
import { enrichSegments, parseContent } from "../../utils/content-parser.js";
import { shouldAutoSwitchTranscriptModeToNormal } from "../../utils/transcript-mode.js";
interface Props {
sessionId: string;
}
let { sessionId }: Props = $props();
let messageList:
| {
scrollToOrdinal: (o: number) => void;
getDisplayItems: () => DisplayItem[];
getNormalDisplayItems: () => DisplayItem[];
}
| undefined = $state(undefined);
// The session in front is the one being read.
$effect(() => {
const id = sessionId;
untrack(() => {
if (sessions.activeSessionId !== id) void sessions.navigateToSession(id);
});
});
// Its messages, and its live updates. Only the session id is followed: the
// loading flags this sets are not reasons to load again.
$effect(() => {
const id = sessions.activeSessionId;
untrack(() => {
// A scroll queued for this session (a search hit selects the session and
// the message together) is kept; any other is dropped.
const pendingMatchesSession =
ui.pendingScrollOrdinal !== null && (ui.pendingScrollSession === null || ui.pendingScrollSession === id);
if (!pendingMatchesSession) {
ui.clearSelection();
ui.pendingScrollOrdinal = null;
ui.pendingScrollSession = null;
}
if (id) {
messages.loadSession(id);
sessions.loadChildSessions(id);
sync.watchSession(id, () => {
messages.reload();
sessions.refreshActiveSession();
sessions.loadChildSessions(id);
});
}
});
return () => {
sync.unwatchSession();
};
});
// Scroll to the message asked for once the messages are loaded. A message
// hidden only because thinking is hidden turns thinking on; one hidden by
// another filter is left alone — changing unrelated filters is unexpected.
$effect(() => {
const ordinal = ui.pendingScrollOrdinal;
const loading = messages.loading;
const thinkingVisible = ui.isBlockVisible("thinking");
untrack(() => {
if (ordinal === null || loading || !messageList) return;
// `-1` is "the last message": resolved once there are messages.
if (ordinal === -1) {
const last = messages.messages[messages.messages.length - 1];
const target = ui.pendingScrollSession;
if (last && (target === null || target === messages.sessionId)) ui.scrollToOrdinal(last.ordinal, target ?? undefined);
return;
}
const items = messageList.getDisplayItems();
const found = items.some((item) => item.ordinals.includes(ordinal));
if (!found) {
if (shouldAutoSwitchTranscriptModeToNormal(ui.transcriptMode, ordinal, items, messageList.getNormalDisplayItems())) {
ui.setTranscriptMode("normal");
return;
}
const message = messages.messages.find((m) => m.ordinal === ordinal);
if (message && !thinkingVisible) {
const segments = enrichSegments(parseContent(message.content, message.has_tool_use), message.tool_calls);
if (segments.some((segment) => segment.type === "thinking")) {
ui.setBlockVisible("thinking", true);
return;
}
}
}
messageList.scrollToOrdinal(ordinal);
ui.selectedOrdinal = ordinal;
ui.pendingScrollOrdinal = null;
ui.pendingScrollSession = null;
});
});
</script>
<SessionEditorView bind:bindMessageList={messageList} />