evOo-local/apps/web/src/pages/shell/thread-state.ts
Claude a0060b8df7
refactor(web): move Shell.tsx module-level code into pages/shell modules
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
2026-10-04 09:38:16 +00:00

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)}`;
}