@anthusai/papyrus 5.39.0 → 5.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/amplify/data/resource.js +6 -0
  2. package/amplify/data/resource.ts +7 -0
  3. package/amplify/data/schema.js +6 -0
  4. package/amplify/data/schema.ts +7 -0
  5. package/amplify/functions/content-actions/handler.py +10 -1
  6. package/amplify/functions/monitoring-view/schema.js +12 -0
  7. package/amplify/functions/monitoring-view/schema.ts +12 -0
  8. package/amplify/functions/monitoring-view/wire.js +25 -0
  9. package/amplify/functions/monitoring-view/wire.ts +32 -0
  10. package/amplify/site-backend.js +2 -1
  11. package/amplify/site-backend.ts +2 -1
  12. package/app/api/newsroom/chat/[...path]/route.ts +17 -0
  13. package/app/layout.tsx +5 -1
  14. package/components/monitoring-overview-view.tsx +182 -9
  15. package/components/monitoring-panel.tsx +23 -3
  16. package/components/notification-center.tsx +12 -8
  17. package/components/papyrus-console-conversation.css +14 -0
  18. package/components/papyrus-console-conversation.tsx +55 -0
  19. package/components/papyrus-console-shell.tsx +62 -420
  20. package/components/papyrus-private-chat-panel.tsx +134 -0
  21. package/infra/amplify-app-shell.js +2 -0
  22. package/infra/amplify-app-shell.ts +2 -0
  23. package/infra/site-config.js +4 -0
  24. package/infra/site-config.ts +4 -0
  25. package/lib/monitoring-client.ts +69 -15
  26. package/lib/monitoring-overview-fixture.ts +1 -1
  27. package/lib/monitoring-overview.ts +48 -5
  28. package/lib/papyrus-chat-admission.ts +65 -0
  29. package/lib/papyrus-hosted-chat.ts +217 -0
  30. package/lib/papyrus-private-chat-client.ts +81 -0
  31. package/lib/prospectus-session.ts +15 -0
  32. package/lib/prospectus-sign-in-routes.ts +2 -12
  33. package/lib/review-client.ts +6 -3
  34. package/lib/review-requests.ts +8 -6
  35. package/lib/shared-chat-admission.ts +182 -0
  36. package/lib/site-origin-value.ts +50 -0
  37. package/lib/site-origin.ts +3 -62
  38. package/package.json +10 -1
  39. package/routes.manifest.json +13 -1
  40. package/src/with-papyrus.mjs +1 -1
