arcane-os 0.39.0 → 0.39.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.39.1
4
+
5
+ - Preserve the Markdown editor's current source viewport and selection during
6
+ configuration, programmatic value changes, preview rendering and toolbar
7
+ formatting. Keep the live textarea attached; only the preview follows edits.
8
+ Native typing, manual scrolling, complete content and save behavior remain
9
+ independent and unchanged.
10
+ - Generate default Chat filenames with a timestamp and an independent native
11
+ random identifier so sessions created in the same clock tick do not share a
12
+ storage name. Preserve explicit and saved filenames, existing stored content,
13
+ persistence format and request-only `persist:false` behavior.
14
+ - Detect continuous-capture activity using fixed 20 ms RMS audio windows so
15
+ brief background peaks do not indefinitely postpone quiet-gap segments.
16
+ Preserve complete original samples, pre-roll, periodic clip adjacency and
17
+ partial final flush. `activityThreshold` now measures sustained RMS level;
18
+ callers retain control of sensitivity for softer input.
19
+
3
20
  ## 0.39.0
4
21
 
5
22
  - Add application-owned assistant display names to new Chat turns through
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
20
20
  and browser run contracts while retaining their selected application layout.
21
21
 
22
- This checkout defines the `0.39.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.39.1` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -535,7 +535,7 @@
535
535
  ],
536
536
  "availability": "Browser and supported native WebViews",
537
537
  "transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
538
- "normalization": "Editor values and optional presentation behavior normalized; injected save result mixed. fit and followPreview default false, preserve omitted prior configuration, and accept explicit false resets; data-fit and data-follow-preview select the initial options. Fit requires an application-bounded host and provides independent input/preview scrolling in roomy columns or narrow stacked panes without changing the application shell. Following responds to body typing/format edits, leaves manual preview scrolling independent until the next edit, and does not treat title edits or programmatic value replacement as follow requests. Public parts editor/title/toolbar/input/preview/actions/status/save-action allow application styling while the title stays native plain text. Existing controls, complete save payload, save cancellation, and destruction behavior remain."
538
+ "normalization": "Editor values and optional presentation behavior normalized; injected save result mixed. fit and followPreview default false, preserve omitted prior configuration, and accept explicit false resets; data-fit and data-follow-preview select the initial options. Fit requires an application-bounded host and provides independent input/preview scrolling in roomy columns or narrow stacked panes without changing the application shell. Following responds to body typing/format edits, leaves manual preview scrolling independent until the next edit, and does not treat title edits or programmatic value replacement as follow requests. Preview rendering preserves the current source viewport and selection; configuration/value replacement retain source offsets and applicable selection positions, and layout changes never reattach the textarea. Formatting selects the formatted text without panning the source or its ancestors. Native typing, keyboard selection, and manual source scrolling remain independent without deferred scroll restoration. Public parts editor/title/toolbar/input/preview/actions/status/save-action allow application styling while the title stays native plain text. Existing controls, complete save payload, save cancellation, and destruction behavior remain."
539
539
  },
