arcane-os 0.6.2 → 0.7.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.
@@ -42,15 +42,40 @@
42
42
  }
43
43
 
44
44
  .description,
45
- .status{
46
- margin:0;
45
+ .status {
46
+ margin: 0;
47
+ overflow-wrap: anywhere;
48
+ }
49
+
50
+ .transcription-feedback {
51
+ min-width: 0;
52
+ }
53
+
54
+ .stt-activation-progress {
55
+ display: block;
56
+ width: 100%;
57
+ height: .6rem;
58
+ margin-block-start: .7rem;
59
+ accent-color: var(--arcane-action, var(--primary-color));
60
+ }
61
+
62
+ .stt-activation-progress[hidden] {
63
+ display: none;
47
64
  }
48
65
  </style>
49
66
 
50
67
  <section class="voice-transcription">
51
68
  <p id="description" class="description"></p>
52
69
  <section id="transcript" class="transcript"></section>
53
- <p id="status" class="status" role="status" aria-live="polite"></p>
70
+ <div class="transcription-feedback">
71
+ <p id="status" class="status" role="status" aria-live="polite"></p>
72
+ <progress
73
+ id="sttActivationProgress"
74
+ class="stt-activation-progress"
75
+ aria-label="Transcription model activation progress"
76
+ hidden
77
+ ></progress>
78
+ </div>
54
79
  <section class="controls">
55
80
  <button id="start" type="button" disabled></button>
56
81
  <button id="sttActivation" type="button" hidden disabled>Start transcription</button>
@@ -104,6 +129,7 @@
104
129
  const status=host.shadowRoot.querySelector('#status');
105
130
  const startButton=host.shadowRoot.querySelector('#start');
106
131
  const sttActivationButton=host.shadowRoot.querySelector('#sttActivation');
132
+ const sttActivationProgress = host.shadowRoot.querySelector('#sttActivationProgress');
107
133
  const stopButton=host.shadowRoot.querySelector('#stop');
108
134
  const completeButton=host.shadowRoot.querySelector('#complete');
109
135
  const runtimeStateAbortController=new AbortController();
@@ -225,6 +251,7 @@
225
251
  {
226
252
  host,
227
253
  button:sttActivationButton,
254
+ progress: sttActivationProgress,
228
255
  onChange:renderState,
229
256
  eventSource:events
230
257
  }
@@ -407,27 +407,60 @@ function effectiveDashboardVisibility(definitions=[],visibility={}){
407
407
  );
408
408
  }
409
409
 
