@musnows/scriverse 0.8.2 → 0.8.3

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.
@@ -48,7 +48,8 @@ export function formatAiContextUsagePercent(occupiedTokens, contextWindow) {
48
48
  const occupied = tokenCount(occupiedTokens);
49
49
  const window = tokenCount(contextWindow);
50
50
  const percent = window > 0 ? Math.min(100, occupied / window * 100) : 0;
51
- return `${percent.toFixed(1)}%`;
51
+ const roundedDecimal = Number(percent.toFixed(1));
52
+ return roundedDecimal < 10 ? `${roundedDecimal.toFixed(1)}%` : `${Math.round(percent)}%`;
52
53
  }
53
54
 
54
55
  export function normalizeAiContextTokenDistribution(usage) {
@@ -24,7 +24,8 @@ import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/
24
24
  import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
25
25
  import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
26
26
  import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
27
- import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260812-context-usage-remaining-v2";
27
+ import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260819-context-percent-v1";
28
+ import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
28
29
  import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
29
30
  import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
30
31
  import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
@@ -1331,12 +1332,15 @@ function constrainPanelLayout() {
1331
1332
  }
1332
1333
 
1333
1334
  function applyPanelLayout(persist = false) {
1335
+ const phoneClient = isPhoneClient();
1336
+ if (phoneClient && !aiConversationWorkspaceOpen) panelLayout.aiCollapsed = true;
1334
1337
  constrainPanelLayout();
1335
1338
  const app = $("#app");
1336
1339
  app.style.setProperty("--left-panel-width", `${panelLayout.leftWidth}px`);
1337
1340
  app.style.setProperty("--ai-panel-width", `${panelLayout.aiWidth}px`);
1338
1341
  app.classList.toggle("left-panel-collapsed", panelLayout.leftCollapsed);
1339
1342
  app.classList.toggle("ai-panel-collapsed", panelLayout.aiCollapsed);
1343
+ app.classList.toggle("phone-client", phoneClient);
1340
1344
  const mobileViewport = isMobileViewport();
1341
1345
  $("#left-panel-toggle").textContent = mobileViewport ? "›" : (panelLayout.leftCollapsed ? "›" : "‹");
1342
1346
  $("#left-panel-toggle").setAttribute("aria-expanded", String(!panelLayout.leftCollapsed));
@@ -1356,7 +1360,7 @@ function applyPanelLayout(persist = false) {
1356
1360
 
1357
1361
  function ensureAiPanelExpanded() {
1358
1362
  if (!state.work || !canReadPermissionModule(state.work, "ai-chat")) return;
1359
- if (isMobileViewport()) {
1363
+ if (isPhoneClient() || isMobileViewport()) {
1360
1364
  setAiConversationWorkspaceVisible(true);
1361
1365
  return;
1362
1366
  }
@@ -2219,7 +2223,7 @@ function setAiConversationSwitcherVisible(visible) {
2219
2223
  }
2220
2224
 
2221
2225
  function setAiConversationWorkspaceVisible(visible) {
2222
- const mobileWorkspace = isMobileViewport();
2226
+ const mobileWorkspace = isPhoneClient() || isMobileViewport();
2223
2227
  aiConversationWorkspaceOpen = Boolean(visible);
2224
2228
  panelLayout.aiCollapsed = aiConversationWorkspaceOpen ? false : mobileWorkspace;
2225
2229
  $("#app").classList.toggle("ai-workspace-mode", aiConversationWorkspaceOpen);
@@ -3808,8 +3812,7 @@ function renderChapterAnnotations() {
3808
3812
  },
3809
3813
  locate: (annotation) => {
3810
3814
  $("#chapter-annotations-dialog").close();
3811
- paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
3812
- selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
3815
+ revealChapterLines(annotation.startLine, annotation.endLine);
3813
3816
  },
3814
3817
  overlayDialog: $("#chapter-annotations-dialog")
3815
3818
  });
@@ -6385,7 +6388,7 @@ async function runWorkSearch() {
6385
6388
  renderSearchResults(results, query);
6386
6389
  }
6387
6390
 
6388
- function revealChapterSearchLines(startLine, endLine) {
6391
+ function revealChapterLines(startLine, endLine) {
6389
6392
  const start = Math.max(0, Number(startLine) - 1);
6390
6393
  const end = Math.max(start, Number(endLine ?? startLine) - 1);
6391
6394
  const input = $("#chapter-content");
@@ -6440,7 +6443,7 @@ async function openSearchResult(result) {
6440
6443
  if (target.kind === "chapter") {
6441
6444
  await selectChapter(target.id);
6442
6445
  if (state.chapter?.id === target.id && target.startLine) {
6443
- revealChapterSearchLines(target.startLine, target.endLine);
6446
+ revealChapterLines(target.startLine, target.endLine);
6444
6447
  toast(target.startLine === target.endLine ? `已定位到第 ${target.startLine} 行` : `已定位到第 ${target.startLine}-${target.endLine} 行`);
6445
6448
  }
6446
6449
  return;
@@ -9441,10 +9444,10 @@ async function renderWorkChapterComments(page = moduleListPages.comments) {
9441
9444
  bindChapterAnnotationCards($("#module-content"), result.items, {
9442
9445
  refresh: () => renderWorkChapterComments(pageResult.page),
9443
9446
  locate: async (annotation) => {
9444
- await selectChapter(annotation.chapterId);
9447
+ const selected = await selectChapter(annotation.chapterId);
9448
+ if (!selected || String(state.chapter?.id ?? "") !== String(annotation.chapterId)) return;
9445
9449
  await new Promise((resolve) => window.requestAnimationFrame(resolve));
9446
- paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
9447
- selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
9450
+ revealChapterLines(annotation.startLine, annotation.endLine);
9448
9451
  }
9449
9452
  });
9450
9453
  }
@@ -12909,8 +12912,17 @@ function updateVditorWordCount(editor, markdown) {
12909
12912
  counter.title = "当前 Markdown 正文的字数,不计空白字符";
12910
12913
  toolbar.append(counter);
12911
12914
  }
12915
+ let value = counter.querySelector("[data-markdown-word-count-value]");
12916
+ if (!value) {
12917
+ value = document.createElement("span");
12918
+ value.className = "markdown-word-count-value";
12919
+ value.dataset.markdownWordCountValue = "true";
12920
+ const unit = document.createElement("span");
12921
+ unit.textContent = " 字";
12922
+ counter.replaceChildren(value, unit);
12923
+ }
12912
12924
  const count = Array.from(String(markdown ?? "").replace(/\s/gu, "")).length;
12913
- counter.textContent = `${count.toLocaleString("zh-CN")} 字`;
12925
+ value.textContent = count.toLocaleString("zh-CN");
12914
12926
  }
12915
12927
 
12916
12928
  function transformVditorPreview(html) {
@@ -15013,6 +15025,22 @@ async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
15013
15025
  }
15014
15026
  }
15015
15027
 
15028
+ function createAiStreamCharacterCount(value) {
15029
+ const count = document.createElement("span");
15030
+ count.className = "ai-stream-character-count";
15031
+ count.textContent = Math.max(0, Number(value) || 0).toLocaleString("zh-CN");
15032
+ return count;
15033
+ }
15034
+
15035
+ function renderAiStreamingCharacterProgress(meta, visibleCharacters, receivedCharacters) {
15036
+ const visible = Math.max(0, Number(visibleCharacters) || 0);
15037
+ const received = Math.max(visible, Number(receivedCharacters) || 0);
15038
+ const children = ["正在生成 · ", createAiStreamCharacterCount(visible)];
15039
+ if (received > visible) children.push(" / ", createAiStreamCharacterCount(received));
15040
+ children.push(" 字");
15041
+ meta.replaceChildren(...children);
15042
+ }
15043
+
15016
15044
  async function streamChat(requestHolder, body, idempotencyKey) {
15017
15045
  const tab = aiChatTabForRequest(requestHolder.snapshot);
15018
15046
  if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
@@ -15040,9 +15068,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
15040
15068
  if (!aiRequestTargetsCurrentState(requestHolder.snapshot) || !mountAssistantMessage()) return;
15041
15069
  content.innerHTML = renderMarkdown(text);
15042
15070
  const receivedCharacters = progress.visibleCharacters + progress.pendingCharacters;
15043
- meta.textContent = progress.pendingCharacters > 0
15044
- ? `正在生成 · ${progress.visibleCharacters} / ${receivedCharacters} 字`
15045
- : `正在生成 · ${progress.visibleCharacters} 字`;
15071
+ renderAiStreamingCharacterProgress(meta, progress.visibleCharacters, receivedCharacters);
15046
15072
  scrollAiFeedToBottom(feed);
15047
15073
  }
15048
15074
  });
@@ -16919,7 +16945,7 @@ $("#ai-assistant-entry").addEventListener("click", () => {
16919
16945
  return toast("当前账户没有创作助手读取权限", "error");
16920
16946
  }
16921
16947
  setModuleNavExpanded(false);
16922
- if (isMobileViewport()) {
16948
+ if (isPhoneClient() || isMobileViewport()) {
16923
16949
  panelLayout.leftCollapsed = true;
16924
16950
  applyPanelLayout(true);
16925
16951
  setAiConversationWorkspaceVisible(true);
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <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=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1">
13
+ <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=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=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -1210,6 +1210,6 @@
1210
1210
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1211
1211
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1212
1212
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1213
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-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-v1&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-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-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-model-picker-v1"></script>
1213
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-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-v1&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-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-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-model-picker-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v1&feature=phone-client-entry-v1"></script>
1214
1214
  </body>
1215
1215
  </html>
@@ -0,0 +1,6 @@
1
+ export function isPhoneClient(navigatorLike = globalThis.navigator) {
2
+ const mobile = navigatorLike?.userAgentData?.mobile;
3
+ if (typeof mobile === "boolean") return mobile;
4
+ const userAgent = String(navigatorLike?.userAgent ?? "");
5
+ return /(?:Android.*Mobile|iPhone|iPod|IEMobile|Windows Phone|Opera Mini)/iu.test(userAgent);
6
+ }
@@ -561,6 +561,8 @@ input[type="checkbox"][hidden] { display: none; }
561
561
  .panel-resize-handle:hover::after, .panel-resize-handle:focus-visible::after, body.is-panel-resizing .panel-resize-handle::after { background: var(--accent); }
562
562
  body.is-panel-resizing { cursor: col-resize; user-select: none; }
563
563
  .app-shell.left-panel-collapsed { --left-panel-width: 42px !important; }.app-shell.ai-panel-collapsed { --ai-panel-width: 42px !important; }
564
+ .app-shell.phone-client.ai-panel-collapsed:not(.ai-workspace-mode) { --ai-panel-width: 0px !important; }
565
+ .app-shell.phone-client.ai-panel-collapsed:not(.ai-workspace-mode) .ai-panel { display: none; }
564
566
  .app-shell.left-panel-collapsed .left-panel, .app-shell.ai-panel-collapsed .ai-panel { padding: 0; overflow: hidden; }
565
567
  .app-shell.left-panel-collapsed .left-panel > .panel-resize-handle,
566
568
  .app-shell.left-panel-collapsed .left-panel-body > :not(.left-actions),
@@ -1415,7 +1417,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1415
1417
  .chapter-line-number { position: absolute; left: 0; box-sizing: border-box; display: flex; width: 100%; align-items: flex-start; justify-content: center; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; font-variant-numeric: tabular-nums; text-align: center; }
1416
1418
  .chapter-line-number-select { position: relative; display: flex; width: 100%; height: 100%; align-items: flex-start; justify-content: center; padding-inline: 3px; border: 0; background: transparent; color: inherit; font: inherit; font-variant-numeric: tabular-nums; cursor: pointer; }
1417
1419
  .chapter-line-number-select:focus-visible { z-index: 1; outline: 1px solid var(--accent); outline-offset: -1px; }
1418
- .chapter-line-annotation-count { position: absolute; top: -2px; right: 0; z-index: 2; display: grid; min-width: 14px; height: 14px; place-items: center; padding: 0 3px; border: 1px solid var(--panel); border-radius: 999px; background: var(--accent); color: var(--paper); font-family: var(--font-latin), monospace; font-size: 9px; font-weight: 700; line-height: 1; cursor: pointer; box-shadow: 0 1px 3px rgba(49,42,32,.16); }
1420
+ .chapter-line-annotation-count { position: absolute; top: -4px; right: -10px; z-index: 2; display: grid; min-width: 14px; height: 14px; place-items: center; padding: 0 3px; border: 1px solid var(--panel); border-radius: 999px; background: var(--accent); color: var(--paper); font-family: var(--font-latin), monospace; font-size: 9px; font-weight: 700; line-height: 1; cursor: pointer; box-shadow: 0 1px 3px rgba(49,42,32,.16); }
1419
1421
  .chapter-line-annotation-count:hover, .chapter-line-annotation-count:focus-visible { background: var(--accent-dark); }
1420
1422
  .chapter-line-annotation-count:focus-visible { outline: 1px solid var(--accent-dark); outline-offset: 1px; }
1421
1423
  .chapter-line-number:hover { background: rgba(139,61,44,.08); color: var(--accent-dark); }
@@ -2655,6 +2657,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2655
2657
  .message-body .markdown-image img { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
2656
2658
  .message-body .markdown-image small { color: var(--muted); font-size: 9px; line-height: 1.45; text-align: center; }
2657
2659
  .message-meta { margin-top: 8px; color: var(--muted); font-size: 9px; }
2660
+ .message-meta .ai-stream-character-count { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; text-align: right; }
2658
2661
  .ai-process-details { margin: 0 0 10px; overflow: hidden; border: 1px solid var(--line); border-radius: 5px; background: color-mix(in srgb, var(--surface-soft) 82%, transparent); }
2659
2662
  .ai-process-details > summary { display: flex; align-items: center; justify-content: space-between; gap: 9px; padding: 8px 10px; color: var(--muted); cursor: pointer; font-size: 10px; list-style: none; }
2660
2663
  .ai-process-details > summary::-webkit-details-marker { display: none; }
@@ -3321,6 +3324,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3321
3324
  .vditor-editor-host.vditor--dark { --border-color: var(--line); --panel-background-color: var(--surface); --toolbar-background-color: var(--surface-soft); --toolbar-icon-color: var(--muted); --toolbar-icon-hover-color: var(--accent); --textarea-background-color: var(--surface); --textarea-text-color: var(--ink); --heading-border-color: var(--line); --blockquote-color: var(--muted); }
3322
3325
  .vditor-editor-host .vditor-toolbar { display: flex; align-items: center; flex: 0 0 auto; }
3323
3326
  .vditor-editor-host .markdown-word-count { display: inline-flex; align-items: center; align-self: center; min-height: 28px; margin-left: 24px; padding: 0 10px; color: var(--muted); font: 10px/1 var(--font-latin), var(--font-cjk), sans-serif; white-space: nowrap; }
3327
+ .vditor-editor-host .markdown-word-count-value { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; text-align: right; }
3324
3328
  .vditor-editor-host .vditor-content { position: relative; }
3325
3329
  .vditor-editor-host .vditor-line-numbers { display: none; position: absolute; z-index: 1; inset: 0 auto 0 0; width: 42px; padding: 0; overflow: hidden; border-right: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 88%, transparent); color: var(--muted); font: 12px/1.75 var(--font-latin), monospace; text-align: center; user-select: none; pointer-events: none; will-change: transform; }
3326
3330
  .vditor-editor-host.vditor.show-line-numbers .vditor-line-numbers { display: block; }
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = "0.8.2";
1
+ export const APP_VERSION = "0.8.3";
2
2
  export const SCRIVERSE_BETA_COMMIT_ENV = "SCRIVERSE_BETA_COMMIT";
3
3
  export function resolveBetaVersionLabel(environment) {
4
4
  const commit = environment[SCRIVERSE_BETA_COMMIT_ENV]?.trim().toLocaleLowerCase() ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@musnows/scriverse",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Command-line client and local server for the Scriverse long-form fiction workspace",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "musnows",