dsh-plugin-local-infer 0.1.2 → 0.1.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.
Files changed (100) hide show
  1. package/README.md +10 -2
  2. package/docs/zh/README.md +10 -2
  3. package/dsh/client.js +669 -191
  4. package/package.json +6 -1
  5. package/packages/core/lib/adapter.d.ts +44 -2
  6. package/packages/core/lib/deps.d.ts +4 -3
  7. package/packages/core/lib/index.d.ts +1 -1
  8. package/packages/core/lib/registrar.d.ts +5 -5
  9. package/packages/core/lib/registrar.js +3 -3
  10. package/packages/core/package.json +1 -1
  11. package/packages/providers/lib/acquire.d.ts +5 -3
  12. package/packages/providers/lib/acquire.js +30 -9
  13. package/packages/providers/lib/adapters.d.ts +19 -209
  14. package/packages/providers/lib/adapters.js +13 -886
  15. package/packages/providers/lib/deps.d.ts +24 -2
  16. package/packages/providers/lib/deps.js +23 -4
  17. package/packages/providers/lib/engines/hyperqwenAdapter.d.ts +38 -0
  18. package/packages/providers/lib/engines/hyperqwenAdapter.js +105 -0
  19. package/packages/providers/lib/{infernix.js → engines/infernix.js} +1 -1
  20. package/packages/providers/lib/engines/infernixAdapter.d.ts +53 -0
  21. package/packages/providers/lib/engines/infernixAdapter.js +202 -0
  22. package/packages/providers/lib/engines/llamacppAdapter.d.ts +51 -0
  23. package/packages/providers/lib/engines/llamacppAdapter.js +232 -0
  24. package/packages/providers/lib/{ninfer.js → engines/ninfer.js} +1 -1
  25. package/packages/providers/lib/engines/ninferAdapter.d.ts +82 -0
  26. package/packages/providers/lib/engines/ninferAdapter.js +324 -0
  27. package/packages/providers/lib/engines/strata.d.ts +53 -0
  28. package/packages/providers/lib/engines/strata.js +197 -0
  29. package/packages/providers/lib/engines/strataAdapter.d.ts +98 -0
  30. package/packages/providers/lib/engines/strataAdapter.js +427 -0
  31. package/packages/providers/lib/engines.d.ts +5 -4
  32. package/packages/providers/lib/engines.js +10 -10
  33. package/packages/providers/lib/guidelines.js +1 -1
  34. package/packages/providers/lib/index.d.ts +14 -9
  35. package/packages/providers/lib/index.js +17 -7
  36. package/packages/providers/lib/model-defaults.d.ts +5 -4
  37. package/packages/providers/lib/model-defaults.js +22 -4
  38. package/packages/providers/lib/platform.js +7 -1
  39. package/packages/providers/package.json +1 -1
  40. package/packages/service/lib/index.d.ts +13 -4
  41. package/packages/service/lib/index.js +79 -5
  42. package/packages/service/lib/types.d.ts +5 -4
  43. package/packages/service/package.json +1 -1
  44. package/packages/tool/package.json +1 -1
  45. package/packages/ui/lib/config.d.ts +79 -0
  46. package/packages/ui/lib/config.js +235 -0
  47. package/packages/ui/lib/deps.d.ts +3 -3
  48. package/packages/ui/lib/ops/engine.d.ts +108 -0
  49. package/packages/ui/lib/ops/engine.js +493 -0
  50. package/packages/ui/lib/ops/hf.d.ts +27 -0
  51. package/packages/ui/lib/ops/hf.js +60 -0
  52. package/packages/ui/lib/ops/logs.d.ts +23 -0
  53. package/packages/ui/lib/ops/logs.js +58 -0
  54. package/packages/ui/lib/ops/model.d.ts +79 -0
  55. package/packages/ui/lib/ops/model.js +130 -0
  56. package/packages/ui/lib/request.d.ts +106 -0
  57. package/packages/ui/lib/request.js +80 -0
  58. package/packages/ui/lib/route.d.ts +88 -420
  59. package/packages/ui/lib/route.js +111 -1843
  60. package/packages/ui/lib/routes/chat-template.d.ts +14 -0
  61. package/packages/ui/lib/routes/chat-template.js +37 -0
  62. package/packages/ui/lib/routes/deps.d.ts +16 -0
  63. package/packages/ui/lib/routes/deps.js +27 -0
  64. package/packages/ui/lib/routes/download.d.ts +16 -0
  65. package/packages/ui/lib/routes/download.js +48 -0
  66. package/packages/ui/lib/routes/get.d.ts +18 -0
  67. package/packages/ui/lib/routes/get.js +57 -0
  68. package/packages/ui/lib/routes/install.d.ts +16 -0
  69. package/packages/ui/lib/routes/install.js +36 -0
  70. package/packages/ui/lib/routes/launch.d.ts +18 -0
  71. package/packages/ui/lib/routes/launch.js +50 -0
  72. package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
  73. package/packages/ui/lib/routes/provider-delete.js +37 -0
  74. package/packages/ui/lib/routes/register.d.ts +16 -0
  75. package/packages/ui/lib/routes/register.js +28 -0
  76. package/packages/ui/lib/routes/save.d.ts +18 -0
  77. package/packages/ui/lib/routes/save.js +139 -0
  78. package/packages/ui/lib/routes/stop.d.ts +16 -0
  79. package/packages/ui/lib/routes/stop.js +29 -0
  80. package/packages/ui/lib/routes/summary.d.ts +15 -0
  81. package/packages/ui/lib/routes/summary.js +26 -0
  82. package/packages/ui/lib/routes/unregister.d.ts +14 -0
  83. package/packages/ui/lib/routes/unregister.js +26 -0
  84. package/packages/ui/lib/viewmodels/deps.d.ts +31 -0
  85. package/packages/ui/lib/viewmodels/deps.js +89 -0
  86. package/packages/ui/lib/viewmodels/depsHelpers.d.ts +28 -0
  87. package/packages/ui/lib/viewmodels/depsHelpers.js +33 -0
  88. package/packages/ui/lib/viewmodels/summary.d.ts +140 -0
  89. package/packages/ui/lib/viewmodels/summary.js +225 -0
  90. package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
  91. package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
  92. package/packages/ui/package.json +1 -1
  93. package/packages/providers/lib/strata.d.ts +0 -15
  94. package/packages/providers/lib/strata.js +0 -14
  95. /package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen.d.ts} +0 -0
  96. /package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen.js} +0 -0
  97. /package/packages/providers/lib/{infernix.d.ts → engines/infernix.d.ts} +0 -0
  98. /package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp.d.ts} +0 -0
  99. /package/packages/providers/lib/{llamacpp.js → engines/llamacpp.js} +0 -0
  100. /package/packages/providers/lib/{ninfer.d.ts → engines/ninfer.d.ts} +0 -0