@@ -0,0 +1,14 @@
1
+ .papyrus-console-conversation {
2
+ display: flex;
3
+ flex: 1;
4
+ flex-direction: column;
5
+ min-height: 0;
6
+ --chatticus-chat-surface: rgb(var(--paper-rgb));
7
+ --chatticus-chat-raised: color-mix(in srgb, rgb(var(--paper-rgb)) 94%, rgb(var(--ink-rgb)));
8
+ --chatticus-chat-foreground: var(--ink);
9
+ --chatticus-chat-accent: var(--ink);
10
+ --chatticus-chat-error: var(--ink);
11
+ --chatticus-chat-action: var(--ink);
12
+ --chatticus-chat-action-text: rgb(var(--paper-rgb));
13
+ }
14
+ .papyrus-console-conversation .papyrus-console__suggestions { padding: 12px 0; }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { useMemo, type ReactNode } from "react";
4
+ import { ChatRuntime, type ChatMessage, type MessagePresentation } from "chatticus/chat-react";
5
+
6
+ /** Editorial console data adapted to the shared presentation without exposing GraphQL or auth. */
7
+ export type ConsoleConversationMessage = {
8
+ id: string;
9
+ role: string | null | undefined;
10
+ text: string;
11
+ createdAt?: string;
12
+ pending?: boolean;
13
+ failed?: boolean;
14
+ error?: string | null;
15
+ context?: boolean;
16
+ };
17
+
18
+ /** Normalize host records; Papyrus keeps thread ownership, persistence and response state. */
19
+ export function consoleConversationMessages(messages: readonly ConsoleConversationMessage[]): ChatMessage[] {
20
+ return messages.map((message) => ({
21
+ id: message.id,
22
+ role: message.role === "USER" ? "user" : "assistant",
23
+ content: [{ type: "text", text: message.failed ? message.error || "Responder failed" : message.text }],
24
+ createdAt: message.createdAt ? new Date(message.createdAt) : undefined,
25
+ status: message.failed ? { type: "incomplete", reason: "error" } : message.pending ? { type: "running" } : undefined,
26
+ metadata: { custom: {
27
+ authorLabel: message.context ? "Page context" : undefined,
28
+ failed: message.failed,
29
+ isStreaming: message.pending,
30
+ isStreamingShell: message.pending && !message.text,
31
+ createdAt: message.createdAt,
32
+ } },
33
+ }));
34
+ }
35
+
36
+ /** Controlled newsroom adapter for the reusable Chatticus transcript and composer. */
37
+ export function PapyrusConsoleConversation({ messages, isSending, isRunning, onSendMessage, presentation, composerActions, emptyState, error }: {
38
+ messages: readonly ConsoleConversationMessage[];
39
+ isSending: boolean;
40
+ isRunning: boolean;
41
+ onSendMessage: (text: string) => Promise<void>;
42
+ presentation?: MessagePresentation;
43
+ composerActions?: ReactNode;
44
+ emptyState?: ReactNode;
45
+ error?: string | null;
46
+ }) {
47
+ const sharedMessages = useMemo(() => consoleConversationMessages(messages), [messages]);
48
+ return <div className="papyrus-console-conversation" data-reply-running={isRunning || undefined}>
49
+ {/* Papyrus routes can accept another editor message while the current reply runs.
50
+ The pending assistant message carries its own running status for display. */}
51
+ <ChatRuntime messages={sharedMessages} isRunning={false} isSendDisabled={isSending}
52
+ onSendMessage={onSendMessage} presentation={presentation}
53
+ threadProps={{ composerPlaceholder: "Write a message...", composerActions, emptyState, streamError: error }} />
54
+ </div>;
55
+ }
@@ -6,7 +6,6 @@ import dynamic from "next/dynamic";
6
6
  import { usePathname, useRouter, useSearchParams } from "next/navigation";
7
7
  import { CheckIcon, UserIcon } from "lucide-react";
8
8
  import { Md5 } from "@smithy/md5-js";
