@musnows/scriverse 1.0.6 → 1.0.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.
@@ -9,7 +9,7 @@
9
9
  <script src="/theme-init.js?v=20260827-reader-prefetch-v1"></script>
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
- <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=continuation-guard-failure-details-v1&feature=entity-editor-back-icon-v1&feature=toast-stack-v1">
12
+ <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=ai-composer-compact-controls-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=continuation-guard-failure-details-v1&feature=entity-editor-back-icon-v1&feature=toast-stack-v1">
13
13
  </head>
14
14
  <body class="auth-pending">
15
15
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -440,7 +440,7 @@
440
440
  <div id="ai-prompt" class="ai-prompt" contenteditable="true" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-controls="ai-mention-menu" aria-haspopup="listbox" aria-expanded="false" aria-keyshortcuts="Enter" title="Enter 发送,Shift+Enter 换行" data-placeholder="告诉 AI 你想讨论或修改什么……"></div>
441
441
  <div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定、章节或上下文能力"></div>
442
442
  <div class="prompt-composer-actions">
443
- <button id="ai-context-meter" class="ai-context-meter is-empty" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-context-popover" aria-live="polite" aria-label="当前上下文用量"><b>—</b></button>
443
+ <button id="ai-context-meter" class="ai-context-meter is-empty" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-context-popover" aria-live="polite" aria-label="当前上下文用量"></button>
444
444
  <section id="ai-context-popover" class="ai-context-popover hidden" role="dialog" aria-labelledby="ai-context-popover-title" aria-describedby="ai-context-popover-description">
445
445
  <header class="ai-context-popover-header"><div><strong id="ai-context-popover-title">Token 分布</strong><small id="ai-context-popover-description">按当前模型上下文窗口计算</small></div><button id="ai-context-popover-close" class="ai-context-popover-close" type="button" aria-label="关闭 Token 分布">×</button></header>
446
446
  <div id="ai-context-distribution" class="ai-context-distribution" role="list" aria-label="当前 Token 分布"></div>
@@ -522,6 +522,8 @@
522
522
  <button type="button" role="menuitemradio" data-chapter-type="设定">设定</button>
523
523
  <button type="button" role="menuitemradio" data-chapter-type="作者的话">作者的话</button>
524
524
  <button type="button" role="menuitemradio" data-chapter-type="其他">其他</button>
525
+ <div id="chapter-type-ai-reference-separator" class="line-citation-menu-separator hidden" role="separator" aria-orientation="horizontal"></div>
526
+ <button type="button" role="menuitem" data-add-chapter-ai-reference>添加到助手引用</button>
525
527
  <button class="danger-button" type="button" role="menuitem" data-delete-chapter>删除章节</button>
526
528
  </div>
527
529
  </div>
@@ -1425,6 +1427,7 @@
1425
1427
  </dialog>
1426
1428
 
1427
1429
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1428
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=continuation-guard-failure-details-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=ai-suggestion-editor-boundary-v1&feature=setting-category-preservation-v1&feature=toast-stack-v1&feature=toast-stack-label-v2"></script>
1430
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=continuation-guard-failure-details-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=ai-suggestion-editor-boundary-v1&feature=setting-category-preservation-v1&feature=toast-stack-v1&feature=toast-stack-label-v2&feature=agent-tool-limits-300-v1&feature=server-dev-logo-v1&feature=chapter-directory-ai-reference-v3&feature=ai-error-origin-v1&feature=ai-stream-render-performance-v2"></script>
1431
+
1429
1432
  </body>
1430
1433
  </html>
@@ -142,8 +142,20 @@ function renderMarkdownTable(headers, alignments, rows) {
142
142
  }
143
143
 