package/dsh/client.js CHANGED
@@ -83,7 +83,7 @@ window.__ModuleLoader__.load({
83
83
  run: 'Run',
84
84
  running: 'running...',
85
85
  get: 'Get',
86
- getting: 'getting...',
86
+ getting: 'Getting...',
87
87
  getFailed: 'get failed',
88
88
  allowRun: 'Allow one-click install (sudo)',
89
89
  depsSatisfied: 'All dependencies satisfied.',
@@ -149,6 +149,7 @@ window.__ModuleLoader__.load({
149
149
  registerNeedsSave: 'Save the Provider configuration before registering with DSH.',
150
150
  logs: 'Logs',
151
151
  logLocation: 'Log location',
152
+ resize: 'resize',
152
153
  entryLaunch: 'Launch',
153
154
  entryStop: 'Stop',
154
155
  entryLogs: 'Logs',
@@ -284,6 +285,7 @@ window.__ModuleLoader__.load({
284
285
  registerNeedsSave: '注册到 DSH 之前请先保存该提供方配置。',
285
286
  logs: '日志',
286
287
  logLocation: '日志位置',
288
+ resize: '调整大小',
287
289
  entryLaunch: '启动',
288
290
  entryStop: '停止',
289
291
  entryLogs: '日志',
@@ -459,15 +461,23 @@ window.__ModuleLoader__.load({
459
461
  border: 'none',
460
462
  });
461
463
 
462
- // The per-entry launch state machine: the card's Launch control lifecycle.
463
- // The states are the button's reads: idle (a plain Launch), the four
464
- // rebuild phases (prereqs, download, build, launch), running (a Stop), and
464
+ // The per-entry provider state machine: the card's Launch/Stop control
465
+ // lifecycle. The states are the button's reads: idle (a plain Launch), the
466
+ // four rebuild phases (prereqs, download, build, launch), running (a
467
+ // Stop), stopping (a disabled throbber while the stop is in flight), and
465
468
  // failed (a red Logs). launchNextState is the hardened transition: a step
466
469
  // event only advances forward through the rebuild phases (a stale or
467
470
  // out-of-order step is a no-op), a healthy result settles to running, a
468
- // failed result settles to failed, and a stop or a break (the Logs control,
469
- // a navigation away) resets to idle. An event that names no legal next
470
- // state leaves the control where it is, so a late event never corrupts it.
471
+ // failed result settles to failed, a stop moves a running (or idle, a
472
+ // provider running per the summary with no machine entry) engine to
473
+ // stopping, a confirmed stop settles to idle, a stop that is still running
474
+ // at the deadline settles to failed with the cause 'stop', and a break
475
+ // (the Logs control, a navigation away) resets: a launch failure reads as
476
+ // Launch (idle), a stop timeout reads as Stop (running), because the
477
+ // engine is probably still up. The machine takes the entry ({status,
478
+ // cause}) and returns the next entry, so the cause-aware reset stays in
479
+ // one place. An event that names no legal next state leaves the control
480
+ // where it is, so a late event never corrupts it.
471
481
  var LAUNCH_STEP_ORDER = { prereqs: 0, download: 1, build: 2, launch: 3 };
472
482
  var LAUNCH_PHASE_STATES = ['prereqs', 'download', 'build', 'launch'];
473
483
  // The health-poll deadline: the route's poll runs up to two minutes, so a
@@ -476,41 +486,99 @@ window.__ModuleLoader__.load({
476
486
  // (prereqs, download, build) run as long as they need to, so a slow build
477
487
  // never reads as a failure.
478
488
  var LAUNCH_HEALTH_DEADLINE_MS = 150000;
489
+ // The stop-poll interval: while a stop is in flight, the card re-fetches
490
+ // the summary on this cadence so the provider's status dot repaints the
491
+ // moment the engine is down, instead of waiting out the host's 5-second
492
+ // process-watch and probe cycle.
493
+ var STOP_POLL_MS = 1000;
494
+ // The stop deadline: a provider still running 10 seconds after a Stop
495
+ // click settles to the red Logs state (the same path a failed launch
496
+ // takes), so the user can open the entry, fix the config, and retry.
497
+ var STOP_HEALTH_DEADLINE_MS = 10000;
479
498
 
480
499
  function launchIsPhase(state) {
481
500
  return LAUNCH_PHASE_STATES.indexOf(state) !== -1;
482
501
  }
483
502
 
484
- function launchNextState(current, event) {
503
+ function launchNextState(entry, event) {
504
+ var current = entry && entry.status !== undefined ? entry.status : 'idle';
505
+ var cause = entry && entry.cause !== undefined ? entry.cause : undefined;
485
506
  var type = event.type;
507
+ var next;
508
+ var nextCause;
486
509
  if (type === 'start') {
487
- // A launch begins from idle (a plain Launch) or failed (a re-launch
488
- // after the Logs control cleared the failed state).
489
- return current === 'idle' || current === 'failed' ? 'prereqs' : current;
490
- }
491
- if (type === 'step') {
510
+ // A launch begins from idle (a plain Launch), failed (a re-launch
511
+ // after the Logs control cleared the failed state), or downloading
512
+ // (a re-poll while the engine fetches its model files).
513
+ next =
514
+ current === 'idle' || current === 'failed' || current === 'downloading'
515
+ ? 'prereqs'
516
+ : current;
517
+ nextCause = undefined;
518
+ } else if (type === 'step') {
492
519
  var target = event.step;
493
- if (LAUNCH_STEP_ORDER[target] === undefined) return current;
494
- if (current === 'idle' || current === 'failed') return target;
495
- if (current === 'running') return current;
496
- var currentOrder = LAUNCH_STEP_ORDER[current];
497
- if (currentOrder === undefined) return current;
498
- return LAUNCH_STEP_ORDER[target] >= currentOrder ? target : current;
499
- }
500
- if (type === 'healthy') {
501
- return launchIsPhase(current) ? 'running' : current;
502
- }
503
- if (type === 'failed') {
504
- return launchIsPhase(current) ? 'failed' : current;
505
- }
506
- if (type === 'stop') {
507
- // A stop settles a running engine (or a launch in flight) back to idle.
508
- return current === 'running' || launchIsPhase(current) ? 'idle' : current;
509
- }
510
- if (type === 'reset') {
511
- return 'idle';
520
+ if (LAUNCH_STEP_ORDER[target] === undefined) {
521
+ next = current;
522
+ nextCause = cause;
523
+ } else if (current === 'idle' || current === 'failed') {
524
+ next = target;
525
+ nextCause = undefined;
526
+ } else if (current === 'running' || current === 'stopping') {
527
+ // A stop in flight supersedes a late launch step.
528
+ next = current;
529
+ nextCause = cause;
530
+ } else {
531
+ var currentOrder = LAUNCH_STEP_ORDER[current];
532
+ if (currentOrder === undefined) {
533
+ next = current;
534
+ nextCause = cause;
535
+ } else {
536
+ next = LAUNCH_STEP_ORDER[target] >= currentOrder ? target : current;
537
+ nextCause = undefined;
538
+ }
539
+ }
540
+ } else if (type === 'healthy') {
541
+ next = launchIsPhase(current) ? 'running' : current;
542
+ nextCause = undefined;
543
+ } else if (type === 'failed') {
544
+ next = launchIsPhase(current) ? 'failed' : current;
545
+ nextCause = undefined;
546
+ } else if (type === 'downloading') {
547
+ // The engine is fetching its model files: a busy throbber, not a
548
+ // failure. The card re-polls (a re-launch) until the engine reports
549
+ // ready, which settles the control to running.
550
+ next = launchIsPhase(current) ? 'downloading' : current;
551
+ nextCause = undefined;
552
+ } else if (type === 'stop') {
553
+ // A stop moves a running engine (or an idle one running per the
554
+ // summary, or a launch in flight) to the stopping state; a stop
555
+ // already in flight is a no-op.
556
+ next =
557
+ current === 'running' ||
558
+ current === 'stopping' ||
559
+ current === 'idle' ||
560
+ launchIsPhase(current)
561
+ ? 'stopping'
562
+ : current;
563
+ nextCause = undefined;
564
+ } else if (type === 'stopped') {
565
+ // The fast poll confirmed the provider is no longer running.
566
+ next = current === 'stopping' ? 'idle' : current;
567
+ nextCause = undefined;
568
+ } else if (type === 'stopFailed') {
569
+ // The deadline fired while the provider was still running.
570
+ next = current === 'stopping' ? 'failed' : current;
571
+ nextCause = current === 'stopping' ? 'stop' : cause;
572
+ } else if (type === 'reset') {
573
+ // A launch failure reads as Launch (idle); a stop timeout reads as
574
+ // Stop (running), because the engine is probably still up.
575
+ next = current === 'failed' ? (cause === 'stop' ? 'running' : 'idle') : 'idle';
576
+ nextCause = undefined;
577
+ } else {
578
+ next = current;
579
+ nextCause = cause;
512
580
  }
513
- return current;
581
+ return { status: next, cause: nextCause };
514
582
  }
515
583
 
516
584
  // The deps panel. The install block is a copy/paste target, so it gets a
@@ -917,20 +985,36 @@ window.__ModuleLoader__.load({
917
985
  };
918
986
  var phaseLabel = phaseLabels[status];
919
987
  if (phaseLabel !== undefined) {
920
- return react.createElement(
921
- 'button',
922
- {
923
- type: 'button',
924
- disabled: true,
925
- style: Object.assign(
926
- { display: 'inline-flex', alignItems: 'center', gap: 4, opacity: 0.7 },
927
- GHOST_BUTTON,
928
- ),
929
- 'aria-label': phaseLabel,
930
- },
931
- renderThrobber(react, primitives),
932
- react.createElement('span', null, phaseLabel),
933
- );
988
+ return renderProcessButton(react, primitives, {
989
+ busy: true,
990
+ label: phaseLabel,
991
+ busyLabel: phaseLabel,
992
+ variant: 'ghost',
993
+ onClick: function () {},
994
+ });
995
+ }
996
+ // A stop in flight: a disabled control with the spinner and the
997
+ // stopping label, the same throbber the launch phases show.
998
+ if (status === 'stopping') {
999
+ return renderProcessButton(react, primitives, {
1000
+ busy: true,
1001
+ label: opts.stoppingLabel,
1002
+ busyLabel: opts.stoppingLabel,
1003
+ variant: 'ghost',
1004
+ onClick: function () {},
1005
+ });
1006
+ }
1007
+ // The engine is fetching its model files (the Strata download): a busy
1008
+ // throbber that reads as in progress, not a failure. The card re-polls
1009
+ // (a re-launch) until the engine reports ready.
1010
+ if (status === 'downloading') {
1011
+ return renderProcessButton(react, primitives, {
1012
+ busy: true,
1013
+ label: opts.downloadingLabel,
1014
+ busyLabel: opts.downloadingLabel,
1015
+ variant: 'ghost',
1016
+ onClick: function () {},
1017
+ });
934
1018
  }
935
1019
  // The Stop control reads from the provider status dot, not just the
936
1020
  // per-entry launch state: the launch state is lost when the user
@@ -979,6 +1063,43 @@ window.__ModuleLoader__.load({
979
1063
  );
980
1064
  }
981
1065
 
1066
+ // The owner/repo short name the provider title bar renders when an entry
1067
+ // names a fork (the git repo that overrides the engine default upstream).
1068
+ // It accepts a protocol URL (https://github.com/owner/repo), an scp-style
1069
+ // git@host:owner/repo reference, and an already-short owner/repo form,
1070
+ // stripping a trailing .git. It returns undefined when no repo name is
1071
+ // present, so the title bar falls back to the engine label.
1072
+ function repoShortName(ref) {
1073
+ if (typeof ref !== 'string' || ref.trim() === '') {
1074
+ return undefined;
1075
+ }
1076
+ var value = ref.trim();
1077
+ var isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
1078
+ if (isUrl) {
1079
+ // Drop the scheme and the host segment, leaving owner/repo.
1080
+ value = value.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '').replace(/^[^/]+\//, '');
1081
+ } else {
1082
+ var colon = value.indexOf(':');
1083
+ var slash = value.indexOf('/');
1084
+ // An scp-style git@host:owner/repo reference: drop the host prefix.
1085
+ if (colon !== -1 && (slash === -1 || colon < slash)) {
1086
+ value = value.slice(colon + 1);
1087
+ }
1088
+ }
1089
+ var segments = value.split('/').filter(function (part) {
1090
+ return part !== '';
1091
+ });
1092
+ if (segments.length === 0) {
1093
+ return undefined;
1094
+ }
1095
+ var repo = segments[segments.length - 1].replace(/\.git$/, '');
1096
+ if (repo === '') {
1097
+ return undefined;
1098
+ }
1099
+ var owner = segments.length > 1 ? segments[segments.length - 2] : undefined;
1100
+ return owner ? owner + '/' + repo : repo;
1101
+ }
1102
+
982
1103
  // unsure), an "Engine: Model" title, and Edit/Delete controls. A New
983
1104
  // provider button resets the form to a fresh entry. The list sits above
984
1105
  // the editing form, so a provider's full config form stays the single
@@ -995,6 +1116,15 @@ window.__ModuleLoader__.load({
995
1116
  ((opts.engines || []).find(function (engine) {
996
1117
  return engine.id === provider.engine;
997
1118
  }) || {}).label || provider.engine;
1119
+ // The title bar renders the fork's owner/repo when the entry names a
1120
+ // fork, so a forked engine reads as its repo (Ninfer/ninfer-all)
1121
+ // rather than the engine label (NInfer). No fork, or an unparseable
1122
+ // one, keeps the engine label.
1123
+ var forkName =
1124
+ provider.platform && typeof provider.platform === 'object'
1125
+ ? repoShortName(provider.platform.engineFork)
1126
+ : undefined;
1127
+ var titleLabel = forkName !== undefined ? forkName : engineLabel;
998
1128
  var modelLabel =
999
1129
  provider.model && provider.model !== '' ? provider.model : opts.modelAuto;
1000
1130
  return react.createElement(
@@ -1023,21 +1153,21 @@ window.__ModuleLoader__.load({
1023
1153
  whiteSpace: 'nowrap',
1024
1154
  },
1025
1155
  title:
1026
- engineLabel +
1156
+ titleLabel +
1027
1157
  ': ' +
1028
1158
  modelLabel +
1029
1159
  ' (' +
1030
1160
  (statusToLabel[provider.status] || provider.status) +
1031
1161
  ')',
1032
1162
  'aria-label':
1033
- engineLabel +
1163
+ titleLabel +
1034
1164
  ': ' +
1035
1165
  modelLabel +
1036
1166
  ' (' +
1037
1167
  (statusToLabel[provider.status] || provider.status) +
1038
1168
  ')',
1039
1169
  },
1040
- engineLabel +
1170
+ titleLabel +
1041
1171
  ': ' +
1042
1172
  modelLabel +
1043
1173
  ' (' +
@@ -1128,6 +1258,39 @@ window.__ModuleLoader__.load({
1128
1258
  });
1129
1259
  }
1130
1260
 
1261
+ // The reusable stateful process button: the Disable/Throbber/Reset
1262
+ // lifecycle every process control (Get, Launch, Stop, Run, Download)
1263
+ // shares. While the process is in flight (busy) it reads as a disabled
1264
+ // control with the spinner and the in-progress label; when it settles the
1265
+ // caller flips busy back to false and the control resets to the idle
1266
+ // action button (the label and the click handler). A host without the
1267
+ // primitives seed keeps the native fallback through renderButton.
1268
+ function renderProcessButton(react, primitives, opts) {
1269
+ if (opts.busy === true) {
1270
+ return react.createElement(
1271
+ 'button',
1272
+ {
1273
+ type: 'button',
1274
+ disabled: true,
1275
+ style: Object.assign(
1276
+ { display: 'inline-flex', alignItems: 'center', gap: 4, opacity: 0.7 },
1277
+ GHOST_BUTTON,
1278
+ ),
1279
+ 'aria-label': opts.busyLabel,
1280
+ },
1281
+ renderThrobber(react, primitives),
1282
+ react.createElement('span', null, opts.busyLabel),
1283
+ );
1284
+ }
1285
+ return renderButton(
1286
+ react,
1287
+ primitives,
1288
+ opts.variant,
1289
+ { onClick: opts.onClick, disabled: opts.disabled === true },
1290
+ opts.label,
1291
+ );
1292
+ }
1293
+
1131
1294
  // The chat-template control, split from the old finicky free-text combo
1132
1295
  // into a preset dropdown (the shipped presets plus a "Custom" entry) and a
1133
1296
  // "Custom Chat template" text field that shows while the preset is
@@ -1778,24 +1941,22 @@ window.__ModuleLoader__.load({
1778
1941
  label: t.allowRun,
1779
1942
  }),
1780
1943
  deps.runAllowed && !deps.allSatisfied
1781
- ? renderButton(
1782
- react,
1783
- primitives,
1784
- 'ghost',
1785
- { onClick: actions.onRun, disabled: actions.installing },
1786
- actions.installing ? t.running : '▶ ' + t.run,
1787
- )
1944
+ ? renderProcessButton(react, primitives, {
1945
+ busy: actions.installing === true,
1946
+ label: '▶ ' + t.run,
1947
+ busyLabel: t.running,
1948
+ variant: 'ghost',
1949
+ onClick: actions.onRun,
1950
+ })
1788
1951
  : null,
1789
- renderButton(
1790
- react,
1791
- primitives,
1792
- 'primary',
1793
- {
1794
- onClick: actions.onGet,
1795
- disabled: !deps.allSatisfied || actions.getting === true,
1796
- },
1797
- actions.getting === true ? t.getting : t.get,
1798
- ),
1952
+ renderProcessButton(react, primitives, {
1953
+ busy: actions.getting === true,
1954
+ label: t.get,
1955
+ busyLabel: t.getting,
1956
+ variant: 'primary',
1957
+ onClick: actions.onGet,
1958
+ disabled: !deps.allSatisfied,
1959
+ }),
1799
1960
  ),
1800
1961
  actions.depsNote
1801
1962
  ? react.createElement(
@@ -1926,12 +2087,39 @@ window.__ModuleLoader__.load({
1926
2087
  // The log-file location the card's log-location field edits. Seeded
1927
2088
  // from the summary's stored template on load; a Save persists it.
1928
2089
  var logPathState = react.useState('');
2090
+ // The Logs section's textarea height in px, resized by the diagonal
2091
+ // chevron drag handle in the section's bottom-right corner.
2092
+ var logsHeightState = react.useState(96);
2093
+ // The Logs section's auto-scroll mode. 'fresh' is the pane just
2094
+ // opened: the first content render scrolls to the bottom and arms
2095
+ // 'scrolling'. 'scrolling' pins the view to the bottom on content
2096
+ // updates. 'manual' means the user scrolled up, so content updates
2097
+ // leave the view where the user put it. A scroll to the very bottom
2098
+ // re-arms 'scrolling'.
2099
+ var logsScrollModeState = react.useState('fresh');
2100
+ // The Logs textarea element, so the auto-scroll effect and the scroll
2101
+ // handler read the live scroll metrics.
2102
+ var logsTextareaRef = react.useRef(null);
2103
+ // The in-flight chevron drag: the pointer's starting Y and the height
2104
+ // it started from, so a move computes the new height from a stable
2105
+ // base instead of a stale render closure.
2106
+ var logsDragRef = react.useRef(null);
1929
2107
  // The per-entry launch state: a map from provider id to the entry's
1930
2108
  // launch-state-machine state (the rebuild phases 'prereqs', 'download',
1931
2109
  // 'build', 'launch', plus 'running' and 'failed') and the start time a
1932
2110
  // launch began, so a card entry can show its phase label, Stop, or red
1933
2111
  // Logs control. Absent entries read as idle (a plain Launch button).
1934
2112
  var entryLaunchState = react.useState({});
2113
+ // The provider statuses the summary last painted, mirrored into a ref
2114
+ // so the stop fast-poll reads the value the last fetch settled, not a
2115
+ // stale closure.
2116
+ var providerStatusRef = react.useRef({});
2117
+ // The latest fetchSummary, mirrored into a ref so the stop fast-poll
2118
+ // re-fetches through the current callback, not a stale one.
2119
+ var fetchSummaryRef = react.useRef(null);
2120
+ // The stop fast-poll and deadline timers (per-entry), plus the
2121
+ // draft-based stop's poll timer, so an unmount clears them all.
2122
+ var stopTimersRef = react.useRef({ poll: null, deadline: null, draftPoll: null });
1935
2123
  var data = dataState[0];
1936
2124
  var draft = draftState[0];
1937
2125
  var note = noteState[0];
@@ -1962,7 +2150,36 @@ window.__ModuleLoader__.load({
1962
2150
  var providerFormMode = providerFormModeState[0];
1963
2151
  var cachedDraft = cachedDraftState[0];
1964
2152
  var logPath = logPathState[0];
2153
+ var logsHeight = logsHeightState[0];
2154
+ var logsScrollMode = logsScrollModeState[0];
2155
+ // The Logs section's displayed text: the streamed Get buffer while a
2156
+ // run is in flight, else the persisted last-run logs the summary
2157
+ // carries. One value the auto-scroll effect and the textarea share.
2158
+ var displayedLogs =
2159
+ getLogs !== null ? getLogs : data !== null && data.lastLogs ? data.lastLogs : '';
1965
2160
  var entryLaunch = entryLaunchState[0];
2161
+ // The title-bar Launch/Stop control mirrors the edited provider's
2162
+ // per-entry launch state, so the entry's Launch button and the form's
2163
+ // control read the same lifecycle (a phase throbber, a Stop when
2164
+ // healthy, a red Logs on a failed launch).
2165
+ var editedProvider =
2166
+ editingProviderId !== null
2167
+ ? (data.providers || []).find((provider) => provider.id === editingProviderId)
2168
+ : undefined;
2169
+ var mirroredStatus =
2170
+ editedProvider !== undefined ? (entryLaunch[editedProvider.id] || {}).status : undefined;
2171
+ var mirroredPhaseLabel =
2172
+ {
2173
+ prereqs: t.entryPrereqs,
2174
+ download: t.entryDownloading,
2175
+ build: t.entryBuilding,
2176
+ launch: t.entryLaunching,
2177
+ // The engine model fetch (the Strata download): the same throbber
2178
+ // label the download phase shows, so the title bar mirrors the
2179
+ // entry's Launch button while the engine fetches its files.
2180
+ downloading: t.entryDownloading,
2181
+ stopping: t.stopping,
2182
+ }[mirroredStatus];
1966
2183
  // The no-entries case forces the form into new-provider mode on
1967
2184
  // render: with nothing stored, the form is where the first provider
1968
2185
  // is entered, and the "New provider" control reads as plain text.
@@ -1980,6 +2197,69 @@ window.__ModuleLoader__.load({
1980
2197
  draftRef.current = draft;
1981
2198
  }, [draft]);
1982
2199
 
2200
+ // The Logs auto-scroll: while the mode is 'fresh' (the pane just
2201
+ // opened) or 'scrolling', a content update pins the view to the
2202
+ // bottom. The first render scrolls and arms 'scrolling'; a manual
2203
+ // scroll up parks the mode at 'manual', so later updates leave the
2204
+ // view where the user put it.
2205
+ react.useEffect(() => {
2206
+ var el = logsTextareaRef.current;
2207
+ if (el === null) {
2208
+ return;
2209
+ }
2210
+ if (logsScrollMode === 'fresh' || logsScrollMode === 'scrolling') {
2211
+ el.scrollTop = el.scrollHeight;
2212
+ if (logsScrollMode === 'fresh') {
2213
+ logsScrollModeState[1]('scrolling');
2214
+ }
2215
+ }
2216
+ }, [displayedLogs, logsScrollMode]);
2217
+
2218
+ // The Logs scroll handler: a scroll to the very bottom re-arms
2219
+ // auto-scroll ('scrolling'); any other position parks the mode at
2220
+ // 'manual', so content updates stop pinning the view. The 4px
2221
+ // tolerance absorbs sub-pixel rounding at the end of the range.
2222
+ var onLogsScroll = react.useCallback(() => {
2223
+ var el = logsTextareaRef.current;
2224
+ if (el === null) {
2225
+ return;
2226
+ }
2227
+ var atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 4;
2228
+ logsScrollModeState[1](atBottom ? 'scrolling' : 'manual');
2229
+ }, []);
2230
+
2231
+ // The Logs resize handle: a pointerdown on the chevron starts a
2232
+ // vertical drag. A move computes the new height from the stable
2233
+ // starting base (drag up shrinks the section to the default height,
2234
+ // drag down expands it), clamped to a readable range. The window
2235
+ // listeners read the drag base from the ref, so a re-render mid-drag
2236
+ // never resets it.
2237
+ var onLogsResizeStart = react.useCallback(
2238
+ (event) => {
2239
+ event.preventDefault();
2240
+ logsDragRef.current = { startClientY: event.clientY, startHeight: logsHeight };
2241
+ var onMove = (moveEvent) => {
2242
+ var drag = logsDragRef.current;
2243
+ if (drag === null) {
2244
+ return;
2245
+ }
2246
+ moveEvent.preventDefault();
2247
+ // Drag down (clientY grows) expands, drag up shrinks to the
2248
+ // default height, so the delta is the pointer's downward travel.
2249
+ var delta = moveEvent.clientY - drag.startClientY;
2250
+ logsHeightState[1](Math.max(96, Math.min(480, drag.startHeight + delta)));
2251
+ };
2252
+ var onUp = () => {
2253
+ logsDragRef.current = null;
2254
+ window.removeEventListener('pointermove', onMove);
2255
+ window.removeEventListener('pointerup', onUp);
2256
+ };
2257
+ window.addEventListener('pointermove', onMove);
2258
+ window.addEventListener('pointerup', onUp);
2259
+ },
2260
+ [logsHeight],
2261
+ );
2262
+
1983
2263
  var fetchSummary = react.useCallback(
1984
2264
  (engine, config) => {
1985
2265
  var params = new URLSearchParams();
@@ -1997,6 +2277,13 @@ window.__ModuleLoader__.load({
1997
2277
  )
1998
2278
  .then((next) => {
1999
2279
  dataState[1](next);
2280
+ // Mirror the provider statuses the summary painted, so the
2281
+ // stop fast-poll reads the latest dot values.
2282
+ var statuses = {};
2283
+ (next.providers || []).forEach(function (provider) {
2284
+ statuses[provider.id] = provider.status;
2285
+ });
2286
+ providerStatusRef.current = statuses;
2000
2287
  // The streamed Get buffer yields to the persisted last-run
2001
2288
  // logs the summary now carries, so the Logs section keeps
2002
2289
  // the run's tail without a second copy.
@@ -2041,6 +2328,31 @@ window.__ModuleLoader__.load({
2041
2328
  [t],
2042
2329
  );
2043
2330
 
2331
+ // Mirror the latest fetchSummary into a ref, so the stop fast-poll
2332
+ // re-fetches through the current callback (its deps change with the
2333
+ // locale), not a stale closure.
2334
+ react.useEffect(() => {
2335
+ fetchSummaryRef.current = fetchSummary;
2336
+ }, [fetchSummary]);
2337
+
2338
+ // Clear the stop fast-poll and deadline timers on unmount, so a
2339
+ // settled or abandoned stop never keeps re-fetching the summary.
2340
+ react.useEffect(
2341
+ () => () => {
2342
+ var timers = stopTimersRef.current;
2343
+ if (timers.poll !== null) {
2344
+ clearTimeout(timers.poll);
2345
+ }
2346
+ if (timers.deadline !== null) {
2347
+ clearTimeout(timers.deadline);
2348
+ }
2349
+ if (timers.draftPoll !== null) {
2350
+ clearInterval(timers.draftPoll);
2351
+ }
2352
+ },
2353
+ [],
2354
+ );
2355
+
2044
2356
  // The deps panel reads its own route, because it re-probes the host
2045
2357
  // on every fetch (the "Get" gate must read the live checkDeps
2046
2358
  // answer), while the summary re-prices the meter. Both ride the same
@@ -2164,6 +2476,9 @@ window.__ModuleLoader__.load({
2164
2476
  }
2165
2477
  downloadingState[1](true);
2166
2478
  downloadNoteState[1]('');
2479
+ // The click writes the "Downloading..." separator, so the run reads
2480
+ // as a fresh Logs section that the download appends to.
2481
+ startProcessLog(t.downloading);
2167
2482
  fetch('/local-infer/download', {
2168
2483
  method: 'POST',
2169
2484
  headers: { 'content-type': 'application/json' },
@@ -2195,7 +2510,7 @@ window.__ModuleLoader__.load({
2195
2510
  downloadingState[1](false);
2196
2511
  });
2197
2512
  },
2198
- [draft, downloading, t, fetchSummary],
2513
+ [draft, downloading, t, fetchSummary, startProcessLog],
2199
2514
  );
2200
2515
 
2201
2516
  // The chat-template download: pulls the selected template (the
@@ -2712,6 +3027,23 @@ window.__ModuleLoader__.load({
2712
3027
  });
2713
3028
  }, [deps, draft, installing, t]);
2714
3029
 
3030
+ // The Logs-section separator a provider process button writes when
3031
+ // clicked: it appends a "--- {{label}}" line to the live Logs buffer
3032
+ // (seeding from the persisted last-run logs when no run is in flight),
3033
+ // so the new process reads as a fresh section that will append its
3034
+ // commands and output to the buffer and the log file. The host route
3035
+ // persists the same separator, so it survives a reload.
3036
+ var startProcessLog = react.useCallback(
3037
+ (label) => {
3038
+ getLogsState[1]((current) => {
3039
+ var base =
3040
+ current !== null ? current : (data !== null && data.lastLogs ? data.lastLogs : '');
3041
+ return base + (base === '' ? '' : '\n') + '--- ' + label + '\n';
3042
+ });
3043
+ },
3044
+ [data],
3045
+ );
3046
+
2715
3047
  var onGet = react.useCallback(() => {
2716
3048
  if (deps === null || draft === null || getting) {
2717
3049
  return;
@@ -2720,10 +3052,11 @@ window.__ModuleLoader__.load({
2720
3052
  // the host through /local-infer/get. The route streams its output;
2721
3053
  // each chunk appends to the Logs section and re-arms the idle
2722
3054
  // timer, and Launch stays disabled until the run finishes, fails,
2723
- // or falls silent for GET_IDLE_MS.
3055
+ // or falls silent for GET_IDLE_MS. The click writes the "Getting..."
3056
+ // separator, so the run reads as a fresh Logs section.
2724
3057
  gettingState[1](true);
2725
3058
  depsNoteState[1]('');
2726
- getLogsState[1]((data && data.lastLogs) || '');
3059
+ startProcessLog(t.getting);
2727
3060
  var append = (text) => {
2728
3061
  getLogsState[1]((current) => (current === null ? '' : current) + text);
2729
3062
  };
@@ -2784,7 +3117,7 @@ window.__ModuleLoader__.load({
2784
3117
  // re-read hands the Logs section back to the stored value.
2785
3118
  fetchSummary(draft.engine, draft.config);
2786
3119
  });
2787
- }, [deps, draft, getting, data, t, fetchSummary]);
3120
+ }, [deps, draft, getting, data, t, fetchSummary, startProcessLog]);
2788
3121
 
2789
3122
  var onRegister = react.useCallback(() => {
2790
3123
  if (draft === null || registering) {
@@ -2903,6 +3236,9 @@ window.__ModuleLoader__.load({
2903
3236
  }
2904
3237
  launchingState[1](true);
2905
3238
  lifecycleNoteState[1]('');
3239
+ // The click writes the "Launching..." separator, so the run reads as
3240
+ // a fresh Logs section that the launch appends to.
3241
+ startProcessLog(t.launching);
2906
3242
  fetch('/local-infer/launch', {
2907
3243
  method: 'POST',
2908
3244
  headers: { 'content-type': 'application/json' },
@@ -2979,7 +3315,7 @@ window.__ModuleLoader__.load({
2979
3315
  // the Get apply when their run ends.
2980
3316
  fetchSummary(draft.engine, draft.config);
2981
3317
  });
2982
- }, [draft, launching, editingProviderId, autoRebuild, t, fetchSummary]);
3318
+ }, [draft, launching, editingProviderId, autoRebuild, t, fetchSummary, startProcessLog]);
2983
3319
 
2984
3320
  var onStop = react.useCallback(() => {
2985
3321
  if (draft === null || stopping) {
@@ -2987,6 +3323,15 @@ window.__ModuleLoader__.load({
2987
3323
  }
2988
3324
  stoppingState[1](true);
2989
3325
  lifecycleNoteState[1]('');
3326
+ // Re-fetch the summary on a fast cadence for the duration of the
3327
+ // stop action, so the provider's status dot repaints the moment the
3328
+ // engine is down, instead of waiting out the host's 5-second
3329
+ // process-watch cycle.
3330
+ if (stopTimersRef.current.draftPoll === null) {
3331
+ stopTimersRef.current.draftPoll = setInterval(function () {
3332
+ fetchSummaryRef.current(null, null);
3333
+ }, STOP_POLL_MS);
3334
+ }
2990
3335
  fetch('/local-infer/stop', {
2991
3336
  method: 'POST',
2992
3337
  headers: { 'content-type': 'application/json' },
@@ -3010,9 +3355,28 @@ window.__ModuleLoader__.load({
3010
3355
  })
3011
3356
  .then(() => {
3012
3357
  stoppingState[1](false);
3358
+ if (stopTimersRef.current.draftPoll !== null) {
3359
+ clearInterval(stopTimersRef.current.draftPoll);
3360
+ stopTimersRef.current.draftPoll = null;
3361
+ }
3013
3362
  });
3014
3363
  }, [draft, stopping, editingProviderId, t]);
3015
3364
 
3365
+ // Move one entry through the provider state machine, reading the
3366
+ // current entry from the latest map so a late event prices the state it
3367
+ // lands on, not a stale one. The per-entry launch and stop both drive
3368
+ // the same machine through this one helper, so a transition is never
3369
+ // priced twice.
3370
+ var moveEntry = react.useCallback((providerId, event) => {
3371
+ entryLaunchState[1]((current) => {
3372
+ var entry = current[providerId] || { status: 'idle' };
3373
+ var next = launchNextState(entry, event);
3374
+ return Object.assign({}, current, {
3375
+ [providerId]: Object.assign({}, next, { startTime: Date.now() }),
3376
+ });
3377
+ });
3378
+ }, []);
3379
+
3016
3380
  // The per-entry launch: a card entry's own Launch control drives its
3017
3381
  // own engine, not the form's draft. It starts the launch state machine
3018
3382
  // at the prereqs phase, then reads the launch stream (an NDJSON step
@@ -3026,28 +3390,18 @@ window.__ModuleLoader__.load({
3026
3390
  (provider) => {
3027
3391
  var prior = entryLaunch[provider.id];
3028
3392
  var status = prior !== undefined ? prior.status : 'idle';
3029
- // A launch in flight (a rebuild phase) is ignored; a running engine
3030
- // is stopped by its own Stop control, not re-launched.
3393
+ // A launch in flight (a rebuild phase) or a stop in flight is
3394
+ // ignored; a running engine is stopped by its own Stop control,
3395
+ // not re-launched.
3031
3396
  if (status !== 'idle' && status !== 'failed') {
3032
3397
  return;
3033
3398
  }
3034
- var startStatus = launchNextState(status, { type: 'start' });
3035
- entryLaunchState[1]((current) =>
3036
- Object.assign({}, current, {
3037
- [provider.id]: { status: startStatus, startTime: Date.now() },
3038
- }),
3039
- );
3040
- // Move the entry's launch state through the machine, reading the
3041
- // current status from the latest map so a late event prices the
3042
- // state it lands on, not a stale one.
3399
+ moveEntry(provider.id, { type: 'start' });
3400
+ // The click writes the "Launching..." separator, so the entry's run
3401
+ // reads as a fresh Logs section that the launch appends to.
3402
+ startProcessLog(t.entryLaunching);
3043
3403
  var move = function (event) {
3044
- entryLaunchState[1]((current) => {
3045
- var entry = current[provider.id];
3046
- var next = launchNextState(entry !== undefined ? entry.status : 'idle', event);
3047
- return Object.assign({}, current, {
3048
- [provider.id]: { status: next, startTime: Date.now() },
3049
- });
3050
- });
3404
+ moveEntry(provider.id, event);
3051
3405
  };
3052
3406
  // The health-poll deadline: the route's poll runs up to two minutes,
3053
3407
  // so a client-side deadline just past that cap forces a hung launch
@@ -3130,6 +3484,16 @@ window.__ModuleLoader__.load({
3130
3484
  }
3131
3485
  if (event.step !== undefined) {
3132
3486
  move({ type: 'step', step: event.step });
3487
+ // The build step writes a "Building..." separator to
3488
+ // the live Logs buffer, so the build reads as a fresh
3489
+ // section the way the launch click writes
3490
+ // "Launching...".
3491
+ if (event.step === 'build') {
3492
+ getLogsState[1]((current) => {
3493
+ var base = current !== null ? current : '';
3494
+ return base + (base === '' ? '' : '\n') + '--- ' + t.entryBuilding + '\n';
3495
+ });
3496
+ }
3133
3497
  // Arm the deadline only for the launch (health-poll)
3134
3498
  // step; the rebuild steps run without a timeout.
3135
3499
  if (event.step === 'launch') {
@@ -3139,7 +3503,17 @@ window.__ModuleLoader__.load({
3139
3503
  settled = true;
3140
3504
  clearDeadline();
3141
3505
  var healthy = event.result.healthy === true;
3142
- move(healthy ? { type: 'healthy' } : { type: 'failed' });
3506
+ // A launch that is still fetching its model files reads
3507
+ // as downloading (a busy throbber the card re-polls),
3508
+ // not as a failure; only a result that is neither
3509
+ // healthy nor downloading settles to the red Logs.
3510
+ if (healthy) {
3511
+ move({ type: 'healthy' });
3512
+ } else if (event.result.state === 'downloading') {
3513
+ move({ type: 'downloading' });
3514
+ } else {
3515
+ move({ type: 'failed' });
3516
+ }
3143
3517
  // The provider status dots price the process probe on a
3144
3518
  // summary fetch, so a launch that turned an engine
3145
3519
  // healthy re-fetches the summary to repaint the dot
@@ -3159,15 +3533,64 @@ window.__ModuleLoader__.load({
3159
3533
  move({ type: 'failed' });
3160
3534
  });
3161
3535
  },
3162
- [entryLaunch, fetchSummary],
3536
+ [entryLaunch, fetchSummary, startProcessLog, moveEntry, getLogsState, t],
3163
3537
  );
3164
3538
 
3165
- // The per-entry stop: a running engine's Stop control settles the
3166
- // launch state machine back to idle (the control reads as Launch
3167
- // again). The stop clears the launch record on this entry, so a
3168
- // recycled pid cannot re-link it.
3539
+ // The per-entry stop: a running engine's Stop control moves the
3540
+ // provider state machine to stopping (the control shows the throbber,
3541
+ // disabled, the same display the launch phases show). For the duration
3542
+ // of the stop action the card re-fetches the summary on a fast cadence
3543
+ // so the provider's status dot repaints the moment the engine is down,
3544
+ // instead of waiting out the host's 5-second process-watch cycle. A
3545
+ // provider still running when the 10-second deadline fires settles to
3546
+ // the red Logs state (the same path a failed launch takes); the Logs
3547
+ // control opens the entry for editing and resets the machine to
3548
+ // running, so the control reads as Stop again. The stop clears the
3549
+ // launch record on this entry, so a recycled pid cannot re-link it.
3169
3550
  var onStopEntry = react.useCallback(
3170
3551
  (provider) => {
3552
+ var prior = entryLaunch[provider.id];
3553
+ var priorStatus = prior !== undefined ? prior.status : 'idle';
3554
+ // A failed launch reads as Logs, not Stop, so a stop only starts
3555
+ // from the other states.
3556
+ if (priorStatus === 'failed') {
3557
+ return;
3558
+ }
3559
+ var move = function (event) {
3560
+ moveEntry(provider.id, event);
3561
+ };
3562
+ var clearStopTimers = function () {
3563
+ if (stopTimersRef.current.poll !== null) {
3564
+ clearTimeout(stopTimersRef.current.poll);
3565
+ stopTimersRef.current.poll = null;
3566
+ }
3567
+ if (stopTimersRef.current.deadline !== null) {
3568
+ clearTimeout(stopTimersRef.current.deadline);
3569
+ stopTimersRef.current.deadline = null;
3570
+ }
3571
+ };
3572
+ // A second Stop click re-arms the timers, so clear the in-flight
3573
+ // ones first, or they would stack and double-settle.
3574
+ clearStopTimers();
3575
+ move({ type: 'stop' });
3576
+ var poll = function () {
3577
+ var status = providerStatusRef.current[provider.id];
3578
+ if (status !== undefined && status !== 'running') {
3579
+ // The provider is no longer running: the stop succeeded.
3580
+ move({ type: 'stopped' });
3581
+ clearStopTimers();
3582
+ return;
3583
+ }
3584
+ fetchSummaryRef.current(null, null);
3585
+ stopTimersRef.current.poll = setTimeout(poll, STOP_POLL_MS);
3586
+ };
3587
+ stopTimersRef.current.poll = setTimeout(poll, STOP_POLL_MS);
3588
+ stopTimersRef.current.deadline = setTimeout(function () {
3589
+ // The provider is still running after the deadline: settle to
3590
+ // the red Logs state, the same path a failed launch takes.
3591
+ clearStopTimers();
3592
+ move({ type: 'stopFailed' });
3593
+ }, STOP_HEALTH_DEADLINE_MS);
3171
3594
  fetch('/local-infer/stop', {
3172
3595
  method: 'POST',
3173
3596
  headers: { 'content-type': 'application/json' },
@@ -3180,25 +3603,12 @@ window.__ModuleLoader__.load({
3180
3603
  providerId: provider.id,
3181
3604
  }),
3182
3605
  })
3183
- .then(() => {
3184
- entryLaunchState[1]((current) => {
3185
- var entry = current[provider.id];
3186
- var status = entry !== undefined ? entry.status : 'idle';
3187
- var next = launchNextState(status, { type: 'stop' });
3188
- var map = Object.assign({}, current);
3189
- if (next === 'idle') {
3190
- delete map[provider.id];
3191
- } else {
3192
- map[provider.id] = { status: next, startTime: Date.now() };
3193
- }
3194
- return map;
3195
- });
3196
- })
3197
3606
  .catch(() => {
3198
- /* a stop that fails leaves the entry in its last state */
3607
+ /* a stop that fails leaves the entry in its last state; the
3608
+ fast-poll and the deadline settle it */
3199
3609
  });
3200
3610
  },
3201
- [],
3611
+ [entryLaunch, moveEntry],
3202
3612
  );
3203
3613
 
3204
3614
  // The per-entry Logs control: opening the logs loads the entry for
@@ -3210,14 +3620,13 @@ window.__ModuleLoader__.load({
3210
3620
  (provider) => {
3211
3621
  onEditProvider(provider);
3212
3622
  entryLaunchState[1]((current) => {
3213
- var entry = current[provider.id];
3214
- var status = entry !== undefined ? entry.status : 'idle';
3215
- var next = launchNextState(status, { type: 'reset' });
3623
+ var entry = current[provider.id] || { status: 'idle' };
3624
+ var next = launchNextState(entry, { type: 'reset' });
3216
3625
  var map = Object.assign({}, current);
3217
- if (next === 'idle') {
3626
+ if (next.status === 'idle') {
3218
3627
  delete map[provider.id];
3219
3628
  } else {
3220
- map[provider.id] = { status: next, startTime: Date.now() };
3629
+ map[provider.id] = Object.assign({}, next, { startTime: Date.now() });
3221
3630
  }
3222
3631
  return map;
3223
3632
  });
@@ -3386,6 +3795,7 @@ window.__ModuleLoader__.load({
3386
3795
  onLogs: onLogsEntry,
3387
3796
  launchLabel: t.entryLaunch,
3388
3797
  stopLabel: t.entryStop,
3798
+ stoppingLabel: t.stopping,
3389
3799
  logsLabel: t.entryLogs,
3390
3800
  launchingLabel: t.entryLaunching,
3391
3801
  prereqsLabel: t.entryPrereqs,
@@ -3555,13 +3965,13 @@ window.__ModuleLoader__.load({
3555
3965
  },
3556
3966
  t.hfCliDetected + ' ' + status.downloadCommand,
3557
3967
  ),
3558
- renderButton(
3559
- react,
3560
- primitives,
3561
- 'ghost',
3562
- { onClick: onDownload, disabled: downloading },
3563
- downloading ? t.downloading : t.download,
3564
- ),
3968
+ renderProcessButton(react, primitives, {
3969
+ busy: downloading === true,
3970
+ label: t.download,
3971
+ busyLabel: t.downloading,
3972
+ variant: 'ghost',
3973
+ onClick: onDownload,
3974
+ }),
3565
3975
  );
3566
3976
  }
3567
3977
  return react.createElement(
@@ -3614,18 +4024,15 @@ window.__ModuleLoader__.load({
3614
4024
  (status.hfCli ? t.hfCliDetected : t.noHfCli) + ' ' + status.downloadCommand,
3615
4025
  ),
3616
4026
  status.hfCli
3617
- ? renderButton(
3618
- react,
3619
- primitives,
3620
- 'ghost',
3621
- {
3622
- onClick: function () {
3623
- onDownloadTemplate(status.repo);
3624
- },
3625
- disabled: templateDownloading,
4027
+ ? renderProcessButton(react, primitives, {
4028
+ busy: templateDownloading === true,
4029
+ label: t.templateDownload,
4030
+ busyLabel: t.templateDownloading,
4031
+ variant: 'ghost',
4032
+ onClick: function () {
4033
+ onDownloadTemplate(status.repo);
3626
4034
  },
3627
- templateDownloading ? t.templateDownloading : t.templateDownload,
3628
- )
4035
+ })
3629
4036
  : null,
3630
4037
  ),
3631
4038
  templateDownloadNote !== ''
@@ -3893,33 +4300,85 @@ window.__ModuleLoader__.load({
3893
4300
  },
3894
4301
  primitives,
3895
4302
  ),
3896
- // The Logs section: the last run's captured output in a fixed-height
3897
- // read-only text area (a launch fills it; the card never edits it),
3898
- // and the log-file location the launch writes to. The location field
3899
- // edits the template the route resolves and writes on the next run.
3900
- // While a Get is in flight the streamed buffer wins: the git sync,
4303
+ // The Logs section: the last run's captured output in a read-only
4304
+ // text area (a launch fills it; the card never edits it), and the
4305
+ // log-file location the launch writes to. The location field edits
4306
+ // the template the route resolves and writes on the next run. While
4307
+ // a Get is in flight the streamed buffer wins: the git sync,
3901
4308
  // dependency, and build output appends there as it lands.
4309
+ //
4310
+ // The textarea auto-scrolls to the bottom while the scroll mode is
4311
+ // 'fresh' (the pane just opened) or 'scrolling'; a manual scroll up
4312
+ // parks the mode at 'manual' so updates stop moving the view, and a
4313
+ // scroll back to the bottom re-arms 'scrolling'. The diagonal
4314
+ // chevron in the section's bottom-right corner drags the height
4315
+ // vertically only.
3902
4316
  react.createElement(
3903
4317
  'div',
3904
4318
  { style: { marginTop: 8 } },
3905
4319
  react.createElement('div', { style: LABEL_STYLE }, t.logs),
3906
- react.createElement('textarea', {
3907
- style: {
3908
- width: '100%',
3909
- height: 96,
3910
- resize: 'none',
3911
- fontFamily: 'monospace',
3912
- fontSize: 11,
3913
- padding: 6,
3914
- border: '1px solid rgba(127,127,127,0.25)',
3915
- borderRadius: 6,
3916
- color: 'inherit',
3917
- background: 'transparent',
3918
- },
3919
- value: getLogs !== null ? getLogs : data.lastLogs || '',
3920
- readOnly: true,
3921
- 'aria-label': t.logs,
3922
- }),
4320
+ react.createElement(
4321
+ 'div',
4322
+ { style: { position: 'relative' } },
4323
+ react.createElement('textarea', {
4324
+ ref: logsTextareaRef,
4325
+ style: {
4326
+ width: '100%',
4327
+ height: logsHeight,
4328
+ boxSizing: 'border-box',
4329
+ resize: 'none',
4330
+ fontFamily: 'monospace',
4331
+ fontSize: 11,
4332
+ padding: 6,
4333
+ border: '1px solid rgba(127,127,127,0.25)',
4334
+ borderRadius: 6,
4335
+ color: 'inherit',
4336
+ background: 'transparent',
4337
+ },
4338
+ value: displayedLogs,
4339
+ readOnly: true,
4340
+ onScroll: onLogsScroll,
4341
+ 'aria-label': t.logs,
4342
+ }),
4343
+ // The diagonal resize chevron: a DSH primitives down chevron
4344
+ // rotated -45 degrees points into the bottom-right corner and
4345
+ // reads as a resize handle. Dragging it vertically resizes the
4346
+ // section; a host without the seed keeps the native corner glyph.
4347
+ react.createElement(
4348
+ 'div',
4349
+ {
4350
+ style: {
4351
+ position: 'absolute',
4352
+ bottom: 0,
4353
+ right: 0,
4354
+ width: 16,
4355
+ height: 16,
4356
+ display: 'flex',
4357
+ alignItems: 'flex-end',
4358
+ justifyContent: 'flex-end',
4359
+ cursor: 'ns-resize',
4360
+ color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
4361
+ userSelect: 'none',
4362
+ touchAction: 'none',
4363
+ },
4364
+ onPointerDown: onLogsResizeStart,
4365
+ role: 'button',
4366
+ 'aria-label': t.logs + ' ' + t.resize,
4367
+ },
4368
+ primitives && typeof primitives.IconChevronDownOutlineRegular === 'function'
4369
+ ? react.createElement(
4370
+ 'span',
4371
+ {
4372
+ style: {
4373
+ display: 'inline-flex',
4374
+ transform: 'rotate(-45deg)',
4375
+ },
4376
+ },
4377
+ react.createElement(primitives.IconChevronDownOutlineRegular, { size: 12 }),
4378
+ )
4379
+ : '\u231F',
4380
+ ),
4381
+ ),
3923
4382
  react.createElement(
3924
4383
  'div',
3925
4384
  { style: ROW_STYLE },
@@ -3945,42 +4404,61 @@ window.__ModuleLoader__.load({
3945
4404
  react.createElement(
3946
4405
  'div',
3947
4406
  { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
3948
- launching
3949
- ? react.createElement(
3950
- 'button',
3951
- {
3952
- type: 'button',
3953
- disabled: true,
3954
- style: Object.assign(
3955
- { display: 'inline-flex', alignItems: 'center', gap: 4, opacity: 0.7 },
3956
- GHOST_BUTTON,
3957
- ),
3958
- 'aria-label': t.launching,
3959
- },
3960
- renderThrobber(react, primitives),
3961
- react.createElement('span', null, t.launching),
3962
- )
3963
- : stopping
3964
- ? renderButton(
3965
- react,
3966
- primitives,
3967
- 'ghost',
3968
- { onClick: function () {}, disabled: true },
3969
- t.stopping,
3970
- )
3971
- : draftRunning
3972
- ? renderButton(react, primitives, 'ghost', { onClick: onStop }, t.stop)
3973
- : renderButton(
3974
- react,
3975
- primitives,
3976
- 'primary',
4407
+ mirroredPhaseLabel !== undefined
4408
+ ? renderProcessButton(react, primitives, {
4409
+ busy: true,
4410
+ label: mirroredPhaseLabel,
4411
+ busyLabel: mirroredPhaseLabel,
4412
+ variant: 'ghost',
4413
+ onClick: function () {},
4414
+ })
4415
+ : mirroredStatus === 'running'
4416
+ ? renderButton(react, primitives, 'ghost', {
4417
+ onClick: function () {
4418
+ onStopEntry(editedProvider);
4419
+ },
4420
+ }, t.stop)
4421
+ : mirroredStatus === 'failed'
4422
+ ? react.createElement(
4423
+ 'button',
3977
4424
  {
3978
- onClick: onLaunch,
3979
- disabled:
3980
- getting || (deps !== null && deps.allSatisfied !== true),
4425
+ type: 'button',
4426
+ onClick: function () {
4427
+ onLogsEntry(editedProvider);
4428
+ },
4429
+ style: Object.assign(
4430
+ { color: 'var(--dsw-alias-label-error, #ff3b30)' },
4431
+ GHOST_BUTTON,
4432
+ ),
4433
+ 'aria-label': t.entryLogs,
3981
4434
  },
3982
- t.launch,
3983
- ),
4435
+ t.entryLogs,
4436
+ )
4437
+ : renderProcessButton(react, primitives, {
4438
+ busy: launching || stopping,
4439
+ label: draftRunning ? t.stop : t.launch,
4440
+ busyLabel: launching ? t.launching : t.stopping,
4441
+ variant: draftRunning ? 'ghost' : 'primary',
4442
+ // While a provider is being edited, the control drives the
4443
+ // provider state machine (the same entry the title bar
4444
+ // mirrors), so the throbber, the fast-poll, and the
4445
+ // 10-second Logs deadline apply. A fresh provider (no
4446
+ // entry) keeps the draft-based Launch/Stop.
4447
+ onClick: draftRunning
4448
+ ? editedProvider !== undefined
4449
+ ? function () {
4450
+ onStopEntry(editedProvider);
4451
+ }
4452
+ : onStop
4453
+ : editedProvider !== undefined
4454
+ ? function () {
4455
+ onLaunchEntry(editedProvider);
4456
+ }
4457
+ : onLaunch,
4458
+ disabled:
4459
+ !draftRunning &&
4460
+ (getting || (deps !== null && deps.allSatisfied !== true)),
4461
+ }),
3984
4462
  // The Auto Rebuild checkbox: checked, a launch runs the git sync
3985
4463
  // (fetch, rebase, rebuild) before the spawn; unchecked, the
3986
4464
  // launch starts the checkout as-is.