9
- import { gsap } from "gsap";
10
9
  import {
11
10
  createConsoleMessage,
12
11
  createConsoleThread,
@@ -35,10 +34,9 @@ import {
35
34
  subscribeReaderSettingsChanges,
36
35
  type ReaderMotionSetting,
37
36
  } from "./reader-settings";
38
- import { Conversation, ConversationContent, ConversationScrollButton } from "./ai-elements/conversation";
37
+ import { PapyrusConsoleConversation, type ConsoleConversationMessage } from "./papyrus-console-conversation";
38
+ import { PapyrusPrivateChatPanel } from "./papyrus-private-chat-panel";
39
39
  import { ModelSelector } from "./ai-elements/model-selector";
40
- import { PromptInput, PromptInputBody, PromptInputFooter, type PromptInputMessage, PromptInputSubmit, PromptInputTextarea, PromptInputTools } from "./ai-elements/prompt-input";
41
- import { Shimmer } from "./ai-elements/shimmer";
42
40
  import { Suggestion, Suggestions } from "./ai-elements/suggestion";
43
41
  import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "./ai-elements/tool";
44
42
  import { CONNECTION_STATE_CHANGE, ConnectionState } from "aws-amplify/data";
@@ -54,7 +52,6 @@ import {
54
52
  useRef,
55
53
  useState,
56
54
  } from "react";
57
- import { useStickToBottomContext } from "use-stick-to-bottom";
58
55
 
59
56
  const CONSOLE_STARTER_SUGGESTIONS = [
60
57
  "Start a research assignment",
@@ -108,6 +105,7 @@ const CONSOLE_MODEL_OPTIONS = [
108
105
 
109
106
  type PapyrusConsoleShellProps = {
110
107
  children: ReactNode;
108
+ privateChatEnabled?: boolean;
111
109
  };
112
110
 
113
111
  type PapyrusConsoleContextValue = {
@@ -148,7 +146,7 @@ type ConsoleAuthorIdentity = {
148
146
 
149
147
  const PapyrusConsoleContext = createContext<PapyrusConsoleContextValue | null>(null);
150
148
 
151
- export function PapyrusConsoleShell({ children }: PapyrusConsoleShellProps) {
149
+ export function PapyrusConsoleShell({ children, privateChatEnabled = false }: PapyrusConsoleShellProps) {
152
150
  const pathname = usePathname();
153
151
  const [session, setSession] = useState<ReaderSessionSnapshot | null>(null);
154
152
  const [open, setOpen] = useState(false);
@@ -367,7 +365,9 @@ export function PapyrusConsoleShell({ children }: PapyrusConsoleShellProps) {
367
365
  <aside className={open ? "papyrus-console papyrus-console--open" : "papyrus-console"} aria-label="Papyrus console">
368
366
  {open ? (
369
367
  canUseConsole
370
- ? <ConsolePanel actorEmail={session?.auth.status === "signedIn" ? (session.auth.email ?? null) : null} actorLabel={session?.auth.label ?? "Papyrus editor"} onClose={toggleOpen} />
368
+ ? privateChatEnabled
369
+ ? <PapyrusPrivateChatPanel onClose={toggleOpen} />
370
+ : <ConsolePanel actorEmail={session?.auth.status === "signedIn" ? (session.auth.email ?? null) : null} actorLabel={session?.auth.label ?? "Papyrus editor"} onClose={toggleOpen} />
371
371
  : <ConsoleAccessPanel onClose={toggleOpen} session={session} />
372
372
  ) : null}
373
373
  </aside>
@@ -421,7 +421,6 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
421
421
  const [thread, setThread] = useState<ConsoleChatThread | null>(null);
422
422
  const [threads, setThreads] = useState<ConsoleChatThread[]>([]);
423
423
  const [messages, setMessages] = useState<ConsoleChatMessage[]>([]);
424
- const [draft, setDraft] = useState("");
425
424
  const [loading, setLoading] = useState(false);
426
425
  const [sending, setSending] = useState(false);
427
426
  const [error, setError] = useState<string | null>(null);
@@ -863,13 +862,6 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
863
862
  }
864
863
  }
865
864
 
866
- async function sendMessage(message: PromptInputMessage) {
867
- const content = message.text.trim();
868
- if (!content) return;
869
- setDraft("");
870
- await submitMessage(content);
871
- }
872
-
873
865
  function handleSuggestionClick(suggestion: string) {
874
866
  if (sending) return;
875
867
  void submitMessage(suggestion);
@@ -1045,18 +1037,18 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
1045
1037
  }
1046
1038
  return latestAgentEntryId;
1047
1039
  }, [isSupersededRunningAssistantMessage, timelineEntries]);
1048
- const streamingAutoStickSignal = useMemo(() => {
1049
- const activeStreamingAssistant = [...displayMessages]
1050
- .reverse()
1051
- .find((message) => message.role === "ASSISTANT" && message.responseStatus === "RUNNING");
1052
- if (!activeStreamingAssistant) return null;
1053
- return [
1054
- activeStreamingAssistant.id,
1055
- activeStreamingAssistant.pendingPlaceholder ? "placeholder" : "message",
1056
- (activeStreamingAssistant.content ?? "").length,
1057
- activeStreamingAssistant.updatedAt ?? activeStreamingAssistant.createdAt,
1058
- ].join(":");
1059
- }, [displayMessages]);
1040
+ const presentationEntries = useMemo(() => new Map(timelineEntries.map((entry) => [entry.id, entry])), [timelineEntries]);
1041
+ const conversationMessages = useMemo<ConsoleConversationMessage[]>(() => timelineEntries.flatMap<ConsoleConversationMessage>((entry) => {
1042
+ if (entry.kind === "tool") return [{ id: entry.id, role: "TOOL", text: "", createdAt: entry.tool.firstCreatedAt }];
1043
+ const message = entry.message;
1044
+ const content = (message.content ?? "").trim();
1045
+ if (isSupersededRunningAssistantMessage(message, content)) return [];
1046
+ const awaiting = message.pendingPlaceholder || (message.role === "ASSISTANT" && message.responseStatus === "RUNNING");
1047
+ return [{ id: message.id, role: message.role,
1048
+ text: content || (awaiting ? "" : sanitizeConsoleSummary((message.summary ?? "").trim())),
1049
+ createdAt: message.createdAt, pending: awaiting, failed: message.responseStatus === "FAILED",
1050
+ error: message.responseError, context: message.messageKind === CONSOLE_WEB_CONTEXT_MESSAGE_KIND }];
1051
+ }), [isSupersededRunningAssistantMessage, timelineEntries]);
1060
1052
 
1061
1053
  return (
1062
1054
  <div className="papyrus-console__panel">
@@ -1089,141 +1081,49 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
1089
1081
  </button>
1090
1082
  ) : null}
1091
1083
  </div>
1092
- <Conversation className="papyrus-console__conversation" resize={pending ? "instant" : "smooth"}>
1093
- <ConsoleStreamingAutoStick active={pending} streamSignal={streamingAutoStickSignal} />
1094
- <ConversationContent
1095
- aria-live="polite"
1096
- className="papyrus-console__body"
1097
- scrollClassName="papyrus-console__scrollport"
1098
- role="log"
1099
- >
1100
- {loading && !sortedMessages.length ? <p className="papyrus-console__empty">Loading conversation…</p> : null}
1101
- {timelineEntries.map((entry) => {
1102
- if (entry.kind === "tool") {
1103
- const showAssistantAvatar = entry.id === latestVisibleAvatarAnchorId;
1104
- const assistantAvatar = showAssistantAvatar
1105
- ? resolveAssistantAvatarPresentation(entry.tool, motionSetting === "low", pending)
1106
- : null;
1107
- return (
1108
- <ConsoleToolTimelineRow key={entry.id}>
1109
- <ConsoleToolMessageContent tool={entry.tool} />
1110
- {assistantAvatar ? (
1111
- <span className="papyrus-console-message__assistant-avatar" data-tone={assistantAvatar.tone}>
1112
- <VultusBotAvatar
1113
- ariaLabel={assistantAvatar.ariaLabel}
1114
- lightColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_LIGHT_COLOR : ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR}
1115
- neutralIdleMode={assistantAvatar.neutralIdleMode}
1116
- shadowColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_SHADOW_COLOR : ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR}
1117
- size={ASSISTANT_AVATAR_SIZE_PX}
1118
- state={assistantAvatar.state}
1119
- transitionDurationSeconds={0.35}
1120
- />
1121
- </span>
1122
- ) : null}
1123
- </ConsoleToolTimelineRow>
1124
- );
1125
- }
1126
-
1127
- const message = entry.message;
1128
- const isWebContextMessage = message.messageKind === CONSOLE_WEB_CONTEXT_MESSAGE_KIND;
1129
- const content = (message.content ?? "").trim();
1130
- const summary = (message.summary ?? "").trim();
1131
- const sanitizedSummary = sanitizeConsoleSummary(summary);
1132
- const isSupersededRunningAssistant = isSupersededRunningAssistantMessage(message, content);
1133
- const showAssistantAvatar = entry.id === latestVisibleAvatarAnchorId;
1134
- const assistantAvatar = showAssistantAvatar
1135
- ? resolveAssistantAvatarPresentation(message, motionSetting === "low", pending)
1136
- : null;
1137
- const userAvatarHash = message.role === "USER" ? readConsoleAuthorAvatarHash(message) : null;
1138
- const userAuthorEmail = message.role === "USER" ? readConsoleAuthorEmail(message) : null;
1139
- const shouldShowThinking = (
1140
- message.pendingPlaceholder
1141
- || (message.role === "ASSISTANT" && message.responseStatus === "RUNNING" && !content && !isSupersededRunningAssistant)
1142
- );
1143
- if (isSupersededRunningAssistant) return null;
1144
-
1145
- return (
1146
- <ConsoleChatMessageRow
1147
- key={message.id}
1148
- role={isWebContextMessage ? "CONTEXT" : message.role}
1149
- >
1150
- <div className={message.role === "ASSISTANT" ? "papyrus-console-message__markdown" : undefined}>
1151
- {message.role === "USER" ? <ConsoleUserAvatar authorEmail={userAuthorEmail} avatarHash={userAvatarHash} /> : null}
1152
- {isWebContextMessage ? (
1153
- <p className="papyrus-console-message__context-label">Page context</p>
1154
- ) : null}
1155
- {shouldShowThinking ? (
1156
- <Shimmer className="papyrus-console__thinking-shimmer">Thinking...</Shimmer>
1157
- ) : <ConsoleChatMessageContent role={message.role} text={content || sanitizedSummary} />}
1158
- </div>
1159
- {message.responseStatus === "FAILED" ? <span className="papyrus-console-message__error">{message.responseError ?? "Responder failed"}</span> : null}
1160
- {assistantAvatar ? (
1161
- <span className="papyrus-console-message__assistant-avatar" data-tone={assistantAvatar.tone}>
1162
- <VultusBotAvatar
1163
- ariaLabel={assistantAvatar.ariaLabel}
1164
- lightColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_LIGHT_COLOR : ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR}
1165
- neutralIdleMode={assistantAvatar.neutralIdleMode}
1166
- shadowColor={assistantAvatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_SHADOW_COLOR : ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR}
1167
- size={ASSISTANT_AVATAR_SIZE_PX}
1168
- state={assistantAvatar.state}
1169
- transitionDurationSeconds={0.35}
1170
- />
1171
- </span>
1172
- ) : null}
1173
- </ConsoleChatMessageRow>
1174
- );
1175
- })}
1176
- </ConversationContent>
1177
- <ConversationScrollButton
1178
- aria-label="Scroll to latest message"
1179
- className="papyrus-console__scroll-bottom"
1180
- />
1181
- </Conversation>
1182
- {!loading && !sortedMessages.length ? (
1183
- <div className="papyrus-console__suggestions papyrus-console-ui">
1184
- <Suggestions>
1185
- {CONSOLE_STARTER_SUGGESTIONS.map((suggestion) => (
1186
- <Suggestion
1187
- disabled={sending}
1188
- key={suggestion}
1189
- onClick={handleSuggestionClick}
1190
- suggestion={suggestion}
1191
- />
1192
- ))}
1193
- </Suggestions>
1194
- </div>
1195
- ) : null}
1196
- {error ? <p className="papyrus-console__error">{error}</p> : null}
1197
- <PromptInput className="papyrus-console__composer" onSubmit={(message) => void sendMessage(message)}>
1198
- <PromptInputBody>
1199
- <PromptInputTextarea
1200
- aria-label="Console message"
1201
- disabled={sending}
1202
- onChange={(event) => setDraft(event.target.value)}
1203
- placeholder="Write a message..."
1204
- rows={3}
1205
- value={draft}
1206
- />
1207
- </PromptInputBody>
1208
- <PromptInputFooter>
1209
- <PromptInputTools>
1210
- <ModelSelector
1211
- aria-label="Console model"
1212
- disabled={sending}
1213
- onValueChange={handleModelChange}
1214
- options={CONSOLE_MODEL_OPTIONS.map((option) => ({
1215
- value: option.value,
1216
- label: option.label,
1217
- }))}
1218
- value={selectedModel}
1219
- />
1220
- </PromptInputTools>
1221
- <PromptInputSubmit
1222
- disabled={!draft.trim() || sending}
1223
- status={pending ? "streaming" : "ready"}
1224
- />
1225
- </PromptInputFooter>
1226
- </PromptInput>
1084
+ <PapyrusConsoleConversation
1085
+ messages={conversationMessages}
1086
+ isSending={sending}
1087
+ isRunning={pending}
1088
+ onSendMessage={submitMessage}
1089
+ error={error}
1090
+ emptyState={loading ? <p className="papyrus-console__empty">Loading conversation…</p> : (
1091
+ <div className="papyrus-console__suggestions papyrus-console-ui">
1092
+ <Suggestions>{CONSOLE_STARTER_SUGGESTIONS.map((suggestion) => (
1093
+ <Suggestion disabled={sending} key={suggestion} onClick={handleSuggestionClick} suggestion={suggestion} />
1094
+ ))}</Suggestions>
1095
+ </div>
1096
+ )}
1097
+ composerActions={<div className="papyrus-console-ui"><ModelSelector
1098
+ aria-label="Console model" disabled={sending} onValueChange={handleModelChange}
1099
+ options={CONSOLE_MODEL_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
1100
+ value={selectedModel}
1101
+ /></div>}
1102
+ presentation={{
1103
+ header: (id) => {
1104
+ const entry = presentationEntries.get(id);
1105
+ if (entry?.kind !== "message" || entry.message.role !== "USER") return null;
1106
+ return <ConsoleUserAvatar authorEmail={readConsoleAuthorEmail(entry.message)} avatarHash={readConsoleAuthorAvatarHash(entry.message)} />;
1107
+ },
1108
+ body: (id, body) => {
1109
+ const entry = presentationEntries.get(id);
1110
+ return entry?.kind === "tool" ? <ConsoleToolMessageContent tool={entry.tool} /> : body;
1111
+ },
1112
+ footer: (id) => {
1113
+ const entry = presentationEntries.get(id);
1114
+ if (!entry || id !== latestVisibleAvatarAnchorId) return null;
1115
+ const avatar = resolveAssistantAvatarPresentation(entry.kind === "tool" ? entry.tool : entry.message, motionSetting === "low", pending);
1116
+ if (!avatar) return null;
1117
+ return <span className="papyrus-console-message__assistant-avatar" data-tone={avatar.tone}>
1118
+ <VultusBotAvatar ariaLabel={avatar.ariaLabel}
1119
+ lightColor={avatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_LIGHT_COLOR : ASSISTANT_AVATAR_DEFAULT_LIGHT_COLOR}
1120
+ shadowColor={avatar.tone === "error" ? ASSISTANT_AVATAR_ERROR_SHADOW_COLOR : ASSISTANT_AVATAR_DEFAULT_SHADOW_COLOR}
1121
+ neutralIdleMode={avatar.neutralIdleMode} size={ASSISTANT_AVATAR_SIZE_PX}
1122
+ state={avatar.state} transitionDurationSeconds={0.35} />
1123
+ </span>;
1124
+ },
1125
+ }}
1126
+ />
1227
1127
  </div>
1228
1128
  );
