@jskit-ai/assistant-core 0.2.6 → 0.2.8

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +18 -12
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -318,30 +318,21 @@
318
318
  <script setup>
319
319
  import AssistantMessageAttachments from "./AssistantMessageAttachments.vue";
320
320
  import AssistantConversationStatus from "./AssistantConversationStatus.vue";
321
- import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
321
+ import { computed, ref, watch } from "vue";
322
322
  import {
323
323
  mdiAccountOutline,
324
324
  mdiInformationOutline,
325
325
  mdiRefresh,
326
326
  mdiRobotOutline
327
327
  } from "@mdi/js";
328
- import { useScrollToBottom } from "./useScrollToBottom.js";
329
328
  import LongTextInlineParts from "./LongTextInlineParts.vue";
330
329
  import LongTextPreviewBlocks from "./LongTextPreviewBlocks.vue";
331
330
  import AssistantProgress from "./AssistantProgress.vue";
332
- import { parseNumberedQuestionPrompt } from "../../shared/conversation/numberedQuestionSugar.js";
333
- import { parseLongTextReviewBlocks } from "../../shared/conversation/longTextBlocks.js";
334
- import {
335
- scrollDistanceFromBottom,
336
- scrollElementNearBottom
337
- } from "../../shared/conversation/scrollFollowState.js";
338
- import {
339
- normalizeThinkingMessageText
340
- } from "../../shared/conversation/thinkingText.js";
331
+ import { useTranscriptEntries, userMessagePreviewText } from "./transcriptEntries.js";
332
+ import { useTranscriptScrollFollow } from "./useTranscriptScrollFollow.js";
341
333
  import { useJskitText } from "../i18n.js";
342
334
 
343
- const jskitText = useJskitText();
344
- const { formatDateTime, serverMessage, t } = jskitText;
335
+ const { t } = useJskitText();
345
336
 
