@musnows/scriverse 1.0.7 → 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.
@@ -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/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = "1.0.7";
1
+ export const APP_VERSION = "1.0.8";
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": "1.0.7",
3
+ "version": "1.0.8",
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",