144
144
  export function renderMarkdown(value) {
145
+ return renderMarkdownParts(value).map((part) => part.html).join("");
146
+ }
147
+
148
+ export function renderMarkdownParts(value) {
145
149
  const lines = String(value ?? "").replace(/\r\n?/gu, "\n").split("\n");
146
150
  const output = [];
151
+ const offsets = [];
152
+ let offset = 0;
153
+ for (const line of lines) {
154
+ offsets.push(offset);
155
+ offset += line.length + 1;
156
+ }
157
+ let lineIndex = 0;
158
+ const emit = (html, endLine = lineIndex) => output.push({ html, end: offsets[endLine] ?? offset - 1 });
147
159
  let paragraph = [];
148
160
  let list = null;
149
161
  let quote = null;
@@ -151,12 +163,12 @@ export function renderMarkdown(value) {
151
163
 
152
164
  const flushParagraph = () => {
153
165
  if (!paragraph.length) return;
154
- output.push(`<p>${paragraph.map(renderInlineMarkdown).join("<br>")}</p>`);
166
+ emit(`<p>${paragraph.map(renderInlineMarkdown).join("<br>")}</p>`);
155
167
  paragraph = [];
156
168
  };
157
169
  const flushList = () => {
158
170
  if (!list) return;
159
- output.push(`<${list.tag}>${list.items.map((item) => `<li class="markdown-depth-${item.depth}">${renderInlineMarkdown(item.text)}</li>`).join("")}</${list.tag}>`);
171
+ emit(`<${list.tag}>${list.items.map((item) => `<li class="markdown-depth-${item.depth}">${renderInlineMarkdown(item.text)}</li>`).join("")}</${list.tag}>`);
160
172
  list = null;
161
173
  };
162
174
  const flushQuote = () => {
@@ -166,7 +178,7 @@ export function renderMarkdown(value) {
166
178
  const html = content.split(/\n\s*\n/gu)
167
179
  .map((part) => part.split("\n").map(renderInlineMarkdown).join("<br>"))
168
180
  .join("<br><br>");
169
- output.push(`<blockquote>${html}</blockquote>`);
181
+ emit(`<blockquote>${html}</blockquote>`);
170
182
  }
171
183
  quote = null;
172
184
  };
@@ -176,11 +188,11 @@ export function renderMarkdown(value) {
176
188
  flushQuote();
177
189
  };
178
190
 
179
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
191
+ for (lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
180
192
  const line = lines[lineIndex];
181
193
  if (codeFence) {
182
194
  if (/^\s*```/u.test(line)) {
183
- output.push(`<pre><code${codeFence.language ? ` class="language-${codeFence.language}"` : ""}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`);
195
+ emit(`<pre><code${codeFence.language ? ` class="language-${codeFence.language}"` : ""}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`, lineIndex + 1);
184
196
  codeFence = null;
185
197
  } else {
186
198
  codeFence.lines.push(line);
@@ -230,12 +242,12 @@ export function renderMarkdown(value) {
230
242
  if (heading) {
231
243
  flushBlocks();
232
244
  const level = heading[1].length;
233
- output.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
245
+ emit(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`, lineIndex + 1);
234
246
  continue;
235
247
  }
236
248
  if (/^\s*(---+|___+|\*\*\*+)\s*$/u.test(line)) {
237
249
  flushBlocks();
238
- output.push("<hr>");
250
+ emit("<hr>", lineIndex + 1);
239
251
  continue;
240
252
  }
241
253
  const tableHeaders = splitMarkdownTableRow(line);
@@ -251,7 +263,7 @@ export function renderMarkdown(value) {
251
263
  lineIndex += 1;
252
264
  }
253
265
  lineIndex -= 1;
254
- output.push(renderMarkdownTable(tableHeaders, alignments, rows));
266
+ emit(renderMarkdownTable(tableHeaders, alignments, rows), lineIndex + 1);
255
267
  continue;
256
268
  }
257
269
  const listItem = line.match(/^(\s*)([-+*]|\d+\.)\s+(.+)$/u);
@@ -266,7 +278,7 @@ export function renderMarkdown(value) {
266
278
  if (list) flushList();
267
279
  paragraph.push(line);
268
280
  }
269
- if (codeFence) output.push(`<pre><code${codeFence.language ? ` class="language-${codeFence.language}"` : ""}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`);
281
+ if (codeFence) emit(`<pre><code${codeFence.language ? ` class="language-${codeFence.language}"` : ""}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`);
270
282
  flushBlocks();
271
- return output.join("");
283
+ return output;
272
284
  }
@@ -0,0 +1,107 @@
1
+ import { renderMarkdownParts } from "./markdown.js?v=20260912-stream-render-v2";
2
+
3
+ // 保留末尾两个块,等待列表、引用和表格的后续行确定语义后再冻结前缀。
4
+ export function createMarkdownStreamState() {
5
+ let previous = "";
6
+ let committed = 0;
7
+ return {
8
+ update(value) {
9
+ const source = String(value ?? "").replace(/\r\n?/gu, "\n");
10
+ const reset = !source.startsWith(previous);
11
+ if (reset) committed = 0;
12
+ const parts = renderMarkdownParts(source.slice(committed));
13
+ const stableCount = Math.max(0, parts.length - 2);
14
+ if (stableCount) committed += parts[stableCount - 1].end;
15
+ previous = source;
16
+ return { reset, parts, stableCount };
17
+ }
18
+ };
19
+ }
20
+
21
+ // 每个节点都是可让出主线程的边界,长列表和表格也不会作为一个大块集中挂载。
22
+ function* patchMarkdownNode(current, next, parent) {
23
+ if (!current || current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) {
24
+ const replacement = next.cloneNode(false);
25
+ if (current) current.replaceWith(replacement);
26
+ else parent.append(replacement);
27
+ current = replacement;
28
+ }
29
+ if (current.nodeType === 3) {
30
+ if (current.data === next.data) return current;
31
+ if (next.data.startsWith(current.data)) current.appendData(next.data.slice(current.data.length));
32
+ else if (current.data !== next.data) current.data = next.data;
33
+ yield;
34
+ return current;
35
+ }
36
+ if (current.nodeType !== 1) return current;
37
+ for (const attribute of [...current.attributes]) {
38
+ if (!next.hasAttribute(attribute.name)) current.removeAttribute(attribute.name);
39
+ }
40
+ for (const attribute of next.attributes) {
41
+ if (current.getAttribute(attribute.name) !== attribute.value) current.setAttribute(attribute.name, attribute.value);
42
+ }
43
+ yield;
44
+ const children = [...current.childNodes];
45
+ const nextChildren = [...next.childNodes];
46
+ for (const [index, child] of nextChildren.entries()) yield* patchMarkdownNode(children[index], child, current);
47
+ for (const child of children.slice(nextChildren.length)) { child.remove(); yield; }
48
+ return current;
49
+ }
50
+
51
+ export function createStreamingMarkdownRenderer(host, {
52
+ enqueue = (callback) => requestAnimationFrame(callback),
53
+ onRender = () => {},
54
+ now = () => performance.now(),
55
+ budgetMs = 3,
56
+ nodeBudget = 120
57
+ } = {}) {
58
+ const state = createMarkdownStreamState();
59
+ let tail = [];
60
+ let requestedValue = null;
61
+ let appliedValue = null;
62
+ let work = null;
63
+ let queued = false;
64
+ function* apply(value) {
65
+ const update = state.update(value);
66
+ if (update.reset) {
67
+ for (const child of [...host.childNodes]) { child.remove(); yield; }
68
+ tail = [];
69
+ }
70
+ const next = [];
71
+ for (const [index, part] of update.parts.entries()) {
72
+ const previous = tail[index];
73
+ if (previous?.html === part.html) { next.push(previous); continue; }
74
+ const template = host.ownerDocument.createElement("template");
75
+ template.innerHTML = part.html;
76
+ const node = yield* patchMarkdownNode(previous?.node, template.content.firstChild, host);
77
+ next.push({ html: part.html, node });
78
+ }
79
+ for (const item of tail.slice(next.length)) { item.node.remove(); yield; }
80
+ tail = next.slice(update.stableCount);
81
+ appliedValue = value;
82
+ }
83
+ const drain = () => {
84
+ queued = false;
85
+ const started = now();
86
+ if (!work && requestedValue !== appliedValue) work = apply(requestedValue);
87
+ let nodes = 0;
88
+ while (work && nodes < nodeBudget && now() - started < budgetMs) {
89
+ if (work.next().done) { work = null; break; }
90
+ nodes += 1;
91
+ }
92
+ onRender();
93
+ if (work || requestedValue !== appliedValue) schedule();
94
+ };
95
+ const schedule = () => {
96
+ if (queued) return;
97
+ queued = true;
98
+ enqueue(drain);
99
+ };
100
+ return (value) => {
101
+ const normalized = String(value ?? "");
102
+ if (normalized === requestedValue) return false;
103
+ requestedValue = normalized;
104
+ schedule();
105
+ return true;
106
+ };
107
+ }
@@ -27,6 +27,7 @@ export function createStreamTypewriter<FrameHandle = number>(options: {
27
27
  scheduleFrame?: (callback: () => void) => FrameHandle;
28
28
  cancelFrame?: (handle: FrameHandle) => void;
29
29
  reducedMotion?: boolean;
30
+ shouldAnimate?: () => boolean;
30
31
  speedController?: StreamTypewriterSpeedController | null;
31
32
  visibilitySource?: Pick<Document, "visibilityState" | "addEventListener" | "removeEventListener"> | null;
32
33
  }): StreamTypewriter;
@@ -75,23 +75,34 @@ export function createStreamTypewriter({
75
75
  cancelFrame = (handle) => window.cancelAnimationFrame(handle),
76
76
  reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true,
77
77
  speedController = null,
78
+ shouldAnimate = () => true,
78
79
  visibilitySource = typeof document === "undefined" ? null : document
79
80
  }) {
80
81
  if (typeof onRender !== "function") throw new TypeError("onRender must be a function");
81
82
 
82
- const visibleCharacters = [];
83
- const pendingCharacters = [];
83
+ let visibleText = "";
84
+ let visibleCharacterCount = 0;
85
+ let pendingCharacters = [];
86
+ let pendingOffset = 0;
87
+ const pendingCount = () => pendingCharacters.length - pendingOffset;
88
+ const takePending = (count) => {
89
+ const characters = pendingCharacters.slice(pendingOffset, pendingOffset + count);
90
+ visibleText += characters.join("");
91
+ visibleCharacterCount += characters.length;
92
+ pendingOffset += characters.length;
93
+ if (pendingOffset === pendingCharacters.length) { pendingCharacters = []; pendingOffset = 0; }
94
+ };
84
95
  const idleResolvers = [];
85
96
  let scheduledFrame = null;
86
97
  let finishing = false;
87
98
  let listeningForVisibility = false;
88
99
 
89
- const snapshot = () => visibleCharacters.join("");
100
+ const snapshot = () => visibleText;
90
101
  const visibilityChanged = () => {
91
- if (pendingCharacters.length) typewriter.reveal();
102
+ if (pendingCount()) typewriter.reveal();
92
103
  };
93
104
  const resolveIdle = () => {
94
- if (pendingCharacters.length || scheduledFrame !== null) return;
105
+ if (pendingCount() || scheduledFrame !== null) return;
95
106
  if (listeningForVisibility) {
96
107
  visibilitySource.removeEventListener("visibilitychange", visibilityChanged);
97
108
  listeningForVisibility = false;
@@ -101,32 +112,33 @@ export function createStreamTypewriter({
101
112
  };
102
113
  const render = () => {
103
114
  onRender(snapshot(), {
104
- visibleCharacters: visibleCharacters.length,
105
- pendingCharacters: pendingCharacters.length
115
+ visibleCharacters: visibleCharacterCount,
116
+ pendingCharacters: pendingCount()
106
117
  });
107
118
  };
108
119
  const schedule = () => {
109
- if (visibilitySource?.visibilityState === "hidden") {
120
+ if (visibilitySource?.visibilityState === "hidden" || !shouldAnimate()) {
110
121
  typewriter.reveal();
111
122
  return;
112
123
  }
113
- if (scheduledFrame !== null || pendingCharacters.length === 0) return;
124
+ if (scheduledFrame !== null || pendingCount() === 0) return;
114
125
  if (visibilitySource && !listeningForVisibility) {
115
126
  visibilitySource.addEventListener("visibilitychange", visibilityChanged);
116
127
  listeningForVisibility = true;
117
128
  }
118
129
  scheduledFrame = scheduleFrame(() => {
119
130
  scheduledFrame = null;
120
- const batchSize = reducedMotion
121
- ? pendingCharacters.length
131
+ const batchSize = reducedMotion || !shouldAnimate()
132
+ ? pendingCount()
122
133
  : streamTypewriterBatchSize(
123
- pendingCharacters.length,
134
+ pendingCount(),
124
135
  finishing,
125
136
  speedController?.charactersPerSecond?.()
126
137
  );
127
- visibleCharacters.push(...pendingCharacters.splice(0, batchSize));
138
+ const catchUp = pendingCount() > 4096 ? Math.min(2048, Math.ceil(pendingCount() / 8)) : batchSize;
139
+ takePending(Math.max(batchSize, catchUp));
128
140
  render();
129
- if (pendingCharacters.length) schedule();
141
+ if (pendingCount()) schedule();
130
142
  else resolveIdle();
131
143
  });
132
144
  };
@@ -136,7 +148,7 @@ export function createStreamTypewriter({
136
148
  const characters = Array.from(String(value ?? ""));
137
149
  if (!characters.length) return;
138
150
  speedController?.observe?.(characters.length);
139
- pendingCharacters.push(...characters);
151
+ for (const character of characters) pendingCharacters.push(character);
140
152
  schedule();
141
153
  },
142
154
  replace(value) {
@@ -144,15 +156,17 @@ export function createStreamTypewriter({
144
156
  cancelFrame(scheduledFrame);
145
157
  scheduledFrame = null;
146
158
  }
147
- visibleCharacters.splice(0, visibleCharacters.length, ...Array.from(String(value ?? "")));
148
- pendingCharacters.splice(0);
159
+ visibleText = String(value ?? "");
160
+ visibleCharacterCount = Array.from(visibleText).length;
161
+ pendingCharacters = [];
162
+ pendingOffset = 0;
149
163
  finishing = false;
150
164
  render();
151
165
  resolveIdle();
152
166
  return snapshot();
153
167
  },
154
168
  finish() {
155
- if (!pendingCharacters.length && scheduledFrame === null) return Promise.resolve(snapshot());
169
+ if (!pendingCount() && scheduledFrame === null) return Promise.resolve(snapshot());
156
170
  finishing = true;
157
171
  const completed = new Promise((resolve) => idleResolvers.push(resolve));
158
172
  schedule();
@@ -163,7 +177,7 @@ export function createStreamTypewriter({
163
177
  cancelFrame(scheduledFrame);
164
178
  scheduledFrame = null;
165
179
  }
166
- visibleCharacters.push(...pendingCharacters.splice(0));
180
+ takePending(pendingCount());
167
181
  finishing = false;
168
182
  render();
169
183
  resolveIdle();
@@ -3172,11 +3172,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3172
3172
  .ai-image-preview-body { display: grid; place-items: center; min-height: 180px; max-height: calc(88vh - 106px); padding: 16px 20px 20px; overflow: auto; background: var(--paper); }
3173
3173
  .ai-image-preview-body img { display: block; max-width: 100%; max-height: calc(88vh - 150px); width: auto; height: auto; object-fit: contain; border-radius: 3px; }
3174
3174
  .ai-attachment-input { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
3175
- .ai-attachment-button, .ai-scene-button { display: grid; place-items: center; width: 32px; min-width: 32px; min-height: 32px; height: 32px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); }
3175
+ .ai-attachment-button, .ai-scene-button { display: grid; place-items: center; width: 24px; min-width: 24px; min-height: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); }
3176
3176
  .ai-attachment-button:hover, .ai-attachment-button:focus-visible, .ai-scene-button:hover, .ai-scene-button:focus-visible, .ai-scene-button[aria-expanded="true"], .ai-scene-button.is-active { border-color: color-mix(in srgb, var(--accent) 56%, var(--line)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark); outline: 0; }
3177
3177
  .ai-attachment-button:disabled, .ai-scene-button:disabled { cursor: wait; opacity: .56; }
3178
3178
  .ai-scene-button.hidden { display: none; }
3179
- .ai-image-button-icon, .ai-scene-button-icon { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
3179
+ .ai-image-button-icon, .ai-scene-button-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
3180
3180
 
3181
3181
  .character-roleplay-memory-surface { display: grid; grid-column: 1 / -1; gap: 14px; min-width: 0; }
3182
3182
  .roleplay-memory-toolbar { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
@@ -3230,7 +3230,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3230
3230
  .ai-skill-option > span { display: grid; gap: 2px; min-width: 0; }
3231
3231
  .ai-skill-option em { color: var(--muted); font-size: 9px; font-style: normal; line-height: 1.35; overflow-wrap: anywhere; }
3232
3232
  .ai-mention-empty { margin: 0; padding: 10px; color: var(--muted); font-size: 10px; text-align: center; }
3233
- .ai-prompt { display: block; width: 100%; min-height: 104px; margin: 0; padding: 10px 10px 44px; overflow-y: auto; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; cursor: text; }
3233
+ .ai-prompt { display: block; width: 100%; min-height: 104px; margin: 0; padding: 10px 10px 36px; overflow-y: auto; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; cursor: text; }
3234
3234
  .ai-prompt:empty::before { color: var(--muted); content: attr(data-placeholder); pointer-events: none; }
3235
3235
  .ai-prompt:focus { border-color: #a77768; box-shadow: 0 0 0 3px rgba(167,119,104,.12); outline: 0; }
3236
3236
  .ai-prompt-reference { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; margin: 0 2px; padding: 2px 4px 2px 7px; border: 1px solid color-mix(in srgb, var(--accent) 48%, var(--line)); border-radius: 999px; background: color-mix(in srgb, var(--accent) 13%, var(--paper)); color: var(--accent-dark); font-size: 10px; line-height: 1.25; vertical-align: baseline; }
@@ -3238,28 +3238,28 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3238
3238
  .ai-prompt-reference button { width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; font: 14px/1 var(--font-latin), monospace; }
3239
3239
  .ai-prompt-reference button:hover, .ai-prompt-reference button:focus-visible { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
3240
3240
  .prompt-composer-actions { position: absolute; right: 8px; bottom: 8px; display: flex; gap: 6px; align-items: center; }
3241
- .ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 32px; place-items: center; width: 32px; min-height: 32px; height: 32px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; cursor: pointer; }
3242
- .ai-context-meter::before { content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--paper); box-shadow: inset 0 0 0 1px rgba(143,132,116,.12); }
3241
+ .ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 24px; place-items: center; width: 24px; min-height: 24px; height: 24px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; cursor: pointer; }
3242
+ .ai-context-meter::before { content: ""; position: absolute; inset: 2px; border-radius: 50%; background: var(--paper); box-shadow: inset 0 0 0 1px rgba(143,132,116,.12); }
3243
3243
  .ai-context-meter:hover, .ai-context-meter:focus-visible, .ai-context-meter[aria-expanded="true"] { outline: 0; filter: brightness(.96); }
3244
3244
  .ai-context-meter:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
3245
- .ai-context-meter b { position: relative; z-index: 1; font-size: 9px; font-weight: 600; }.ai-context-meter.is-warning { --context-meter-color: #bf6a35; }.ai-context-meter.is-danger { --context-meter-color: #ad463c; }.ai-context-meter.is-empty { opacity: .62; }
3245
+ .ai-context-meter.is-warning { --context-meter-color: #bf6a35; }.ai-context-meter.is-danger { --context-meter-color: #ad463c; }.ai-context-meter.is-empty { opacity: .62; }
3246
3246
  .ai-context-popover { position: absolute; right: 0; bottom: calc(100% + 10px); z-index: 40; display: grid; width: min(320px, calc(100vw - 32px)); gap: 12px; padding: 14px; border: 1px solid var(--line); border-radius: 7px; background: var(--paper); color: var(--ink); box-shadow: 0 14px 38px rgba(36, 30, 24, .18); }
3247
3247
  .ai-context-popover.hidden { display: none; }
3248
- .ai-context-popover::after { position: absolute; right: 87px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
3248
+ .ai-context-popover::after { position: absolute; right: 67px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
3249
3249
  .ai-context-popover-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
3250
3250
  .ai-context-popover-header > div { display: grid; gap: 3px; min-width: 0; }
3251
3251
  .ai-context-popover-header strong { font-size: 12px; }
3252
3252
  .ai-context-popover-header small { color: var(--muted); font: 9px/1.4 var(--font-latin), var(--font-cjk), sans-serif; }
3253
3253
  .ai-context-popover-close { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); font: 17px/1 var(--font-latin), sans-serif; }
3254
3254
  .ai-context-popover-close:hover, .ai-context-popover-close:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: 0; }
3255
- .ai-model-picker { display: grid; flex: 0 0 32px; place-items: center; width: 32px; min-width: 32px; min-height: 32px; height: 32px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); }
3255
+ .ai-model-picker { display: grid; flex: 0 0 24px; place-items: center; width: 24px; min-width: 24px; min-height: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); }
3256
3256
  .ai-model-picker:hover, .ai-model-picker:focus-visible, .ai-model-picker[aria-expanded="true"] { border-color: color-mix(in srgb, var(--accent) 56%, var(--line)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark); outline: 0; }
3257
3257
  .ai-model-picker:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
3258
3258
  .ai-model-picker:disabled { cursor: wait; opacity: .56; }
3259
- .ai-model-picker-icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
3259
+ .ai-model-picker-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
3260
3260
  .ai-model-popover { position: absolute; right: 0; bottom: calc(100% + 10px); z-index: 41; display: grid; width: min(280px, calc(100vw - 32px)); gap: 7px; padding: 12px; border: 1px solid var(--line); border-radius: 7px; background: var(--paper); color: var(--ink); box-shadow: 0 14px 38px rgba(36, 30, 24, .18); }
3261
3261
  .ai-model-popover.hidden { display: none; }
3262
- .ai-model-popover::after { position: absolute; right: 49px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
3262
+ .ai-model-popover::after { position: absolute; right: 37px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
3263
3263
  .ai-model-popover > label { color: var(--muted); font-size: 10px; font-weight: 600; }
3264
3264
  .ai-model-native-select { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
3265
3265
  .ai-model-options { display: grid; max-height: 240px; gap: 3px; overflow-y: auto; }
@@ -3287,16 +3287,16 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3287
3287
  .ai-context-distribution-row[data-key="input"] { --distribution-color: var(--green); }
3288
3288
  .ai-context-distribution-row[data-key="output"] { --distribution-color: #718a9f; }
3289
3289
  .ai-context-distribution-row[data-key="left"] { --distribution-color: color-mix(in srgb, var(--muted) 55%, transparent); }
3290
- .ai-send-button { display: grid; flex: 0 0 32px; place-items: center; width: 32px; min-width: 32px; min-height: 32px; height: 32px; padding: 0; border: 0; border-radius: 4px; background: var(--accent); color: #fff; box-shadow: 0 4px 12px rgba(139,61,44,.2); }
3290
+ .ai-send-button { display: grid; flex: 0 0 24px; place-items: center; width: 24px; min-width: 24px; min-height: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: var(--accent); color: #fff; box-shadow: 0 3px 8px rgba(139,61,44,.2); }
3291
3291
  .ai-send-button:hover, .ai-send-button:focus-visible { background: var(--accent-dark); outline: 0; }
3292
- .ai-send-button:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent), 0 4px 12px rgba(139,61,44,.2); }
3292
+ .ai-send-button:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent), 0 3px 8px rgba(139,61,44,.2); }
3293
3293
  .ai-send-button:disabled { cursor: wait; opacity: .62; }
3294
- .ai-send-button-icon { width: 17px; height: 17px; overflow: visible; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
3294
+ .ai-send-button-icon { width: 14px; height: 14px; overflow: visible; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
3295
3295
  .ai-message-images { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
3296
3296
  .ai-message-image-preview { display: block; width: 68px; height: 68px; padding: 0; overflow: hidden; border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line)); border-radius: 5px; background: var(--paper); cursor: zoom-in; }
3297
3297
  .ai-message-image-preview:hover, .ai-message-image-preview:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 72%, transparent); outline-offset: 1px; }
3298
3298
  .ai-message-image-preview img { display: block; width: 100%; height: 100%; object-fit: cover; }
3299
- .ai-send-button.is-stop .ai-send-button-icon { width: 15px; height: 15px; fill: currentColor; stroke: none; }
3299
+ .ai-send-button.is-stop .ai-send-button-icon { width: 12px; height: 12px; fill: currentColor; stroke: none; }
3300
3300
  .ai-citations { display: grid; gap: 6px; max-height: 190px; overflow-y: auto; margin-bottom: 8px; }
3301
3301
  .ai-citation-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; border: 1px solid var(--line); border-radius: 5px; background: var(--paper); overflow: hidden; }
3302
3302
  .ai-citation-main { min-width: 0; padding: 8px 9px; border: 0; background: transparent; text-align: left; }
@@ -4705,7 +4705,6 @@ body { font-size: var(--ui-font-size); }
4705
4705
  .user-message-scene { font-size: calc(10px * var(--ai-font-scale)); }
4706
4706
  .user-message-scene-label { font-size: calc(8px * var(--ai-font-scale)); }
4707
4707
  .ai-prompt-reference { font-size: calc(10px * var(--ai-font-scale)); }
4708
- .ai-context-meter b { font-size: calc(9px * var(--ai-font-scale)); }
4709
4708
  .ai-context-popover-header strong { font-size: calc(12px * var(--ai-font-scale)); }
4710
4709
  .ai-context-popover-header small, .ai-context-distribution-label { font-size: calc(10px * var(--ai-font-scale)); }
4711
4710
  .ai-context-distribution-label small, .ai-context-distribution-label strong { font-size: calc(9px * var(--ai-font-scale)); }
package/dist/store.js CHANGED
@@ -11,7 +11,7 @@ import { currentRequestActor } from "./request-context.js";
11
11
  import { canReadWorkModule, canWriteWorkModule, classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
12
12
  import { countWords, documentShortSearchTerms, escapeSqlLikePattern, id, json, normalizeDocumentSearchText, now, splitDocumentParagraphs } from "./utils.js";
13
13
  import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
14
- import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
14
+ import { DEFAULT_AGENT_TOOL_CALL_LIMIT, MIN_AGENT_TOOL_CALL_LIMIT, resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
15
15
  import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
16
16
  import { normalizeRoleplayScenePin, roleplayUserTurnDisplayText, roleplayUserTurnTitleSource } from "./roleplay-turn.js";
17
17
  import { chapterAnnotationLineHashes, createChapterLineIds, MAX_CHAPTER_LINE_IDS, parseChapterAnnotationLineIds, parseChapterAnnotationLineHashes, parseChapterLineIds, reconcileChapterLineIds, reanchorChapterAnnotations } from "./chapter-annotation-anchor.js";
@@ -1104,7 +1104,7 @@ export class Store {
1104
1104
  bookSummaryContextPercent: Math.min(90, Math.max(1, Number(row?.book_summary_context_percent ?? 50) || 50)),
1105
1105
  contextCompactThreshold: Math.min(90, Math.max(50, Number(row?.context_compact_threshold ?? 85) || 85)),
1106
1106
  agentToolCallLimitMaximum: maximumAgentToolCallLimit,
1107
- agentToolCallLimit: Math.min(maximumAgentToolCallLimit, Math.max(5, Number(row?.agent_tool_call_limit ?? 12) || 12)),
1107
+ agentToolCallLimit: Math.min(maximumAgentToolCallLimit, Math.max(MIN_AGENT_TOOL_CALL_LIMIT, Number(row?.agent_tool_call_limit ?? DEFAULT_AGENT_TOOL_CALL_LIMIT) || DEFAULT_AGENT_TOOL_CALL_LIMIT)),
1108
1108
  agentToolCallGlobalMultiplier: Math.min(6, Math.max(1, Number(row?.agent_tool_call_global_multiplier ?? 3) || 3)),
1109
1109
  agentTools: normalizeWorkAgentTools(row?.agent_tools_json),
1110
1110
  imageToolModelId: row?.image_tool_model_id === null || row?.image_tool_model_id === undefined
@@ -1190,7 +1190,7 @@ export class Store {
1190
1190
  title_generation_model_id = excluded.title_generation_model_id,
1191
1191
  image_tool_model_id = excluded.image_tool_model_id,
1192
1192
  always_include_setting_info = excluded.always_include_setting_info,
1193
- updated_at = excluded.updated_at`, workId, nextPrompt, nextSystemPromptOverride ? 1 : 0, nextDailyTokenQuota, nextMonthlyTokenQuota, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), Math.min(120, Math.max(1, nextStabilityDelayMinutes)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), Math.min(maximumAgentToolCallLimit, Math.max(5, nextAgentToolCallLimit)), Math.min(6, Math.max(1, nextAgentToolCallGlobalMultiplier)), JSON.stringify(nextAgentTools), nextTitleGenerationModelId, nextImageToolModelId, nextAlwaysIncludeSettingInfo ? 1 : 0, timestamp);
1193
+ updated_at = excluded.updated_at`, workId, nextPrompt, nextSystemPromptOverride ? 1 : 0, nextDailyTokenQuota, nextMonthlyTokenQuota, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), Math.min(120, Math.max(1, nextStabilityDelayMinutes)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), Math.min(maximumAgentToolCallLimit, Math.max(MIN_AGENT_TOOL_CALL_LIMIT, nextAgentToolCallLimit)), Math.min(6, Math.max(1, nextAgentToolCallGlobalMultiplier)), JSON.stringify(nextAgentTools), nextTitleGenerationModelId, nextImageToolModelId, nextAlwaysIncludeSettingInfo ? 1 : 0, timestamp);
1194
1194
  this.audit(workId, "work.ai-settings.updated", "work-ai-settings", workId, {
1195
1195
  systemPromptChanged: input.systemPrompt !== undefined,
1196
1196
  systemPromptOverride: nextSystemPromptOverride,
@@ -1204,7 +1204,7 @@ export class Store {
1204
1204
  autoRunStabilityDelayMinutes: Math.min(120, Math.max(1, nextStabilityDelayMinutes)),
1205
1205
  bookSummaryContextPercent: Math.min(90, Math.max(1, nextBookSummaryContextPercent)),
1206
1206
  contextCompactThreshold: Math.min(90, Math.max(50, nextContextCompactThreshold)),
1207
- agentToolCallLimit: Math.min(maximumAgentToolCallLimit, Math.max(5, nextAgentToolCallLimit)),
1207
+ agentToolCallLimit: Math.min(maximumAgentToolCallLimit, Math.max(MIN_AGENT_TOOL_CALL_LIMIT, nextAgentToolCallLimit)),
1208
1208
  agentToolCallGlobalMultiplier: Math.min(6, Math.max(1, nextAgentToolCallGlobalMultiplier)),
1209
1209
  agentTools: nextAgentTools,
1210
1210
  imageToolModelId: nextImageToolModelId,
@@ -1233,8 +1233,8 @@ export class Store {
1233
1233
  this.db.run(`INSERT INTO work_ai_settings (
1234
1234
  work_id, semantic_search_enabled, semantic_embedding_model_id, semantic_rerank_model_id,
1235
1235
  semantic_vector_dimension, semantic_recall_limit, semantic_result_limit,
1236
- semantic_budget_tokens, semantic_channel_weight, updated_at
1237
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1236
+ semantic_budget_tokens, semantic_channel_weight, updated_at, agent_tool_call_limit
1237
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1238
1238
  ON CONFLICT(work_id) DO UPDATE SET
1239
1239
  semantic_search_enabled = excluded.semantic_search_enabled,
1240
1240
  semantic_embedding_model_id = excluded.semantic_embedding_model_id,
@@ -1244,7 +1244,7 @@ export class Store {
1244
1244
  semantic_result_limit = excluded.semantic_result_limit,
1245
1245
  semantic_budget_tokens = excluded.semantic_budget_tokens,
1246
1246
  semantic_channel_weight = excluded.semantic_channel_weight,
1247
- updated_at = excluded.updated_at`, workId, enabled ? 1 : 0, embeddingModelId, rerankModelId, vectorDimension, recallLimit, resultLimit, budgetTokens, channelWeight, timestamp);
1247
+ updated_at = excluded.updated_at`, workId, enabled ? 1 : 0, embeddingModelId, rerankModelId, vectorDimension, recallLimit, resultLimit, budgetTokens, channelWeight, timestamp, Number(current.agentToolCallLimit));
1248
1248
  this.audit(workId, "semantic.settings.updated", "work-ai-settings", workId, {
1249
1249
  enabled,
1250
1250
  embeddingModelId,