346
337
  function previewForTurn(turn) {
347
338
  const preview = turn.previewMessage || props.previewMessage;
@@ -427,234 +418,11 @@ const systemName = computed(() => props.systemLabel ?? t("jskit.assistant.transc
427
418
  const assistantName = computed(() => props.assistantLabel ?? t("jskit.assistant.transcript.assistant"));
428
419
  const reloadLabel = computed(() => props.reloading ? t("jskit.assistant.transcript.reloading") : t("jskit.assistant.transcript.reload"));
429
420
 
430
- const USER_MESSAGE_COLLAPSE_MIN_CHARACTERS = 360;
431
- const USER_MESSAGE_PREVIEW_MAX_CHARACTERS = 280;
432
- const USER_MESSAGE_PREVIEW_MAX_LINES = 4;
433
- const DISPLAY_MESSAGE_CACHE_LIMIT = 500;
434
421
  const bodyElement = ref(null);
435
422
  const bottomElement = ref(null);
436
423
  const expandedUserMessages = ref(new Set());
437
424
  const isWorking = computed(() => props.working ?? props.turns.some(turn => turn.pending));
438
- const followingLatest = ref(true);
439
- const initialScrollSettled = ref(false);
440
- const userScrollIntent = ref(false);
441
- const displayMessageCache = new Map();
442
- let liveScrollFrame = 0;
443
- let userScrollIntentTimer = null;
444
- let initialScrollVersion = 0;
445
- let loadMoreScrollSnapshot = null;
446
- let retainedReaderScrollSnapshot = null;
447
- let loadMoreRequestVersion = 0;
448
- let pendingTailFollow = false;
449
- let previousScrollTop = 0;
450
- let previousScrollHeight = 0;
451
- let userScrollIntentUpward = false;
452
- let lastTouchY = null;
453
- const USER_SCROLL_INTENT_RESET_MS = 600;
454
- const LOAD_MORE_THRESHOLD_PX = 160;
455
- const KEYBOARD_SCROLL_KEYS = new Set([
456
- " ",
457
- "ArrowDown",
458
- "ArrowUp",
459
- "End",
460
- "Home",
461
- "PageDown",
462
- "PageUp",
463
- "Spacebar"
464
- ]);
465
- const UPWARD_SCROLL_KEYS = new Set(["ArrowUp", "Home", "PageUp"]);
466
- const TIME_FORMAT = Object.freeze({
467
- year: "numeric",
468
- month: "short",
469
- day: "2-digit",
470
- hour: "2-digit",
471
- minute: "2-digit"
472
- });
473
-
474
- function displayTime(value = "") {
475
- const date = new Date(String(value || ""));
476
- if (Number.isNaN(date.getTime())) {
477
- return "";
478
- }
479
- return formatDateTime(date, TIME_FORMAT);
480
- }
481
-
482
- function userMessagePreviewText(value = "") {
483
- const text = String(value || "");
484
- const characters = [...text];
485
- const lines = text.split(/\r\n?|\n/u);
486
- if (
487
- characters.length <= USER_MESSAGE_COLLAPSE_MIN_CHARACTERS &&
488
- lines.length <= USER_MESSAGE_PREVIEW_MAX_LINES
489
- ) {
490
- return "";
491
- }
492
- const firstLines = lines.slice(0, USER_MESSAGE_PREVIEW_MAX_LINES).join("\n");
493
- const preview = [...firstLines]
494
- .slice(0, USER_MESSAGE_PREVIEW_MAX_CHARACTERS)
495
- .join("")
496
- .trimEnd();
497
- return preview ? `${preview}…` : "";
498
- }
499
-
500
- function displayMessage(message = null, {
501
- allowNumberedQuestions = false,
502
- preserveParagraphLineBreaks = false,
503
- previewUserMessage = false
504
- } = {}, cacheKey = "") {
505
- if (!message) {
506
- return null;
507
- }
508
- const normalizedCacheKey = String(cacheKey || "").trim();
509
- const cached = normalizedCacheKey ? displayMessageCache.get(normalizedCacheKey) : null;
510
- // Times and status texts follow the active locale, so a locale change re-renders them.
511
- const activeLocale = jskitText.locale;
512
- if (
513
- cached &&
514
- cached.locale === activeLocale &&
515
- cached.allowNumberedQuestions === allowNumberedQuestions &&
516
- cached.attachments === message.attachments &&
517
- cached.at === message.at &&
518
- cached.messageId === message.messageId &&
519
- cached.preserveParagraphLineBreaks === preserveParagraphLineBreaks &&
520
- cached.previewUserMessage === previewUserMessage &&
521
- cached.role === message.role &&
522
- cached.text === message.text
523
- ) {
524
- return { ...message, ...cached.value };
525
- }
526
- const messageText = message.textCode ? serverMessage({ message: message.text, messageCode: message.textCode }) : message.text;
527
- const questionInput = allowNumberedQuestions
528
- ? parseNumberedQuestionPrompt(messageText)
529
- : {
530
- intro: "",
531
- outro: "",
532
- questions: []
533
- };
534
- const hasQuestions = questionInput.questions.length > 0;
535
- const previewText = previewUserMessage ? userMessagePreviewText(message.text) : "";
536
- const value = {
537
- blocks: parseLongTextReviewBlocks(hasQuestions ? questionInput.intro : messageText, {
538
- preserveParagraphLineBreaks
539
- }),
540
- outroBlocks: parseLongTextReviewBlocks(hasQuestions ? questionInput.outro : "", {
541
- preserveParagraphLineBreaks
542
- }),
543
- previewBlocks: previewText
544
- ? parseLongTextReviewBlocks(previewText, { preserveParagraphLineBreaks: true })
545
- : null,
546
- questions: hasQuestions ? questionInput.questions : [],
547
- displayAt: displayTime(message.at)
548
- };
549
- if (normalizedCacheKey) {
550
- if (
551
- displayMessageCache.size >= DISPLAY_MESSAGE_CACHE_LIMIT &&
552
- !displayMessageCache.has(normalizedCacheKey)
553
- ) {
554
- displayMessageCache.delete(displayMessageCache.keys().next().value);
555
- }
556
- displayMessageCache.set(normalizedCacheKey, {
557
- allowNumberedQuestions,
558
- attachments: message.attachments,
559
- at: message.at,
560
- locale: activeLocale,
561
- messageId: message.messageId,
562
- preserveParagraphLineBreaks,
563
- previewUserMessage,
564
- role: message.role,
565
- text: message.text,
566
- value
567
- });
568
- }
569
- return { ...message, ...value };
570
- }
571
-
572
- function displayThinkingMessage(message = null) {
573
- if (!message) {
574
- return null;
575
- }
576
- const text = normalizeThinkingMessageText(message.text);
577
- if (!text) {
578
- return null;
579
- }
580
- return {
581
- ...message,
582
- text,
583
- displayAt: displayTime(message.at)
584
- };
585
- }
586
-
587
- function conversationAgentMessages(turn = {}) {
588
- if (Array.isArray(turn.messages)) {
589
- return turn.messages.filter((message) => (
590
- ["assistant", "commentary", "thinking"].includes(String(message?.role || "").trim())
591
- ));
592
- }
593
- return [
594
- ...(Array.isArray(turn.thinking) ? turn.thinking : []),
595
- ...(Array.isArray(turn.commentary) ? turn.commentary : []),
596
- turn.assistant
597
- ].filter(Boolean);
598
- }
599
-
600
- function conversationMessageKey(message = {}, index = 0) {
601
- return [
602
- String(message.messageId || "").trim(),
603
- String(message.role || "").trim(),
604
- String(message.at || "").trim(),
605
- index
606
- ].join(":");
607
- }
608
-
609
- // Storage rows are not visual boundaries. Only an intervening message ends a
610
- // reasoning group, including when history is prepended or execution continues.
611
- const displayEntries = computed(() => {
612
- const candidates = [];
613
- for (const [index, turn] of props.turns.entries()) {
614
- const turnId = String(turn.turnId || index + 1);
615
- // A delivery notice belongs to its exact user message, not the previous reply.
616
- const userDelivery = turn.user && turn.system?.delivery && turn.system.delivery.messageId === turn.user.messageId
617
- ? turn.system : null;
618
- const messages = [
619
- turn.system && !userDelivery && { ...turn.system, role: "system" },
620
- turn.user && { ...turn.user, role: "user" },
621
- ...conversationAgentMessages(turn)
622
- ].filter(Boolean);
623
- for (const [messageIndex, message] of messages.entries()) {
624
- const role = String(message.role || "").trim();
625
- const key = `${turnId}:${conversationMessageKey(message, messageIndex)}`;
626
- const displayed = role === "thinking" ? displayThinkingMessage(message) : displayMessage(message, {
627
- allowNumberedQuestions: role !== "user" && role !== "system",
628
- preserveParagraphLineBreaks: role === "user",
629
- previewUserMessage: role === "user"
630
- }, key);
631
- if (!displayed) continue;
632
- candidates.push({ key, role, turn: { ...turn, turnId }, message: displayed,
633
- sortAt: turn.optimistic && turn.optimistic.status !== "accepted" && turn.optimistic.id === message.messageId
634
- ? NaN : Date.parse(message.at),
635
- delivery: role === "user" ? userDelivery : null });
636
- }
637
- }
638
- // Native streaming turns and saved authored turns can arrive independently.
639
- // Local queued timestamps can precede an earlier message's native admission.
640
- // Keep unconfirmed messages in their supplied positions, along with undated
641
- // notices. Accepted local copies join chronology even before history arrives.
642
- const dated = candidates.filter(entry => Number.isFinite(entry.sortAt))
643
- .sort((a, b) => a.sortAt - b.sortAt)[Symbol.iterator]();
644
- const entries = [];
645
- for (const candidate of candidates) {
646
- const entry = Number.isFinite(candidate.sortAt) ? dated.next().value : candidate;
647
- const previous = entries.at(-1);
648
- if (entry.role === "thinking" && previous?.role === "thinking") previous.messages.push({ ...entry.message, key: entry.key });
649
- else entries.push({ ...entry, ...(entry.role === "thinking" ? { messages: [{ ...entry.message, key: entry.key }] } : {}) });
650
- }
651
- // Anchor a progress group to the following message, or to the transcript end.
652
- // Appending progress or prepending history then preserves its expansion state.
653
- for (const [index, entry] of entries.entries()) {
654
- if (entry.role === "thinking") entry.key = `progress:${entries[index + 1]?.key || "tail"}`;
655
- }
656
- return entries;
657
- });
425
+ const { clearDisplayCache, displayEntries } = useTranscriptEntries(props);
658
426
 
659
427
  function handleLongTextLinkClick(payload) {
660
428
  emit("link-click", payload);
@@ -692,971 +460,30 @@ function toggleUserMessage(turn = {}) {
692
460
  toggleExpandedKey(expandedUserMessages, key);
693
461
  }
694
462
 
695
- const loadingIndicatorVisible = computed(() => Boolean(
696
- props.loading &&
697
- !displayEntries.value.length
698
- ));
699
- const initialScrollPending = computed(() => Boolean(
700
- props.visible &&
701
- displayEntries.value.length &&
702
- !initialScrollSettled.value
703
- ));
704
-
705
- function messageScrollKey(message = null) {
706
- if (!message) {
707
- return "empty";
708
- }
709
- return [
710
- message.messageId || "",
711
- message.role || "",
712
- message.at || "",
713
- String(message.text || "")
714
- ].join("/");
715
- }
716
-
717
- const timelineScrollTrigger = computed(() => [
718
- loadingIndicatorVisible.value ? "loading" : "ready",
719
- displayEntries.value.length ? "has-messages" : "empty",
720
- props.scrollKey
721
- ].join(":"));
722
- const latestRenderedTailKey = computed(() => {
723
- const entry = displayEntries.value.at(-1);
724
- if (!entry) return "";
725
- return [
726
- entry.key,
727
- isWorking.value,
728
- entry.role === "thinking" ? entry.messages.map(messageScrollKey).join("+") : messageScrollKey(entry.message),
729
- entry.turn.optimistic?.status || "",
730
- entry.turn.optimistic?.error || ""
731
- ].join(":");
732
- });
733
- const autoScrollEnabled = computed(() => Boolean(
734
- props.visible &&
735
- followingLatest.value
736
- ));
737
-
738
- const {
739
- clearScheduledScrolls,
740
- scrollAfterLayout: scrollToLatestMessage,
741
- scrollNow: scrollToLatestMessageNow
742
- } = useScrollToBottom({
743
- anchor: bottomElement,
744
- enabled: autoScrollEnabled,
745
- scrollAnchorIntoView: false,
746
- target: bodyElement
747
- });
748
-
749
- function clearUserScrollIntent() {
750
- if (userScrollIntentTimer && typeof window !== "undefined" && typeof window.clearTimeout === "function") {
751
- window.clearTimeout(userScrollIntentTimer);
752
- }
753
- userScrollIntentTimer = null;
754
- userScrollIntent.value = false;
755
- }
756
-
757
- function resumePendingTailFollow() {
758
- const shouldResume = pendingTailFollow && followingLatest.value;
759
- pendingTailFollow = false;
760
- if (shouldResume) {
761
- queueLiveBottomScroll();
762
- }
763
- }
764
-
765
- function markUserScrollIntent({ upward = false } = {}) {
766
- retainedReaderScrollSnapshot = null;
767
- userScrollIntent.value = true;
768
- userScrollIntentUpward = upward;
769
- clearLiveBottomScroll();
770
- clearScheduledScrolls();
771
- if (userScrollIntentTimer && typeof window !== "undefined" && typeof window.clearTimeout === "function") {
772
- window.clearTimeout(userScrollIntentTimer);
773
- }
774
- if (typeof window === "undefined" || typeof window.setTimeout !== "function") {
775
- return;
776
- }
777
- userScrollIntentTimer = window.setTimeout(() => {
778
- userScrollIntentTimer = null;
779
- userScrollIntent.value = false;
780
- resumePendingTailFollow();
781
- }, USER_SCROLL_INTENT_RESET_MS);
782
- }
783
-
784
- function markKeyboardScrollIntent(event = {}) {
785
- if (!KEYBOARD_SCROLL_KEYS.has(String(event.key || "")) || event.defaultPrevented) {
786
- return;
787
- }
788
- const target = event.target;
789
- const tagName = String(target?.tagName || "").toLowerCase();
790
- if (
791
- target?.isContentEditable ||
792
- ["input", "select", "textarea"].includes(tagName) ||
793
- ([" ", "Spacebar"].includes(event.key) && ["a", "button"].includes(tagName))
794
- ) {
795
- return;
796
- }
797
- markUserScrollIntent({
798
- upward: UPWARD_SCROLL_KEYS.has(event.key) || (event.shiftKey && [" ", "Spacebar"].includes(event.key))
799
- });
800
- }
801
-
802
- function rememberTouch(event = {}) {
803
- lastTouchY = event.touches?.[0]?.clientY ?? null;
804
- }
805
-
806
- function markTouchScrollIntent(event = {}) {
807
- const touchY = event.touches?.[0]?.clientY ?? null;
808
- // A finger moving down the screen scrolls the transcript up.
809
- markUserScrollIntent({ upward: touchY !== null && lastTouchY !== null && touchY > lastTouchY });
810
- lastTouchY = touchY;
811
- }
812
-
813
- function scrollToLatestMessageAfterLayout({
814
- behavior = "auto",
815
- force = false
816
- } = {}) {
817
- if (force) {
818
- retainedReaderScrollSnapshot = null;
819
- followingLatest.value = true;
820
- clearUserScrollIntent();
821
- }
822
- return scrollToLatestMessage({
823
- behavior
824
- });
825
- }
826
-
827
- function queueInitialBottomScroll() {
828
- const version = initialScrollVersion + 1;
829
- initialScrollVersion = version;
830
- initialScrollSettled.value = false;
831
- if (props.retainWhenHidden && !props.visible) {
832
- return;
833
- }
834
- void scrollToLatestMessageAfterLayout({
835
- behavior: "auto",
836
- force: true
837
- }).finally(() => {
838
- if (initialScrollVersion === version) {
839
- initialScrollSettled.value = true;
840
- }
841
- });
842
- }
843
-
844
- function queueLiveBottomScroll({
845
- force = false
846
- } = {}) {
847
- if (userScrollIntent.value && !force) {
848
- pendingTailFollow = true;
849
- return;
850
- }
851
- if (force) {
852
- retainedReaderScrollSnapshot = null;
853
- pendingTailFollow = false;
854
- followingLatest.value = true;
855
- clearUserScrollIntent();
856
- }
857
- if (props.retainWhenHidden && !props.visible) {
858
- return;
859
- }
860
- if (liveScrollFrame) {
861
- return;
862
- }
863
- if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
864
- void scrollToLatestMessageAfterLayout({ behavior: "auto" });
865
- return;
866
- }
867
- liveScrollFrame = window.requestAnimationFrame(() => {
868
- liveScrollFrame = 0;
869
- scrollToLatestMessageNow({ behavior: "auto" });
870
- });
871
- }
872
-
873
- function clearLiveBottomScroll() {
874
- if (
875
- liveScrollFrame &&
876
- typeof window !== "undefined" &&
877
- typeof window.cancelAnimationFrame === "function"
878
- ) {
879
- window.cancelAnimationFrame(liveScrollFrame);
880
- }
881
- liveScrollFrame = 0;
882
- }
883
-
884
- function updateLatestFollowFromScroll(event = {}) {
885
- if (props.retainWhenHidden && !props.visible) {
886
- return;
887
- }
888
- const target = event?.currentTarget || bodyElement.value;
889
- if (retainedReaderScrollSnapshot?.restoring) {
890
- previousScrollTop = target?.scrollTop || 0;
891
- previousScrollHeight = target?.scrollHeight || 0;
892
- return;
893
- }
894
- const scrollTop = target?.scrollTop || 0;
895
- const scrollHeight = target?.scrollHeight || 0;
896
- const scrolledUp = scrollTop < previousScrollTop;
897
- // Layout changes such as scroll anchoring move scrollTop together with the
898
- // content height; moving up over unchanged content is the reader's own.
899
- const readerMovedUp = scrolledUp && scrollHeight === previousScrollHeight;
900
- previousScrollTop = scrollTop;
901
- previousScrollHeight = scrollHeight;
902
- // Keep following the reader while the request is pending, before rows prepend.
903
- if (loadMoreScrollSnapshot && !loadMoreScrollSnapshot.completed) {
904
- loadMoreScrollSnapshot.scrollTop = scrollTop;
905
- loadMoreScrollSnapshot.anchor = visibleHistoryAnchor(target);
906
- loadMoreScrollSnapshot.scrollHeight = target.scrollHeight;
907
- }
908
- // The reader's own upward scrolling leaves the latest message from its first
909
- // frame, even within the follow threshold.
910
- const userScrollsUp = userScrollIntent.value && userScrollIntentUpward;
911
- const shouldFollow = scrollElementNearBottom(target) &&
912
- !(userScrollsUp && scrollDistanceFromBottom(target) > 1);
913
- if (!shouldFollow && !userScrollIntent.value && !readerMovedUp && followingLatest.value) {
914
- return;
915
- }
916
- followingLatest.value = shouldFollow;
917
- if (shouldFollow) {
918
- // A scroll event still at the end, queued before upward input, must not
919
- // end that input's intent.
920
- if (!userScrollsUp) {
921
- clearUserScrollIntent();
922
- }
923
- resumePendingTailFollow();
924
- return;
925
- }
926
- clearLiveBottomScroll();
927
- clearScheduledScrolls();
928
- if (
929
- scrolledUp &&
930
- initialScrollSettled.value &&
931
- scrollTop <= LOAD_MORE_THRESHOLD_PX &&
932
- !props.loadMoreError
933
- ) {
934
- requestLoadMore();
935
- }
936
- }
937
-
938
- function visibleHistoryAnchor(element) {
939
- const top = element.getBoundingClientRect().top;
940
- for (const turn of element.querySelectorAll(".assistant-transcript__turn")) {
941
- const bounds = turn.getBoundingClientRect();
942
- if (bounds.bottom > top) {
943
- return { element: turn, offset: bounds.top - top };
944
- }
945
- }
946
- return null;
947
- }
948
-
949
- // Offscreen turns use estimated heights. Preserve the visible anchor row instead
950
- // of moving by the total height difference, which can change during layout.
951
- function restoreAnchoredScroll(element, snapshot) {
952
- const anchor = snapshot.anchor;
953
- if (anchor && element.contains(anchor.element)) {
954
- // Materialize the anchor row before reading the browser's adjusted scrollTop.
955
- const offset = anchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top;
956
- previousScrollTop = element.scrollTop + offset - anchor.offset;
957
- } else {
958
- previousScrollTop = snapshot.scrollTop + (element.scrollHeight - snapshot.scrollHeight);
959
- }
960
- element.scrollTop = previousScrollTop;
961
- }
962
-
963
- // Reopening materializes previously hidden content-visibility estimates.
964
- async function waitForTwoFrames() {
965
- if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
966
- await new Promise(resolve => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
967
- }
968
- }
969
-
970
- async function restoreRetainedReaderScroll() {
971
- const snapshot = retainedReaderScrollSnapshot;
972
- snapshot.restoring = true;
973
- await nextTick();
974
- for (let pass = 0; pass < 2; pass += 1) {
975
- const element = bodyElement.value;
976
- if (
977
- retainedReaderScrollSnapshot !== snapshot ||
978
- element !== snapshot.body ||
979
- snapshot.scrollKey !== props.scrollKey ||
980
- !props.retainWhenHidden ||
981
- !props.visible ||
982
- followingLatest.value ||
983
- loadMoreScrollSnapshot
984
- ) {
985
- if (retainedReaderScrollSnapshot === snapshot) retainedReaderScrollSnapshot = null;
986
- return;
987
- }
988
- restoreAnchoredScroll(element, snapshot);
989
- if (pass === 0) await waitForTwoFrames();
990
- }
991
- if (retainedReaderScrollSnapshot === snapshot) retainedReaderScrollSnapshot = null;
992
- }
993
-
994
- function requestLoadMore() {
995
- if ((props.retainWhenHidden && !props.visible) || !props.hasMoreBefore || props.loadingMore || loadMoreScrollSnapshot) {
996
- return;
997
- }
998
- followingLatest.value = false;
999
- pendingTailFollow = false;
1000
- markUserScrollIntent();
1001
- const element = bodyElement.value;
1002
- const version = loadMoreRequestVersion + 1;
1003
- loadMoreRequestVersion = version;
1004
- loadMoreScrollSnapshot = element
1005
- ? {
1006
- anchor: visibleHistoryAnchor(element),
1007
- scrollHeight: element.scrollHeight,
1008
- scrollKey: props.scrollKey,
1009
- scrollTop: element.scrollTop,
1010
- version
1011
- }
1012
- : null;
1013
- emit("load-more", {
1014
- complete: ({ changed = false } = {}) => {
1015
- void completeLoadMoreRequest(version, changed);
1016
- }
1017
- });
1018
- }
1019
-
1020
- function clearLoadMoreScrollSnapshot() {
1021
- loadMoreRequestVersion += 1;
1022
- loadMoreScrollSnapshot = null;
1023
- }
1024
-
1025
- async function completeLoadMoreRequest(version, changed) {
1026
- const snapshot = loadMoreScrollSnapshot;
1027
- if (!snapshot || snapshot.version !== version) {
1028
- return;
1029
- }
1030
- if (!changed || snapshot.scrollKey !== props.scrollKey) {
1031
- clearLoadMoreScrollSnapshot();
1032
- return;
1033
- }
1034
- snapshot.completed = true;
1035
- await nextTick();
1036
- if (loadMoreScrollSnapshot?.version !== version) {
1037
- return;
1038
- }
1039
- const passes = props.retainWhenHidden ? 2 : 1;
1040
- for (let pass = 0; pass < passes; pass += 1) {
1041
- if (
1042
- (props.retainWhenHidden && !props.visible) ||
1043
- loadMoreScrollSnapshot?.version !== version ||
1044
- snapshot.scrollKey !== props.scrollKey
1045
- ) {
1046
- return;
1047
- }
1048
- const element = bodyElement.value;
1049
- if (element) restoreAnchoredScroll(element, snapshot);
1050
- if (pass + 1 < passes) await waitForTwoFrames();
1051
- }
1052
- clearLoadMoreScrollSnapshot();
1053
- }
1054
-
1055
- onBeforeUnmount(() => {
1056
- retainedReaderScrollSnapshot = null;
1057
- clearLiveBottomScroll();
1058
- clearUserScrollIntent();
1059
- clearLoadMoreScrollSnapshot();
1060
- });
1061
-
463
+ // Created before the scroll-following watchers, so a new conversation's expansion and cache reset run first.
1062
464
  watch(() => props.scrollKey, () => {
1063
465
  expandedUserMessages.value = new Set();
1064
- displayMessageCache.clear();
466
+ clearDisplayCache();
1065
467
  });
1066
468
 
1067
- watch(() => [
1068
- timelineScrollTrigger.value,
1069
- latestRenderedTailKey.value
1070
- ], ([timelineKey, value], [previousTimelineKey, previous] = []) => {
1071
- if (timelineKey !== previousTimelineKey) {
1072
- return;
1073
- }
1074
- if (!value || value === previous) {
1075
- return;
1076
- }
1077
- queueLiveBottomScroll({
1078
- force: false
1079
- });
1080
- }, {
1081
- flush: "post"
1082
- });
1083
-
1084
- watch(() => [
1085
- props.scrollKey,
1086
- props.followLatestKey
1087
- ], ([scrollKey, value], [previousScrollKey, previous] = []) => {
1088
- if (scrollKey !== previousScrollKey || value === previous) {
1089
- return;
1090
- }
1091
- queueLiveBottomScroll({
1092
- force: true
1093
- });
1094
- }, {
1095
- flush: "post"
1096
- });
1097
-
1098
- watch(timelineScrollTrigger, () => {
1099
- retainedReaderScrollSnapshot = null;
1100
- clearLoadMoreScrollSnapshot();
1101
- queueInitialBottomScroll();
1102
- }, {
1103
- flush: "post",
1104
- immediate: true
469
+ const {
470
+ initialScrollPending,
471
+ loadingIndicatorVisible,
472
+ markKeyboardScrollIntent,
473
+ markTouchScrollIntent,
474
+ markUserScrollIntent,
475
+ rememberTouch,
476
+ requestLoadMore,
477
+ updateLatestFollowFromScroll
478
+ } = useTranscriptScrollFollow({
479
+ props,
480
+ displayEntries,
481
+ isWorking,
482
+ bodyElement,
483
+ bottomElement,
484
+ loadMore: (request) => emit("load-more", request)
1105
485
  });
1106
486
 
1107
- watch(() => props.error, () => {
1108
- if (followingLatest.value) queueLiveBottomScroll();
1109
- }, { flush: "post" });
1110
-
1111
- // Capture before this transcript or its parent's v-show removes its geometry.
1112
- watch(() => props.visible, (visible) => {
1113
- if (visible) return;
1114
- retainedReaderScrollSnapshot = null;
1115
- const element = bodyElement.value;
1116
- if (
1117
- props.retainWhenHidden &&
1118
- initialScrollSettled.value &&
1119
- !followingLatest.value &&
1120
- !loadMoreScrollSnapshot &&
1121
- element?.clientHeight > 0
1122
- ) {
1123
- retainedReaderScrollSnapshot = {
1124
- anchor: visibleHistoryAnchor(element),
1125
- body: element,
1126
- scrollHeight: element.scrollHeight,
1127
- scrollKey: props.scrollKey,
1128
- scrollTop: element.scrollTop
1129
- };
1130
- }
1131
- }, { flush: "sync" });
1132
-
1133
- watch(() => props.visible, (visible) => {
1134
- if (props.retainWhenHidden) {
1135
- if (!visible) {
1136
- clearLiveBottomScroll();
1137
- clearScheduledScrolls();
1138
- clearUserScrollIntent();
1139
- initialScrollVersion += 1;
1140
- } else if (loadMoreScrollSnapshot?.completed) {
1141
- void completeLoadMoreRequest(loadMoreScrollSnapshot.version, true);
1142
- } else if (!initialScrollSettled.value) {
1143
- queueInitialBottomScroll();
1144
- } else if (retainedReaderScrollSnapshot) {
1145
- void restoreRetainedReaderScroll();
1146
- } else {
1147
- queueLiveBottomScroll();
1148
- }
1149
- return;
1150
- }
1151
- clearLoadMoreScrollSnapshot();
1152
- if (visible) queueLiveBottomScroll({ force: true });
1153
- }, { flush: "post" });
1154
487
  </script>
1155
488
 
1156
- <style scoped>
1157
- .assistant-transcript__plain-user { white-space: pre-wrap; margin: 0; }
1158
- .assistant-transcript {
1159
- border: 1px solid rgba(var(--v-theme-outline), 0.24);
1160
- border-radius: 8px;
1161
- display: grid;
1162
- gap: 0.35rem;
1163
- grid-template-rows: minmax(0, 1fr);
1164
- min-height: 0;
1165
- overflow: hidden;
1166
- padding: 0.5rem;
1167
- position: relative;
1168
- text-align: left;
1169
- }
1170
-
1171
- .assistant-transcript__loading-skeleton {
1172
- inset: 0;
1173
- overflow: hidden;
1174
- position: absolute;
1175
- z-index: 1;
1176
- }
1177
-
1178
- .assistant-transcript--error {
1179
- grid-template-rows: max-content minmax(0, 1fr);
1180
- min-height: min-content;
1181
- }
1182
-
1183
- .assistant-transcript--error .assistant-transcript__body {
1184
- contain: size;
1185
- }
1186
-
1187
- .assistant-transcript__error {
1188
- position: relative;
1189
- z-index: 4;
1190
- background: rgb(var(--v-theme-surface));
1191
- padding: 0.25rem 0.5rem;
1192
- font-size: 0.85rem;
1193
- min-width: 0;
1194
- overflow-wrap: anywhere;
1195
- }
1196
-
1197
- .assistant-transcript__error-content {
1198
- align-items: center;
1199
- display: flex;
1200
- gap: 0.5rem;
1201
- }
1202
-
1203
- .assistant-transcript__error-content > span {
1204
- flex: 1 1 auto;
1205
- min-width: 0;
1206
- }
1207
-
1208
- .assistant-transcript__error-content > .v-btn {
1209
- flex: 0 0 auto;
1210
- }
1211
-
1212
- .assistant-transcript__settling {
1213
- background: rgb(var(--v-theme-surface));
1214
- inset: 0;
1215
- overflow: hidden;
1216
- padding: 0.5rem;
1217
- position: absolute;
1218
- z-index: 1;
1219
- }
1220
-
1221
- .assistant-transcript__settling-skeleton {
1222
- height: 100%;
1223
- }
1224
-
1225
- .assistant-transcript__reload {
1226
- color: rgba(var(--v-theme-on-surface), 0.66);
1227
- position: absolute;
1228
- right: 0.38rem;
1229
- top: 0.38rem;
1230
- z-index: 2;
1231
- }
1232
-
1233
- .assistant-transcript__body {
1234
- display: flex;
1235
- flex-direction: column;
1236
- gap: 0.65rem;
1237
- min-height: 0;
1238
- min-width: 0;
1239
- overflow-x: hidden;
1240
- overflow-y: auto;
1241
- padding-right: 0.15rem;
1242
- }
1243
-
1244
- .assistant-transcript__welcome {
1245
- background: rgba(var(--v-theme-primary), 0.08);
1246
- border: 1px solid rgba(var(--v-theme-primary), 0.18);
1247
- border-radius: 16px;
1248
- color: rgb(var(--v-theme-on-surface));
1249
- display: grid;
1250
- flex: 0 0 auto;
1251
- gap: 0.55rem;
1252
- margin: 0.1rem 0.15rem 0;
1253
- padding: 0.8rem 0.9rem 0.9rem;
1254
- }
1255
-
1256
- .assistant-transcript__welcome p {
1257
- line-height: 1.48;
1258
- margin: 0;
1259
- }
1260
-
1261
- .assistant-transcript__body--settling {
1262
- visibility: hidden;
1263
- }
1264
-
1265
- .assistant-transcript__load-more {
1266
- align-items: center;
1267
- display: flex;
1268
- flex: 0 0 auto;
1269
- flex-direction: column;
1270
- gap: 0.35rem;
1271
- padding: 0.35rem 0 0.55rem;
1272
- }
1273
-
1274
- .assistant-transcript__load-more-error {
1275
- color: rgb(var(--v-theme-error));
1276
- font-size: 0.76rem;
1277
- line-height: 1.3;
1278
- text-align: center;
1279
- }
1280
-
1281
- .assistant-transcript__load-more + .assistant-transcript__turn,
1282
- .assistant-transcript__body > .assistant-transcript__turn:first-child {
1283
- margin-top: auto;
1284
- }
1285
-
1286
- .assistant-transcript__turn {
1287
- contain-intrinsic-block-size: auto 12rem;
1288
- content-visibility: auto;
1289
- display: flex;
1290
- flex: 0 0 auto;
1291
- flex-direction: column;
1292
- gap: 0.65rem;
1293
- min-height: 0;
1294
- min-width: 0;
1295
- }
1296
-
1297
- .assistant-transcript--retained .assistant-transcript__turn {
1298
- /* Late offscreen height estimates cannot move a restored reader anchor. */
1299
- content-visibility: visible;
1300
- }
1301
-
1302
- .assistant-transcript__message-row {
1303
- align-items: start;
1304
- display: grid;
1305
- gap: 0.65rem;
1306
- max-width: 100%;
1307
- min-width: 0;
1308
- overflow-x: hidden;
1309
- }
1310
-
1311
- .assistant-transcript__message-row--user {
1312
- grid-template-columns: minmax(0, auto) auto;
1313
- justify-self: end;
1314
- max-width: min(28rem, 88%);
1315
- margin-left: auto;
1316
- }
1317
-
1318
- .assistant-transcript__message-row--assistant {
1319
- display: flex;
1320
- flex-direction: column;
1321
- gap: 0.28rem;
1322
- justify-self: start;
1323
- max-width: min(52rem, 94%);
1324
- margin-right: auto;
1325
- }
1326
-
1327
- .assistant-transcript__assistant-header {
1328
- align-items: center;
1329
- display: grid;
1330
- gap: 0.65rem;
1331
- grid-template-columns: auto minmax(0, 1fr);
1332
- min-width: 0;
1333
- }
1334
-
1335
- .assistant-transcript__message {
1336
- display: flex;
1337
- flex-direction: column;
1338
- gap: 0.24rem;
1339
- max-width: 100%;
1340
- min-width: 0;
1341
- overflow-wrap: anywhere;
1342
- word-break: break-word;
1343
- }
1344
-
1345
- .assistant-transcript__avatar {
1346
- align-items: center;
1347
- border-radius: 999px;
1348
- display: inline-flex;
1349
- height: 1.5rem;
1350
- justify-content: center;
1351
- width: 1.5rem;
1352
- }
1353
-
1354
- .assistant-transcript__avatar--user {
1355
- background: rgba(var(--v-theme-primary), 0.1);
1356
- color: rgb(var(--v-theme-primary));
1357
- margin-top: 0.2rem;
1358
- }
1359
-
1360
- .assistant-transcript__avatar--assistant {
1361
- background: rgb(var(--v-theme-primary));
1362
- color: rgb(var(--v-theme-on-primary));
1363
- margin-top: 0.05rem;
1364
- }
1365
-
1366
- .assistant-transcript__message--user {
1367
- background: rgba(var(--v-theme-primary), 0.1);
1368
- border-radius: 16px;
1369
- color: rgb(var(--v-theme-on-surface));
1370
- gap: 0.75rem;
1371
- justify-content: space-between;
1372
- overflow-x: auto;
1373
- padding: 0.75rem 1rem 0.72rem;
1374
- width: fit-content;
1375
- }
1376
-
1377
- .assistant-transcript__message--user.assistant-transcript__message--pending {
1378
- background: rgba(var(--v-theme-on-surface), 0.04);
1379
- outline: 1px dashed rgba(var(--v-theme-on-surface), 0.4);
1380
- outline-offset: -1px;
1381
- }
1382
-
1383
- .assistant-transcript__message--pending .assistant-transcript__message-footer {
1384
- color: rgba(var(--v-theme-on-surface), 0.7);
1385
- }
1386
-
1387
- .assistant-transcript__message--user.assistant-transcript__message--failed {
1388
- outline: 1px solid rgb(var(--v-theme-error));
1389
- outline-offset: -1px;
1390
- }
1391
-
1392
- .assistant-transcript__message--assistant {
1393
- background: rgb(var(--v-theme-surface));
1394
- color: rgb(var(--v-theme-on-surface));
1395
- padding: 0;
1396
- }
1397
-
1398
- .assistant-transcript--task {
1399
- border-color: rgba(126, 87, 194, 0.32);
1400
- }
1401
-
1402
- .assistant-transcript--task .assistant-transcript__avatar--assistant {
1403
- background: #7e57c2;
1404
- }
1405
-
1406
- .assistant-transcript--task .assistant-transcript__message--user {
1407
- background: #eee8f8;
1408
- color: #2d2440;
1409
- }
1410
-
1411
- .assistant-transcript__thinking {
1412
- justify-self: start;
1413
- margin-left: 2.15rem;
1414
- max-width: min(34rem, 86%);
1415
- }
1416
-
1417
- .assistant-transcript__user-content-toggle {
1418
- background: transparent;
1419
- border: 0;
1420
- color: rgb(var(--v-theme-primary));
1421
- cursor: pointer;
1422
- font: inherit;
1423
- text-align: left;
1424
- }
1425
-
1426
- .assistant-transcript__system {
1427
- align-items: start;
1428
- background: transparent;
1429
- color: rgba(var(--v-theme-on-surface), 0.84);
1430
- display: grid;
1431
- gap: 0.45rem;
1432
- grid-template-columns: auto minmax(0, 1fr);
1433
- justify-self: start;
1434
- max-width: min(34rem, 96%);
1435
- min-width: 0;
1436
- padding: 0.1rem 0.15rem;
1437
- }
1438
-
1439
- .assistant-transcript__system-icon {
1440
- color: rgba(var(--v-theme-primary), 0.82);
1441
- margin-top: 0.15rem;
1442
- }
1443
-
1444
- .assistant-transcript__system-body {
1445
- display: grid;
1446
- gap: 0.1rem;
1447
- min-width: 0;
1448
- overflow-wrap: anywhere;
1449
- }
1450
-
1451
- .assistant-transcript__system-meta {
1452
- align-items: center;
1453
- color: rgba(var(--v-theme-on-surface), 0.48);
1454
- display: flex;
1455
- font-size: 0.72rem;
1456
- gap: 0.5rem;
1457
- line-height: 1.15;
1458
- }
1459
-
1460
- .assistant-transcript__message-header {
1461
- align-items: center;
1462
- color: rgba(var(--v-theme-on-surface), 0.82);
1463
- display: flex;
1464
- font-size: 0.9rem;
1465
- gap: 0.7rem;
1466
- justify-content: space-between;
1467
- line-height: 1.2;
1468
- }
1469
-
1470
- .assistant-transcript__message--assistant .assistant-transcript__message-header {
1471
- color: rgba(var(--v-theme-on-surface), 0.78);
1472
- font-weight: 560;
1473
- }
1474
-
1475
- .assistant-transcript__message-header span {
1476
- align-items: center;
1477
- display: flex;
1478
- gap: 0.25rem;
1479
- min-width: 0;
1480
- overflow-wrap: anywhere;
1481
- }
1482
-
1483
- .assistant-transcript__message-header time {
1484
- color: #6d7888;
1485
- font-weight: 650;
1486
- }
1487
-
1488
- .assistant-transcript__message-footer {
1489
- align-items: center;
1490
- color: #9aa6b6;
1491
- display: flex;
1492
- font-size: 0.88rem;
1493
- font-weight: 500;
1494
- gap: 0.65rem;
1495
- line-height: 1.2;
1496
- }
1497
-
1498
- .assistant-transcript__message-footer--user {
1499
- justify-content: flex-start;
1500
- }
1501
-
1502
- .assistant-transcript__message-footer--assistant {
1503
- justify-content: flex-start;
1504
- margin-top: -0.15rem;
1505
- }
1506
-
1507
- .assistant-transcript__optimistic-failure {
1508
- align-items: center;
1509
- color: rgba(var(--v-theme-error), 0.92);
1510
- display: flex;
1511
- flex-wrap: wrap;
1512
- font-size: 0.76rem;
1513
- gap: 0.35rem 0.55rem;
1514
- line-height: 1.25;
1515
- }
1516
-
1517
- .assistant-transcript__optimistic-actions {
1518
- align-items: center;
1519
- display: inline-flex;
1520
- gap: 0.25rem;
1521
- }
1522
-
1523
- @media (max-width: 600px), (pointer: coarse) {
1524
- .assistant-transcript__delivery-action {
1525
- min-height: 3rem;
1526
- min-width: 3rem;
1527
- }
1528
- }
1529
-
1530
- .assistant-transcript__message--assistant :deep(.studio-long-text-review__blocks),
1531
- .assistant-transcript__message--user :deep(.studio-long-text-review__blocks) {
1532
- color: inherit;
1533
- font-size: 0.94rem;
1534
- line-height: 1.5;
1535
- min-width: 0;
1536
- overflow-wrap: anywhere;
1537
- }
1538
-
1539
- .assistant-transcript__message--assistant :deep(.studio-long-text-review__paragraph),
1540
- .assistant-transcript__message--user :deep(.studio-long-text-review__paragraph) {
1541
- font-size: 0.94rem;
1542
- margin-block: 0;
1543
- }
1544
-
1545
- .assistant-transcript__message--user :deep(.studio-long-text-review__paragraph) {
1546
- white-space: pre-wrap;
1547
- }
1548
-
1549
- .assistant-transcript__user-content {
1550
- min-width: 0;
1551
- }
1552
-
1553
- .assistant-transcript__user-content-toggle {
1554
- align-self: flex-start;
1555
- font-size: 0.78rem;
1556
- font-weight: 650;
1557
- padding: 0;
1558
- }
1559
-
1560
- .assistant-transcript__user-content-toggle:hover,
1561
- .assistant-transcript__user-content-toggle:focus-visible {
1562
- text-decoration: underline;
1563
- }
1564
-
1565
- .assistant-transcript__message--assistant :deep(.studio-long-text-review__paragraph code),
1566
- .assistant-transcript__message--assistant :deep(.studio-long-text-review__list li > code),
1567
- .assistant-transcript__message--assistant :deep(.studio-long-text-review__table th > code),
1568
- .assistant-transcript__message--assistant :deep(.studio-long-text-review__table td > code),
1569
- .assistant-transcript__message--assistant :deep(.studio-long-text-review__details-summary code),
1570
- .assistant-transcript__message--user :deep(.studio-long-text-review__paragraph code),
1571
- .assistant-transcript__message--user :deep(.studio-long-text-review__list li > code),
1572
- .assistant-transcript__message--user :deep(.studio-long-text-review__table th > code),
1573
- .assistant-transcript__message--user :deep(.studio-long-text-review__table td > code),
1574
- .assistant-transcript__message--user :deep(.studio-long-text-review__details-summary code) {
1575
- background: rgba(var(--v-theme-primary), 0.07);
1576
- border: 1px solid rgba(var(--v-theme-primary), 0.14);
1577
- border-radius: 3px;
1578
- color: rgba(var(--v-theme-on-surface), 0.9);
1579
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1580
- font-size: 0.9em;
1581
- padding: 0.02rem 0.2rem;
1582
- }
1583
-
1584
- .assistant-transcript__questions {
1585
- box-sizing: border-box;
1586
- display: grid;
1587
- gap: 0.2rem;
1588
- list-style: none;
1589
- margin: 0;
1590
- max-width: 100%;
1591
- min-width: 0;
1592
- padding: 0;
1593
- }
1594
-
1595
- .assistant-transcript__question {
1596
- align-items: start;
1597
- background: rgba(var(--v-theme-surface), 0.62);
1598
- border: 1px solid rgba(var(--v-theme-outline), 0.2);
1599
- border-radius: 8px;
1600
- box-sizing: border-box;
1601
- display: grid;
1602
- gap: 0.32rem;
1603
- grid-template-columns: auto minmax(0, 1fr);
1604
- max-width: 100%;
1605
- min-width: 0;
1606
- padding: 0.28rem 0.42rem;
1607
- }
1608
-
1609
- .assistant-transcript__question-number {
1610
- align-items: center;
1611
- background: rgba(var(--v-theme-primary), 0.1);
1612
- border: 1px solid rgba(var(--v-theme-primary), 0.2);
1613
- border-radius: 999px;
1614
- color: rgb(var(--v-theme-primary));
1615
- display: inline-flex;
1616
- font-size: 0.72rem;
1617
- font-weight: 760;
1618
- height: 1.35rem;
1619
- justify-content: center;
1620
- line-height: 1;
1621
- min-width: 1.35rem;
1622
- }
1623
-
1624
- .assistant-transcript__question-text {
1625
- color: rgb(var(--v-theme-on-surface));
1626
- font-size: 0.9rem;
1627
- line-height: 1.35;
1628
- max-width: 100%;
1629
- min-width: 0;
1630
- overflow-wrap: anywhere;
1631
- }
1632
-
1633
- .assistant-transcript__question-content {
1634
- display: block;
1635
- min-width: 0;
1636
- }
1637
-
1638
- .assistant-transcript__question-choices {
1639
- color: rgba(var(--v-theme-on-surface), 0.72);
1640
- display: inline;
1641
- font-size: 0.8rem;
1642
- margin: 0;
1643
- padding: 0;
1644
- }
1645
-
1646
- .assistant-transcript__question-choices::before {
1647
- content: " · ";
1648
- }
1649
-
1650
- .assistant-transcript__question-choices li {
1651
- display: inline;
1652
- overflow-wrap: anywhere;
1653
- }
1654
-
1655
- .assistant-transcript__question-choices li + li::before {
1656
- content: " · ";
1657
- }
1658
-
1659
- .assistant-transcript__bottom {
1660
- height: 1px;
1661
- }
1662
- </style>
489
+ <style scoped src="./AssistantTranscript.css"></style>