410
- function formatAIRuntimeProgress(progress,fallback){
411
- const phase=is.string(progress?.phase)&&progress.phase
412
- ?progress.phase
413
- :fallback;
414
- const completed=progress?.completed;
415
- const total=progress?.total;
416
- const unit=is.string(progress?.unit)
417
- ?progress.unit
418
- :'';
419
- if(!is.finite(completed)
420
- ||!is.finite(total)
421
- ||total<=0
422
- ||!unit){
423
- return phase;
410
+ function formatAIRuntimeProgress(progress, fallback) {
411
+ const phase = is.string(progress?.phase) && progress.phase
412
+ ? progress.phase
413
+ : fallback;
414
+ const parts = [phase];
415
+ const message = is.string(progress?.message) && progress.message
416
+ ? progress.message
417
+ : is.string(progress?.stage) && progress.stage
418
+ ? progress.stage
419
+ : '';
420
+ if (message && message !== phase) {
421
+ parts.push(message);
424
422
  }
425
- return `${phase} · ${completed} of ${total} ${unit}`;
423
+ const item = is.string(progress?.item) && progress.item
424
+ ? progress.item
425
+ : is.string(progress?.file) && progress.file
426
+ ? progress.file
427
+ : '';
428
+ if (item && item !== message) {
429
+ parts.push(item);
430
+ }
431
+ const completed = progress?.completed;
432
+ const total = progress?.total;
433
+ const unit = is.string(progress?.unit)
434
+ ? progress.unit
435
+ : '';
436
+ if (is.finite(completed) && is.finite(total) && total > 0 && unit) {
437
+ parts.push(`${completed} of ${total} ${unit}`);
438
+ } else if (is.finite(completed) && completed >= 0 && unit === 'files') {
439
+ parts.push(`${completed} files complete`);
440
+ }
441
+ if (is.finite(progress?.elapsedMs) && progress.elapsedMs >= 0) {
442
+ const elapsedSeconds = Math.floor(progress.elapsedMs / 1000);
443
+ const hours = Math.floor(elapsedSeconds / 3600);
444
+ const minutes = Math.floor(elapsedSeconds % 3600 / 60);
445
+ const seconds = elapsedSeconds % 60;
446
+ parts.push(
447
+ hours > 0
448
+ ? `${hours}h ${minutes}m elapsed`
449
+ : minutes > 0
450
+ ? `${minutes}m ${String(seconds).padStart(2, '0')}s elapsed`
451
+ : `${seconds}s elapsed`
452
+ );
453
+ }
454
+ if (progress?.heartbeat === true) {
455
+ parts.push('Still working');
456
+ }
457
+ return parts.join(' · ');
426
458
  }
427
459
 
428
460
  function createSTTActivationController({
429
461
  host,
430
462
  button,
463
+ progress = null,
431
464
  onChange,
432
465
  EventClass=globalThis.CustomEvent,
433
466
  eventSource=null
@@ -595,6 +628,40 @@ function createSTTActivationController({
595
628
  &&['unloaded','loading','unloading','error'].includes(role.state);
596
629
  }
597
630
 
631
+ function renderProgress() {
632
+ if (!progress) {
633
+ return;
634
+ }
635
+ const active = !destroyed
636
+ && (requestPending || ['loading', 'unloading'].includes(role?.state));
637
+ progress.hidden = !active;
638
+ if (!active) {
639
+ progress.removeAttribute('value');
640
+ progress.removeAttribute('max');
641
+ progress.removeAttribute('aria-valuetext');
642
+ return;
643
+ }
644
+ const completed = role?.progress?.completed;
645
+ const total = role?.progress?.total;
646
+ const unit = role?.progress?.unit;
647
+ if (!is.finite(completed)
648
+ || completed < 0
649
+ || !is.finite(total)
650
+ || total <= 0
651
+ || !is.string(unit)
652
+ || !unit) {
653
+ progress.removeAttribute('value');
654
+ progress.removeAttribute('max');
655
+ } else {
656
+ progress.max = total;
657
+ progress.value = completed;
658
+ }
659
+ progress.setAttribute(
660
+ 'aria-valuetext',
661
+ status()
662
+ );
663
+ }
664
+
598
665
  async function request(nextAction){
599
666
  if(destroyed||requestPending||action()!==nextAction){
600
667
  return false;
@@ -647,6 +714,7 @@ function createSTTActivationController({
647
714
  ||action()!==nextAction){
648
715
  return false;
649
716
  }
717
+ renderProgress();
650
718
  onChange();
651
719
  if(destroyed
652
720
  ||generation!==requestGeneration
@@ -715,6 +783,7 @@ function createSTTActivationController({
715
783
  if(!destroyed&&generation===requestGeneration){
716
784
  requestPending=false;
717
785
  try{
786
+ renderProgress();
718
787
  onChange();
719
788
  }catch(error){
720
789
  arcaneLogging.error('Unable to render STT activation state:',error);
@@ -749,6 +818,7 @@ function createSTTActivationController({
749
818
  requestPending=false;
750
819
  }
751
820
  role=nextRole;
821
+ renderProgress();
752
822
  }
753
823
 
754
824
  function destroy(){
@@ -758,6 +828,7 @@ function createSTTActivationController({
758
828
  destroyed=true;
759
829
  requestGeneration+=1;
760
830
  requestPending=false;
831
+ renderProgress();
761
832
  try{
762
833
  button.removeEventListener('click',activateSelectedSTT);
763
834
  }catch(error){
package/src/cli/main.mjs CHANGED
@@ -35,6 +35,7 @@ const VALUE_OPTIONS=new Set([
35
35
  ]);
36
36
  const FLAG_OPTIONS=new Set([
37
37
  'git',
38
+ 'public',
38
39
  'skip-tests',
39
40
  'dry-run',
40
41
  'require-local-ai',
@@ -60,7 +61,7 @@ Usage:
60
61
  ${CLI_NAME} upgrade [--workspace <directory>] [--app <id>]
61
62
  ${CLI_NAME} doctor [--workspace <directory>] [--arcane-root <directory>]
62
63
  ${CLI_NAME} import-map [--workspace <directory>] [--app <id>]
63
- ${CLI_NAME} dev [--app <id>] [--host 127.0.0.1] [--port 8000] [--sdk-runtime-source <sdk-root>]
64
+ ${CLI_NAME} dev [--app <id>] [--public] [--host <address>] [--port 8000] [--sdk-runtime-source <sdk-root>]
64
65
  ${CLI_NAME} test [--app <id>] [--scope app]
65
66
  ${CLI_NAME} test --scope shared --test-file <repo-relative.test.mjs>
66
67
  ${CLI_NAME} check [--app <id>] [--scope app] [--skip-tests]
@@ -83,6 +84,8 @@ Usage:
83
84
  ${CLI_NAME} mail serve --profile <profile> --from <address> --app <id> --origin <origin> [--allow-to <addresses>] [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
84
85
 
85
86
  Development:
87
+ --public Bind all IPv4 interfaces (0.0.0.0) and print network URLs.
88
+ --host <address> Override the bind address; takes precedence over --public.
86
89
  --sdk-runtime-source <sdk-root> Dev-only live SDK checkout; omitted preserves the workspace runtime mode.
87
90
 
88
91
  Global:
@@ -445,6 +448,9 @@ function operationOptions(command,parsed,cwd){
445
448
  if(values['sdk-runtime-source']!==undefined&&command!=='dev'){
446
449
  usage('--sdk-runtime-source is supported only by dev.');
447
450
  }
451
+ if(flags.has('public')&&command!=='dev'){
452
+ usage('--public is supported only by dev.');
453
+ }
448
454
  if(flags.has('overwrite')&&command!=='bundle'){
449
455
  usage('--overwrite is supported only by bundle.');
450
456
  }
@@ -506,7 +512,7 @@ function operationOptions(command,parsed,cwd){
506
512
  noExtraPositionals(command,positionals);
507
513
  return {
508
514
  ...common,
509
- host:values.host??'127.0.0.1',
515
+ host:values.host??(flags.has('public')?'0.0.0.0':'127.0.0.1'),
510
516
  port:readPort(values.port,8000),
511
517
  ...(values['sdk-runtime-source']===undefined?{}:{
512
518
  sdkRuntimeSourceRoot:path.resolve(cwd,values['sdk-runtime-source'])
@@ -831,6 +837,7 @@ function serverSummary(result){
831
837
  host:result.host,
832
838
  port:result.port,
833
839
  url:result.url,
840
+ ...(result.networkUrls===undefined?{}:{networkUrls:result.networkUrls}),
834
841
  ...(result.callerAuthentication
835
842
  ?{callerAuthentication:result.callerAuthentication}
836
843
  :{}),
@@ -841,7 +848,11 @@ function serverSummary(result){
841
848
  }
842
849
 
843
850
  async function waitForServer(result,signal,reporter){
844
- reporter.emit('server.ready',serverSummary(result),`Development server ready at ${result.url}`);
851
+ const readyMessage=[
852
+ `Development server ready at ${result.url}`,
853
+ ...(result.networkUrls??[]).map(function networkAddress(url){return `Network: ${url}`;})
854
+ ].join('\n');
855
+ reporter.emit('server.ready',serverSummary(result),readyMessage);
845
856
  if(result.lifecycle&&is.function(result.lifecycle.then)){
846
857
  const abort=()=>{
847
858
  void Promise.resolve().then(()=>result.close?.()).catch(()=>{});
@@ -2,6 +2,7 @@ import Is from 'strong-type';
2
2
  import {constants as FS_CONSTANTS} from 'node:fs';
3
3
  import {lstat,open,readFile,realpath} from 'node:fs/promises';
4
4
  import http from 'node:http';
5
+ import os from 'node:os';
5
6
  import path from 'node:path';
6
7
  import {resolveWorkspace} from './workspace.mjs';
7
8
  import {createEventQueue} from './event-queue.mjs';
@@ -489,6 +490,29 @@ function listen(server,{host,port,signal}){
489
490
  });
490
491
  }
491
492
 
493
+ function browserHostname(host){
494
+ return host.includes(':')?`[${host}]`:host;
495
+ }
496
+
497
+ function networkUrlsForAddress(address,startPath){
498
+ if(address.address==='127.0.0.1'||address.address==='::1')return [];
499
+ const allIPv4=address.address==='0.0.0.0';
500
+ const allInterfaces=address.address==='::';
501
+ const urls=new Set();
502
+ for(const entries of Object.values(os.networkInterfaces())){
503
+ for(const entry of entries??[]){
504
+ if(entry.internal)continue;
505
+ // Scoped IPv6 addresses need the receiving device's interface,
506
+ // so they cannot provide a portable browser link for another device.
507
+ if(entry.family==='IPv6'&&entry.scopeid)continue;
508
+ if(allIPv4&&entry.family!=='IPv4')continue;
509
+ if(!allIPv4&&!allInterfaces&&entry.address!==address.address)continue;
510
+ urls.add(`http://${browserHostname(entry.address)}:${address.port}${startPath}`);
511
+ }
512
+ }
513
+ return [...urls];
514
+ }
515
+
492
516
  async function startOwnedDevServer({
493
517
  workspaceRoot=process.cwd(),
494
518
  appId,
@@ -504,8 +528,8 @@ async function startOwnedDevServer({
504
528
  if(sdkRuntimeSourceRoot!==undefined&&mode!=='source'){
505
529
  fail('sdkRuntimeSourceRoot is supported only in source development mode.','ARCANE_USAGE');
506
530
  }
507
- if(host!=='127.0.0.1'&&host!=='::1'){
508
- fail('Development server host must be a numeric loopback address (127.0.0.1 or ::1).','ARCANE_POLICY_DENIED');
531
+ if(!is.string(host)||!host.trim()){
532
+ fail('Development server host must be a nonempty string.','ARCANE_USAGE');
509
533
  }
510
534
  if(!is.integer(port)||port<0||port>65535)fail('port must be an integer from 0 through 65535.','ARCANE_USAGE');
511
535
  const requestedRuntimeMode=mode==='source'&&sdkRuntimeSourceRoot!==undefined
@@ -631,7 +655,9 @@ async function startOwnedDevServer({
631
655
  server.close();
632
656
  fail('Development server did not expose a TCP address.');
633
657
  }
634
- const visibleHost=address.family==='IPv6'?`[${address.address}]`:address.address;
658
+ const visibleHost=address.address==='0.0.0.0'||address.address==='::'
659
+ ?'localhost'
660
+ :browserHostname(address.address);
635
661
  const endpoint=new URL(`http://${visibleHost}:${address.port}`);
636
662
  const origin=endpoint.origin;
637
663
  const cleanUrl=`${origin}${routeSet.startPath}`;
@@ -728,17 +754,20 @@ async function startOwnedDevServer({
728
754
  origin,
729
755
  cleanUrl,
730
756
  url,
757
+ networkUrls:[],
731
758
  close,
732
759
  closed:lifecycle,
733
760
  lifecycle
734
761
  };
735
762
  try{
763
+ result.networkUrls=networkUrlsForAddress(address,routeSet.startPath);
736
764
  await events.send({
737
765
  type:'server.started',
738
766
  mode,
739
767
  host:result.host,
740
768
  port:result.port,
741
769
  url,
770
+ networkUrls:result.networkUrls,
742
771
  appId:result.appId,
743
772
  ...(routeSet.runtime?{
744
773
  runtimeMode:routeSet.runtime.mode,