540
540
  {
541
541
  "file": "runtime/arcane/components/media-embed.html",
@@ -9,7 +9,7 @@
9
9
  "modules": [
10
10
  {"file":"runtime/arcane/entities/ApiModelRecord.js","exports":["default"],"availability":"cross-host","normalization":"normalized"},
11
11
  {"file":"runtime/arcane/entities/Calculation.js","exports":["default"],"availability":"cross-host","normalization":"normalized"},
12
- {"file":"runtime/arcane/entities/Chat.js","exports":["default"],"availability":"browser/native-webview","normalization":"Provider-facing recurring messages preserve raw structural protocol only for the one active matching continuation, then replace settled exchanges with complete ordinary visible content; transcript records remain sanitized and readable, including ui_hidden rows. aiName defaults to empty; addAIMessage and addTurn capture an optional exact nonblank application-owned name on new assistant records only. Provider names do not establish display attribution; assistant display names stay out of messages and memory extraction, and existing records are never renamed or rewritten. ordered parallel structural calls require one exact nonblank result for every pending ID in one atomic turn, with rollback-safe persistence."},
12
+ {"file":"runtime/arcane/entities/Chat.js","exports":["default"],"availability":"browser/native-webview","normalization":"New default filenames combine the timestamp with an independent random identifier; explicit and saved filenames remain unchanged. Provider-facing recurring messages preserve raw structural protocol only for the one active matching continuation, then replace settled exchanges with complete ordinary visible content; transcript records remain sanitized and readable, including ui_hidden rows. aiName defaults to empty; addAIMessage and addTurn capture an optional exact nonblank application-owned name on new assistant records only. Provider names do not establish display attribution; assistant display names stay out of messages and memory extraction, and existing records are never renamed or rewritten. ordered parallel structural calls require one exact nonblank result for every pending ID in one atomic turn, with rollback-safe persistence."},
13
13
  {"file":"runtime/arcane/entities/CommunicationMessage.js","exports":["communicationChannels","default"],"availability":"cross-host","normalization":"normalized"},
14
14
  {"file":"runtime/arcane/entities/CommunicationThread.js","exports":["default"],"availability":"cross-host","normalization":"normalized"},
15
15
  {"file":"runtime/arcane/entities/Document.js","exports":["default"],"availability":"browser/native-webview","normalization":"mixed"},
@@ -1393,7 +1393,7 @@
1393
1393
  "summary": "Segments microphone samples on the audio rendering thread and transfers complete mono Float32 WAV clips to ContinuousVoiceCapture.",
1394
1394
  "availability": "AudioWorkletGlobalScope",
1395
1395
  "protocol": "AudioWorklet processor arcane-continuous-voice-capture and its owner-local MessagePort",
1396
- "normalization": "Rolling pre-roll, amplitude activity, quiet and periodic boundaries, sequence, complete WAV audio, and stop acknowledgement are owned here. Silence does not emit clips; adjacent periodic clips have no repeated samples.",
1396
+ "normalization": "Rolling pre-roll, fixed 20 ms RMS activity windows independent of callback length, audio-frame quiet duration, periodic boundaries, sequence, complete original WAV audio, and stop acknowledgement are owned here. activityThreshold is RMS amplitude, not peak amplitude. Stop classifies a partial final window using its actual samples and retains softer trailing audio in the active clip. Inactive capture emits no clips; adjacent periodic clips have no repeated samples.",
1397
1397
  "surface": "No ESM exports. ContinuousVoiceCapture loads this static worklet, supplies processorOptions, receives segment messages, and requests stop followed by the stopped acknowledgement."
1398
1398
  },
1399
1399
  {
@@ -1067,6 +1067,14 @@ programmatic value replacement, and ordinary preview scrolling do not request
1067
1067
  edit-position following. Moving the selection alone does not trigger following,
1068
1068
  and following never scrolls the input pane or containing page. The title
1069
1069
  remains a native plain-text input.
1070
+ Preview rendering preserves the source pane's current horizontal and vertical
1071
+ scroll offsets and selection. Value replacement and reconfiguration retain
1072
+ those offsets and the selection positions that still fit the new text; layout
1073
+ changes move only the preview, keeping the live textarea attached. Toolbar
1074
+ formatting focuses the source without scrolling its ancestors, selects the
1075
+ formatted text, and preserves the source viewport. Native typing, keyboard
1076
+ selection, and manual source scrolling retain their ordinary behavior; there
1077
+ is no scroll lock or deferred restoration of an earlier source position.
1070
1078
  Following uses the shared [Markdown block mapping](runtime-modules.md#mdjs);
1071
1079
  lists, quotes, and tables follow their block, while invisible source positions
1072
1080
  use a nearby available rendered block rather than exact glyph geometry.
@@ -73,6 +73,11 @@ console.log(new Calculation({expression: '2 + 2', result: 4}).toJSON());
73
73
  and optional app-scoped DBOPFS persistence. It installs no independent native
74
74
  authority; AI and storage dependencies must be available to the host.
75
75
 
76
+ Each new chat receives a timestamp plus independently generated random identifier
77
+ in its default `.jsonl` filename, so chats created during the same clock tick do
78
+ not share a storage key. Explicit filenames and existing saved files remain
79
+ unchanged; no saved records are renamed or migrated.
80
+
76
81
  `messages` returns provider-facing recurring context. An unresolved structural
77
82
  call and its matching results remain raw only through their one active provider
78
83
  continuation. Once that continuation settles, `messages` replaces the protocol
@@ -4069,7 +4069,7 @@ Load with a stylesheet link before rendering uPlot charts.
4069
4069
  ### Overview
4070
4070
 
4071
4071
  Internal audio processor for [`ContinuousVoiceCapture.js`](#continuousvoicecapturejs).
4072
- It keeps rolling pre-roll, detects audio amplitude, preserves complete active
4072
+ It keeps rolling pre-roll, detects RMS audio level, preserves complete active
4073
4073
  audio through quiet and periodic cuts, and encodes mono Float32 WAV clips on
4074
4074
  the audio rendering thread. It performs no semantic speech detection or STT.
4075
4075
 
@@ -4079,6 +4079,22 @@ No ESM exports. The capture owner loads this static file with
4079
4079
  `audioContext.audioWorklet.addModule()` and creates the
4080
4080
  `arcane-continuous-voice-capture` processor with
4081
4081
  `{preRollMs,quietMs,chunkMs,activityThreshold}` in `processorOptions`.
4082
+ `activityThreshold` measures root-mean-square amplitude over fixed 20 ms
4083
+ windows counted from the audio sample rate, independently of browser callback
4084
+ lengths. Brief peaks do not reset the quiet duration when the window's RMS
4085
+ remains below that threshold. This is an RMS threshold, not a peak threshold:
4086
+ a sine wave with peak amplitude `0.025` can remain below the default `0.02`
4087
+ RMS threshold. Applications can select the existing threshold for their audio
4088
+ level; no relative-noise or semantic speech classifier is implied.
4089
+
4090
+ Classification selects segment boundaries without modifying the captured
4091
+ samples. Quiet duration advances by audio frames, pre-roll remains complete,
4092
+ and periodic clips remain adjacent. Stop classifies any partial final window
4093
+ using its actual samples and flushes the final active clip, including softer
4094
+ trailing audio before the quiet boundary. Analysis adds at most one 20 ms
4095
+ window of capture-to-segmentation delay; model processing is independently
4096
+ owned by the transcription queue.
4097
+
4082
4098
  The owner sends `{type:'stop'}`. The processor transfers each complete
4083
4099
  `{type:'segment',audio:ArrayBuffer,sampleRate,sequence,reason,durationMs}`
4084
4100
  and finally `{type:'stopped'}`. Each complete WAV includes its own format,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.39.0",
3
+ "version": "0.39.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -224,6 +224,7 @@
224
224
  if(destroyed){
225
225
  return false;
226
226
  }
227
+ const sourceView = captureSourceView();
227
228
  const setInitialValue=Object.prototype.hasOwnProperty.call(
228
229
  input||{},
229
230
  'initialValue'
@@ -240,9 +241,12 @@
240
241
  }
241
242
  if(setInitialValue){
242
243
  supersedeSave('markdown-content-replaced');
243
- textarea.value=options.initialValue;
244
+ if (textarea.value !== options.initialValue) {
245
+ textarea.value = options.initialValue;
246
+ }
244
247
  }
245
248
  updatePreview();
249
+ restoreSourceView(sourceView);
246
250
  return getOptions();
247
251
  }
248
252
 
@@ -250,13 +254,14 @@
250
254
  host.dataset.fit=String(options.fit);
251
255
  editor.classList.toggle('fit',options.fit);
252
256
  editor.classList.toggle('without-preview',!options.showPreview);
253
- // Keyboard order follows the displayed order in each layout.
254
- if(options.fit){
255
- editor.insertBefore(toolbar,preview);
256
- editor.insertBefore(textarea,preview);
257
- }else{
258
- editor.insertBefore(preview,toolbar);
259
- editor.insertBefore(textarea,toolbar.nextSibling);
257
+ // Move only the preview when the layout changes. Reattaching the live
258
+ // textarea during ordinary configuration would disturb focus/selection.
259
+ if (options.fit) {
260
+ if (textarea.nextSibling !== preview) {
261
+ editor.insertBefore(preview, textarea.nextSibling);
262
+ }
263
+ } else if (preview.nextSibling !== toolbar) {
264
+ editor.insertBefore(preview, toolbar);
260
265
  }
261
266
  titleInput.placeholder=options.labels.titlePlaceholder;
262
267
  titleInput.setAttribute('aria-label',options.labels.titlePlaceholder);
@@ -331,6 +336,36 @@
331
336
  previewFollowPending=false;
332
337
  }
333
338
 
339
+ function captureSourceView() {
340
+ return {
341
+ scrollTop: textarea.scrollTop,
342
+ scrollLeft: textarea.scrollLeft,
343
+ selectionStart: textarea.selectionStart,
344
+ selectionEnd: textarea.selectionEnd,
345
+ selectionDirection: textarea.selectionDirection
346
+ };
347
+ }
348
+
349
+ function restoreSourceView(view) {
350
+ if (textarea.selectionStart !== view.selectionStart ||
351
+ textarea.selectionEnd !== view.selectionEnd ||
352
+ textarea.selectionDirection !== view.selectionDirection) {
353
+ textarea.setSelectionRange(
354
+ view.selectionStart,
355
+ view.selectionEnd,
356
+ view.selectionDirection
357
+ );
358
+ }
359
+ // Native value/focus/selection updates may pan the textarea. Restore
360
+ // this operation's viewport, never a position retained across user input.
361
+ if (textarea.scrollTop !== view.scrollTop) {
362
+ textarea.scrollTop = view.scrollTop;
363
+ }
364
+ if (textarea.scrollLeft !== view.scrollLeft) {
365
+ textarea.scrollLeft = view.scrollLeft;
366
+ }
367
+ }
368
+
334
369
  function updatePreview(follow=false){
335
370
  if(destroyed){
336
371
  return;
@@ -340,6 +375,7 @@
340
375
  }
341
376
  previewFrame=0;
342
377
  previewFollowPending=false;
378
+ const sourceView = captureSourceView();
343
379
  const scrollTop=preview.scrollTop;
344
380
  const scrollLeft=preview.scrollLeft;
345
381
  const markdown=new MD(textarea.value,{sourceMap:options.followPreview});
@@ -355,6 +391,7 @@
355
391
  if(follow&&options.followPreview&&options.showPreview){
356
392
  followTypingPosition(markdown.sourceMap);
357
393
  }
394
+ restoreSourceView(sourceView);
358
395
  }
359
396
 
360
397
  function followTypingPosition(sourceMap){
@@ -434,6 +471,7 @@
434
471
  return;
435
472
  }
436
473
 
474
+ const sourceView = captureSourceView();
437
475
  const result=applyMarkdownFormat(
438
476
  textarea.value,
439
477
  textarea.selectionStart,
@@ -441,8 +479,12 @@
441
479
  format
442
480
  );
443
481
  textarea.value=result.value;
444
- textarea.focus();
445
- textarea.setSelectionRange(result.selectionStart,result.selectionEnd);
482
+ textarea.focus(
483
+ {preventScroll: true}
484
+ );
485
+ sourceView.selectionStart = result.selectionStart;
486
+ sourceView.selectionEnd = result.selectionEnd;
487
+ restoreSourceView(sourceView);
446
488
  contentChanged();
447
489
  }
448
490
 
@@ -524,8 +566,13 @@
524
566
  return false;
525
567
  }
526
568
  supersedeSave('markdown-content-replaced');
527
- textarea.value=`${value??''}`;
569
+ const sourceView = captureSourceView();
570
+ const nextValue = `${value ?? ''}`;
571
+ if (textarea.value !== nextValue) {
572
+ textarea.value = nextValue;
573
+ }
528
574
  updatePreview();
575
+ restoreSourceView(sourceView);
529
576
  return textarea.value;
530
577
  }