1229
1129
  }
@@ -1516,30 +1416,6 @@ function ConsoleToolMessageContent({ tool }: { tool: ConsoleToolTimelineEntry })
1516
1416
  );
1517
1417
  }
1518
1418
 
1519
- function ConsoleToolTimelineRow({ children }: { children: ReactNode }) {
1520
- const rowRef = useRef<HTMLElement | null>(null);
1521
-
1522
- useEffect(() => {
1523
- const row = rowRef.current;
1524
- if (!row || prefersReducedMotion()) return;
1525
- const tween = gsap.fromTo(
1526
- row,
1527
- { autoAlpha: 0, y: 8 },
1528
- { autoAlpha: 1, y: 0, duration: 0.16, ease: "power2.out", overwrite: "auto" },
1529
- );
1530
- return () => {
1531
- tween.kill();
1532
- gsap.killTweensOf(row);
1533
- };
1534
- }, []);
1535
-
1536
- return (
1537
- <article className="papyrus-console-tool-row" ref={rowRef}>
1538
- {children}
1539
- </article>
1540
- );
1541
- }
1542
-
1543
1419
  function formatToolOutputText(value: unknown): string {
1544
1420
  if (typeof value === "string") return value;
1545
1421
  if (value === undefined || value === null) return "";
@@ -1550,240 +1426,6 @@ function formatToolOutputText(value: unknown): string {
1550
1426
  }
1551
1427
  }
