Shell.tsx had grown to about 8,000 lines, roughly 2,200 of them module-level components and helpers below ShellPage. That made the page hard to navigate and review and turned every chat change into an edit of one hot file. This is a move-only refactor: code is moved verbatim and only imports and exports change. No renames, no JSX or memo boundary changes. - shell/transcript.tsx: Transcript, QuoteSelectionButton, MessageHoverActions, MessageView - shell/composer.tsx: Composer, slashActionLabel, MentionOptionIcon, MentionChipIcon, ATTACHMENT_ACCEPT - shell/computer-screen.tsx: ComputerReleaseActions, embeddableScreenUrl, screenIframeSandbox, DesktopKindEmptyState, computerPlaceholder, computerLabel (all used by the computer pane) - shell/thread-state.ts: threadSnapshotSignal, firstThreadRoute, applyThreadEvent, newClientNonce - shell/roster-format.ts: stripSpaceTitlePrefix, formatRosterTime, cleanRosterPreview - shell/message-preview.ts: previewMessageText (shared by composer and transcript) - shell/collapsed-sidebar-sections.ts: collapsed sidebar storage helpers - shell/shell-types.ts: PendingAttachment and FALLBACK_BOT_COLOR, shared by Shell.tsx, composer and transcript Types, constants and one-off helpers used only by ShellPage stay in Shell.tsx. The Lingui catalogs only change their file-reference comments; message ids and translations are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import type { Bot, ComputerStatus, Group, ProductEvent, ThreadSnapshot } from "@rakazo/contracts";
|
|
import type { MutableRefObject } from "react";
|
|
import {
|
|
isComputerStatusEvent,
|
|
isThreadSnapshotEvent,
|
|
reduceComputerStatus,
|
|
reduceThreadSnapshot,
|
|
} from "../../lib/thread-events";
|
|
|
|
const THREAD_SNAPSHOT_TIMEOUT_MS = 2_000;
|
|
|
|
export function threadSnapshotSignal(parent: AbortSignal): AbortSignal {
|
|
return AbortSignal.any([parent, AbortSignal.timeout(THREAD_SNAPSHOT_TIMEOUT_MS)]);
|
|
}
|
|
|
|
export function firstThreadRoute(
|
|
bots: readonly Pick<Bot, "id">[],
|
|
groups: readonly Pick<Group, "id" | "kind">[],
|
|
/** In a project the user lands in its main conversation. */
|
|
inProject = false,
|
|
): string {
|
|
const main = inProject ? groups.find((group) => group.kind === "main") : undefined;
|
|
if (main) return `/app/g/${main.id}`;
|
|
if (bots[0]) return `/app/${bots[0].id}`;
|
|
if (groups[0]) return `/app/g/${groups[0].id}`;
|
|
return "/app";
|
|
}
|
|
|
|
export function applyThreadEvent(
|
|
event: ProductEvent,
|
|
commitSnapshot: (next: ThreadSnapshot | null) => void,
|
|
commitComputer: (next: ComputerStatus | null) => void,
|
|
snapshotRef: MutableRefObject<ThreadSnapshot | null>,
|
|
computerRef: MutableRefObject<ComputerStatus | null>,
|
|
) {
|
|
if (isThreadSnapshotEvent(event)) {
|
|
const next = reduceThreadSnapshot(snapshotRef.current, event);
|
|
commitSnapshot(next);
|
|
}
|
|
if (isComputerStatusEvent(event)) {
|
|
const next = reduceComputerStatus(computerRef.current, event);
|
|
commitComputer(next);
|
|
}
|
|
}
|
|
|
|
export function newClientNonce(): string {
|
|
const webCrypto = globalThis.crypto;
|
|
if (webCrypto && typeof webCrypto.randomUUID === "function") {
|
|
return webCrypto.randomUUID();
|
|
}
|
|
return `m-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
}
|