531
578
 
@@ -433,8 +433,8 @@ class ChatEntity{
433
433
  /**
434
434
  * Creates a new chat session.
435
435
  *
436
- * A unique file name is generated automatically using
437
- * the current timestamp.
436
+ * The default file name combines the current timestamp with an
437
+ * independent random identifier, including across concurrent tabs.
438
438
  *
439
439
  * Optionally accepts a system prompt which will be added
440
440
  * as the first message in the conversation.
@@ -444,7 +444,8 @@ class ChatEntity{
444
444
  */
445
445
  constructor(systemPrompt=''){
446
446
 
447
- this.fileName=`chat-${Date.now()}.jsonl`;
447
+ const identity=globalThis.crypto.getRandomValues(new Uint32Array(4)).join('-');
448
+ this.fileName=`chat-${Date.now()}-${identity}.jsonl`;
448
449
 
449
450
  if(systemPrompt){
450
451
  this.#messages.push(
@@ -7,6 +7,9 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
7
7
  #quietFrames;
8
8
  #chunkFrames;
9
9
  #activityThreshold;
10
+ #analysisSamples;
11
+ #analysisCount = 0;
12
+ #analysisEnergy = 0;
10
13
  #active = false;
11
14
  #hasActivity = false;
12
15
  #quietCount = 0;
@@ -25,6 +28,9 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
25
28
  this.#quietFrames = Math.max(1, Math.round(configuration.quietMs * sampleRate / 1000));
26
29
  this.#chunkFrames = Math.max(1, Math.round(configuration.chunkMs * sampleRate / 1000));
27
30
  this.#activityThreshold = configuration.activityThreshold;
31
+ // Measure sustained loudness, not individual peaks. The audio clock
32
+ // owns this fixed 20 ms window, independently of callback block sizes.
33
+ this.#analysisSamples = new Float32Array(Math.max(1, Math.round(sampleRate * 0.02)));
28
34
  this.port.onmessage = this.#receiveControl.bind(this);
29
35
  }
30
36
 
@@ -32,6 +38,7 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
32
38
  if (this.#stopped || event.data.type !== 'stop') return;
33
39
  this.#stopped = true;
34
40
  try {
41
+ this.#flushAnalysis();
35
42
  this.#emit('stop');
36
43
  this.#release();
37
44
  this.port.postMessage(
@@ -50,15 +57,33 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
50
57
  const samples = inputs[0]?.[0];
51
58
  if (!samples) return true;
52
59
  try {
53
- for (const sample of samples) this.#capture(sample);
60
+ for (const sample of samples) {
61
+ this.#analysisSamples[this.#analysisCount] = sample;
62
+ this.#analysisCount += 1;
63
+ this.#analysisEnergy += sample * sample;
64
+ if (this.#analysisCount === this.#analysisSamples.length) {
65
+ this.#flushAnalysis();
66
+ }
67
+ }
54
68
  } catch (error) {
55
69
  this.#fail(error);
56
70
  }
57
71
  return !this.#stopped;
58
72
  }
59
73
 
60
- #capture(sample) {
61
- const activity = Math.abs(sample) > this.#activityThreshold;
74
+ #flushAnalysis() {
75
+ if (!this.#analysisCount) return;
76
+ const activity = Math.sqrt(this.#analysisEnergy / this.#analysisCount) > this.#activityThreshold;
77
+ // Classification selects boundaries only. Every original sample still
78
+ // enters capture in order, including a partial final analysis window.
79
+ for (let index = 0; index < this.#analysisCount; index += 1) {
80
+ this.#capture(this.#analysisSamples[index], activity);
81
+ }
82
+ this.#analysisCount = 0;
83
+ this.#analysisEnergy = 0;
84
+ }
85
+
86
+ #capture(sample, activity) {
62
87
  if (!this.#active) {
63
88
  if (!activity) {
64
89
  this.#retainPreRoll(sample);
@@ -185,6 +210,9 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
185
210
  this.#view = null;
186
211
  this.#preRoll = new Float32Array(0);
187
212
  this.#preRollLength = 0;
213
+ this.#analysisSamples = new Float32Array(0);
214
+ this.#analysisCount = 0;
215
+ this.#analysisEnergy = 0;
188
216
  this.#sampleCount = 0;
189
217
  this.#hasActivity = false;
190
218
  this.#active = false;