1552
1428
 
1553
- function ConsoleChatMessageContent({ role, text }: { role: string | null | undefined; text: string }) {
1554
- const blocks = useMemo(() => parseConsoleMarkdownBlocks(text), [text]);
1555
- if (role !== "ASSISTANT") return text;
1556
- return (
1557
- <>
1558
- {blocks.map((block, index) => renderConsoleMarkdownBlock(block, index))}
1559
- </>
1560
- );
1561
- }
1562
-
1563
- type ConsoleMarkdownBlock =
1564
- | { type: "code"; text: string }
1565
- | { type: "heading"; depth: number; text: string }
1566
- | { type: "list"; ordered: boolean; items: string[] }
1567
- | { type: "paragraph"; text: string };
1568
-
1569
- function parseConsoleMarkdownBlocks(text: string): ConsoleMarkdownBlock[] {
1570
- const blocks: ConsoleMarkdownBlock[] = [];
1571
- const lines = text.split(/\r?\n/);
1572
- let paragraph: string[] = [];
1573
- let list: string[] = [];
1574
- let listOrdered = false;
1575
- let code: string[] = [];
1576
- let inCode = false;
1577
-
1578
- const flushParagraph = () => {
1579
- if (!paragraph.length) return;
1580
- blocks.push({ type: "paragraph", text: paragraph.join(" ").trim() });
1581
- paragraph = [];
1582
- };
1583
- const flushList = () => {
1584
- if (!list.length) return;
1585
- blocks.push({ type: "list", ordered: listOrdered, items: list });
1586
- list = [];
1587
- };
1588
-
1589
- for (const line of lines) {
1590
- if (line.trim().startsWith("```")) {
1591
- if (inCode) {
1592
- blocks.push({ type: "code", text: code.join("\n") });
1593
- code = [];
1594
- inCode = false;
1595
- } else {
1596
- flushParagraph();
1597
- flushList();
1598
- inCode = true;
1599
- }
1600
- continue;
1601
- }
1602
- if (inCode) {
1603
- code.push(line);
1604
- continue;
1605
- }
1606
- const trimmed = line.trim();
1607
- if (!trimmed) {
1608
- flushParagraph();
1609
- flushList();
1610
- continue;
1611
- }
1612
- const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed);
1613
- if (heading) {
1614
- flushParagraph();
1615
- flushList();
1616
- blocks.push({ type: "heading", depth: heading[1].length, text: heading[2].trim() });
1617
- continue;
1618
- }
1619
- const bullet = /^([-*+])\s+(.+)$/.exec(trimmed);
1620
- if (bullet) {
1621
- flushParagraph();
1622
- if (!list.length) listOrdered = false;
1623
- list.push(bullet[2].trim());
1624
- continue;
1625
- }
1626
- const ordered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
1627
- if (ordered) {
1628
- flushParagraph();
1629
- if (!list.length) listOrdered = true;
1630
- list.push(ordered[1].trim());
1631
- continue;
1632
- }
1633
- flushList();
1634
- paragraph.push(trimmed);
1635
- }
1636
-
1637
- flushParagraph();
1638
- flushList();
1639
- if (code.length) blocks.push({ type: "code", text: code.join("\n") });
1640
- return blocks;
1641
- }
1642
-
1643
- function renderConsoleMarkdownBlock(block: ConsoleMarkdownBlock, index: number): ReactNode {
1644
- const key = `${block.type}-${index}`;
1645
- if (block.type === "code") return <pre className="papyrus-console-message__code" key={key}>{block.text}</pre>;
1646
- if (block.type === "list") {
1647
- if (block.ordered) {
1648
- return (
1649
- <ol key={key}>
1650
- {block.items.map((item, itemIndex) => <li key={`${key}-${itemIndex}`}>{renderConsoleInlineMarkdown(item)}</li>)}
1651
- </ol>
1652
- );
1653
- }
1654
- return (
1655
- <ul key={key}>
1656
- {block.items.map((item, itemIndex) => <li key={`${key}-${itemIndex}`}>{renderConsoleInlineMarkdown(item)}</li>)}
1657
- </ul>
1658
- );
1659
- }
1660
- if (block.type === "heading") {
1661
- if (block.depth <= 1) return <h3 key={key}>{renderConsoleInlineMarkdown(block.text)}</h3>;
1662
- if (block.depth === 2) return <h4 key={key}>{renderConsoleInlineMarkdown(block.text)}</h4>;
1663
- return <h5 key={key}>{renderConsoleInlineMarkdown(block.text)}</h5>;
1664
- }
1665
- return <p key={key}>{renderConsoleInlineMarkdown(block.text)}</p>;
1666
- }
1667
-
1668
- function renderConsoleInlineMarkdown(text: string): ReactNode[] {
1669
- const nodes: ReactNode[] = [];
1670
- const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\))/g;
1671
- let lastIndex = 0;
1672
- let match: RegExpExecArray | null;
1673
- while ((match = pattern.exec(text)) !== null) {
1674
- if (match.index > lastIndex) nodes.push(text.slice(lastIndex, match.index));
1675
- const token = match[0];
1676
- if (token.startsWith("`")) {
1677
- nodes.push(<code key={`${match.index}-code`}>{token.slice(1, -1)}</code>);
1678
- } else if (token.startsWith("**")) {
1679
- nodes.push(<strong key={`${match.index}-strong`}>{token.slice(2, -2)}</strong>);
1680
- } else {
1681
- const linkMatch = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
1682
- if (linkMatch) {
1683
- nodes.push(
1684
- <a href={linkMatch[2]} key={`${match.index}-link`} rel="noreferrer noopener" target="_blank">
1685
- {linkMatch[1]}
1686
- </a>,
1687
- );
1688
- } else {
1689
- nodes.push(token);
1690
- }
1691
- }
1692
- lastIndex = match.index + token.length;
1693
- }
1694
- if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
1695
- return nodes;
1696
- }
1697
-
1698
- function ConsoleChatMessageRow({
1699
- role,
1700
- children,
1701
- }: {
1702
- role: string | null | undefined;
1703
- children: ReactNode;
1704
- }) {
1705
- const rowRef = useRef<HTMLElement | null>(null);
1706
-
1707
- useEffect(() => {
1708
- const row = rowRef.current;
1709
- if (!row || prefersReducedMotion()) return;
1710
- const tween = gsap.fromTo(
1711
- row,
1712
- { autoAlpha: 0, y: 8 },
1713
- { autoAlpha: 1, y: 0, duration: 0.16, ease: "power2.out", overwrite: "auto" },
1714
- );
1715
- return () => {
1716
- tween.kill();
1717
- gsap.killTweensOf(row);
1718
- };
1719
- }, []);
1720
-
1721
- return (
1722
- <article
1723
- className={
1724
- role === "USER"
1725
- ? "papyrus-console-message papyrus-console-message--user"
1726
- : role === "CONTEXT"
1727
- ? "papyrus-console-message papyrus-console-message--context"
1728
- : "papyrus-console-message papyrus-console-message--assistant"
1729
- }
1730
- ref={rowRef}
1731
- >
1732
- {children}
1733
- </article>
1734
- );
1735
- }
1736
-
1737
- function ConsoleStreamingAutoStick({ active, streamSignal }: { active: boolean; streamSignal: string | null }) {
1738
- const { scrollRef, scrollToBottom } = useStickToBottomContext();
1739
- const previousSignalRef = useRef<string | null>(null);
1740
-
1741
- const snapToBottom = useCallback(() => {
1742
- void scrollToBottom({ animation: "instant" });
1743
- const scrollElement = scrollRef.current;
1744
- if (!scrollElement) return;
1745
- scrollElement.scrollTop = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight);
1746
- }, [scrollRef, scrollToBottom]);
1747
-
1748
- useEffect(() => {
1749
- if (!active || !streamSignal) return;
1750
- if (previousSignalRef.current === streamSignal) return;
1751
- previousSignalRef.current = streamSignal;
1752
-
1753
- snapToBottom();
1754
- const frame = window.requestAnimationFrame(() => {
1755
- snapToBottom();
1756
- });
1757
- return () => {
1758
- window.cancelAnimationFrame(frame);
1759
- };
1760
- }, [active, snapToBottom, streamSignal]);
1761
-
1762
- useEffect(() => {
1763
- if (!active) return;
1764
- let frame = 0;
1765
- const pin = () => {
1766
- snapToBottom();
1767
- frame = window.requestAnimationFrame(pin);
1768
- };
1769
- pin();
1770
- return () => {
1771
- window.cancelAnimationFrame(frame);
1772
- };
1773
- }, [active, snapToBottom]);
1774
-
1775
- useEffect(() => {
1776
- if (!active) previousSignalRef.current = null;
1777
- }, [active]);
1778
-
1779
- return null;
1780
- }
1781
-
1782
- function prefersReducedMotion(): boolean {
1783
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
1784
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1785
- }
1786
-
1787
1429
  function formatConsoleThreadOption(thread: ConsoleChatThread): string {
1788
1430
  const stamp = thread.lastMessageAt ?? thread.updatedAt ?? thread.createdAt;
1789
1431
  const date = stamp ? new Date(stamp) : null;