dsh-plugin-local-infer 0.1.0 → 0.1.2

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 (73) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +8 -1
  3. package/cordis.patch.yml +6 -6
  4. package/docs/screenshot-20261014.png +0 -0
  5. package/docs/zh/README.md +100 -0
  6. package/dsh/client.js +644 -143
  7. package/dsh/host.js +1 -1
  8. package/package.json +14 -11
  9. package/packages/core/lib/adapter.d.ts +75 -4
  10. package/packages/core/lib/debug.d.ts +1 -1
  11. package/packages/core/lib/debug.js +1 -1
  12. package/packages/core/lib/index.d.ts +3 -2
  13. package/packages/core/lib/index.js +2 -1
  14. package/packages/core/lib/launch-command.d.ts +13 -0
  15. package/packages/core/lib/launch-command.js +64 -0
  16. package/packages/core/lib/models.d.ts +2 -1
  17. package/packages/core/lib/models.js +10 -1
  18. package/packages/core/lib/status.d.ts +9 -1
  19. package/packages/core/lib/status.js +6 -1
  20. package/packages/core/package.json +1 -1
  21. package/packages/providers/lib/acquire.d.ts +125 -0
  22. package/packages/providers/lib/acquire.js +325 -0
  23. package/packages/providers/lib/adapters.d.ts +43 -6
  24. package/packages/providers/lib/adapters.js +386 -58
  25. package/packages/providers/lib/debug.d.ts +1 -1
  26. package/packages/providers/lib/debug.js +1 -1
  27. package/packages/providers/lib/deps.d.ts +6 -4
  28. package/packages/providers/lib/deps.js +17 -3
  29. package/packages/providers/lib/engines.d.ts +6 -5
  30. package/packages/providers/lib/engines.js +12 -4
  31. package/packages/providers/lib/guidelines.d.ts +2 -2
  32. package/packages/providers/lib/guidelines.js +17 -1
  33. package/packages/providers/lib/hyperqwen.d.ts +2 -2
  34. package/packages/providers/lib/hyperqwen.js +1 -1
  35. package/packages/providers/lib/index.d.ts +10 -4
  36. package/packages/providers/lib/index.js +9 -5
  37. package/packages/providers/lib/infernix.d.ts +170 -0
  38. package/packages/providers/lib/infernix.js +671 -0
  39. package/packages/providers/lib/llamacpp.d.ts +2 -2
  40. package/packages/providers/lib/llamacpp.js +1 -1
  41. package/packages/providers/lib/model-defaults.d.ts +7 -5
  42. package/packages/providers/lib/model-defaults.js +28 -4
  43. package/packages/providers/lib/ninfer.d.ts +9 -2
  44. package/packages/providers/lib/ninfer.js +10 -3
  45. package/packages/providers/lib/platform.d.ts +31 -0
  46. package/packages/providers/lib/platform.js +58 -0
  47. package/packages/providers/lib/presets.d.ts +2 -2
  48. package/packages/providers/lib/presets.js +1 -1
  49. package/packages/providers/lib/strata.d.ts +2 -2
  50. package/packages/providers/lib/strata.js +1 -1
  51. package/packages/providers/lib/vram.d.ts +2 -2
  52. package/packages/providers/lib/vram.js +1 -1
  53. package/packages/providers/package.json +3 -3
  54. package/packages/service/lib/index.d.ts +63 -20
  55. package/packages/service/lib/index.js +279 -43
  56. package/packages/service/lib/types.d.ts +44 -2
  57. package/packages/service/package.json +2 -2
  58. package/packages/tool/lib/index.d.ts +1 -1
  59. package/packages/tool/lib/index.js +1 -1
  60. package/packages/tool/package.json +3 -3
  61. package/packages/ui/lib/deps.d.ts +2 -2
  62. package/packages/ui/lib/deps.js +1 -1
  63. package/packages/ui/lib/form.d.ts +2 -2
  64. package/packages/ui/lib/form.js +1 -1
  65. package/packages/ui/lib/index.d.ts +1 -1
  66. package/packages/ui/lib/index.js +1 -1
  67. package/packages/ui/lib/meter.d.ts +2 -2
  68. package/packages/ui/lib/meter.js +1 -1
  69. package/packages/ui/lib/route.d.ts +84 -13
  70. package/packages/ui/lib/route.js +496 -56
  71. package/packages/ui/lib/warnings.d.ts +2 -2
  72. package/packages/ui/lib/warnings.js +1 -1
  73. package/packages/ui/package.json +5 -5
package/dsh/client.js CHANGED
@@ -20,13 +20,16 @@
20
20
  // table predates the primitives seed keep the native controls through the
21
21
  // require fallback in mountCard.
22
22
  window.__ModuleLoader__.load({
23
- id: '@dsh/dsh-plugin-local-infer',
23
+ id: 'dsh-plugin-local-infer',
24
24
  factory: (require) => {
25
25
  var module = { exports: {} };
26
26
  var exports = module.exports;
27
27
 
28
- // The Plugins page dispatches a bundle's configuration by npm package name.
29
- var PACKAGE_NAME = '@dsh/dsh-plugin-local-infer';
28
+ // The Plugins page dispatches a bundle's configuration by the bundle's
29
+ // manifest name (the root package.json "name" field, unscoped for git-URL
30
+ // installs), which is the key the package inventory uses for the
31
+ // plugins.bundle.config slot.
32
+ var PACKAGE_NAME = 'dsh-plugin-local-infer';
30
33
 
31
34
  // The card's copy. Two short label sets rather than a locale bundle: the
32
35
  // card has a couple of dozen strings, and a bundle would be more machinery
@@ -80,6 +83,8 @@ window.__ModuleLoader__.load({
80
83
  run: 'Run',
81
84
  running: 'running...',
82
85
  get: 'Get',
86
+ getting: 'getting...',
87
+ getFailed: 'get failed',
83
88
  allowRun: 'Allow one-click install (sudo)',
84
89
  depsSatisfied: 'All dependencies satisfied.',
85
90
  installRejected: 'one-click install is not allowed; copy the install string instead',
@@ -91,6 +96,7 @@ window.__ModuleLoader__.load({
91
96
  unregistered: 'removed',
92
97
  unregisterFailed: 'remove failed',
93
98
  launch: 'Launch',
99
+ autoRebuild: 'Auto Rebuild',
94
100
  launching: 'Launching...',
95
101
  launchFailed: 'launch failed',
96
102
  runningOn: 'running on port',
@@ -103,6 +109,8 @@ window.__ModuleLoader__.load({
103
109
  enginePlatform: 'Engine platform',
104
110
  engineDirectory: 'Engine Directory',
105
111
  engineFork: 'Engine Fork (Git Repo)',
112
+ buildCommands: 'Build commands',
113
+ dockerTag: 'Docker tag',
106
114
  dirPlaceholder: 'directory path',
107
115
  forkPlaceholder: 'git repo url',
108
116
  hfCliDetected: 'HF CLI detected:',
@@ -112,6 +120,7 @@ window.__ModuleLoader__.load({
112
120
  downloaded: 'download complete',
113
121
  downloadFailed: 'download failed',
114
122
  helpToggle: 'Show or hide field help',
123
+ refresh: 'Re-fetch the card data from the host',
115
124
  engineHelp:
116
125
  'Inference engines have different strengths and weaknesses for specific models and hardware.',
117
126
  modelHelp:
@@ -120,8 +129,12 @@ window.__ModuleLoader__.load({
120
129
  'How the engine is deployed. local builds it from source on the host; docker pulls a prebuilt image. The dependency plan and hardware guidelines price the selected mode.',
121
130
  modelDirHelp: 'Where model files are downloaded and served from.',
122
131
  engineDirHelp:
123
- 'Local directory of the engine source. In local build mode the deploy step clones and builds the engine here.',
132
+ 'Local directory of the engine source. Get clones or updates the git repo and builds the engine here; with no override it lands in the model directory under the engine name.',
124
133
  engineForkHelp: 'Git repo that overrides the engine default upstream for the clone step.',
134
+ buildCommandsHelp:
135
+ 'Local build command that overrides the engine default for the checkout build. Shown only in local build mode.',
136
+ dockerTagHelp:
137
+ 'Docker image tag the docker deploy pulls. Shown only in docker build mode.',
125
138
  providers: 'Providers',
126
139
  newProvider: 'New provider',
127
140
  edit: 'Edit',
@@ -139,6 +152,10 @@ window.__ModuleLoader__.load({
139
152
  entryLaunch: 'Launch',
140
153
  entryStop: 'Stop',
141
154
  entryLogs: 'Logs',
155
+ entryPrereqs: 'Prereqs...',
156
+ entryDownloading: 'Downloading...',
157
+ entryBuilding: 'Building...',
158
+ entryLaunching: 'Launching...',
142
159
  chatTemplatePreset: 'Chat template preset',
143
160
  customChatTemplate: 'Custom Chat template',
144
161
  custom: 'Custom',
@@ -207,6 +224,8 @@ window.__ModuleLoader__.load({
207
224
  run: '运行',
208
225
  running: '运行中…',
209
226
  get: '获取',
227
+ getting: '获取中…',
228
+ getFailed: '获取失败',
210
229
  allowRun: '允许一键安装 (sudo)',
211
230
  depsSatisfied: '所有依赖已满足。',
212
231
  installRejected: '未允许一键安装;请复制安装命令。',
@@ -218,6 +237,7 @@ window.__ModuleLoader__.load({
218
237
  unregistered: '已移除',
219
238
  unregisterFailed: '移除失败',
220
239
  launch: '启动',
240
+ autoRebuild: '自动重建',
221
241
  launching: '启动中…',
222
242
  launchFailed: '启动失败',
223
243
  runningOn: '运行于端口',
@@ -230,6 +250,8 @@ window.__ModuleLoader__.load({
230
250
  enginePlatform: '引擎平台',
231
251
  engineDirectory: '引擎目录',
232
252
  engineFork: '引擎 Fork (Git 仓库)',
253
+ buildCommands: '构建命令',
254
+ dockerTag: 'Docker 标签',
233
255
  dirPlaceholder: '目录路径',
234
256
  forkPlaceholder: 'git 仓库地址',
235
257
  hfCliDetected: '检测到 HF CLI:',
@@ -239,13 +261,16 @@ window.__ModuleLoader__.load({
239
261
  downloaded: '下载完成',
240
262
  downloadFailed: '下载失败',
241
263
  helpToggle: '显示或隐藏字段帮助',
264
+ refresh: '从主机重新加载卡片数据',
242
265
  engineHelp: '推理引擎针对特定模型和硬件各有优劣。',
243
266
  modelHelp: '引擎服务的模型。可选项包括引擎默认模型和模型目录中已有的模型文件。',
244
267
  buildHelp:
245
268
  '引擎的部署方式。local 在主机上从源码构建;docker 拉取预构建镜像。依赖计划和硬件准则按所选模式计价。',
246
269
  modelDirHelp: '模型文件的下载和服务目录。',
247
- engineDirHelp: '引擎源码的本地目录。本地构建模式下,部署步骤在此克隆并构建引擎。',
270
+ engineDirHelp: '引擎源码的本地目录。“获取”会在此克隆或更新 git 仓库并构建引擎;未覆盖时,仓库落在模型目录下以引擎命名的子目录中。',
248
271
  engineForkHelp: '用于克隆步骤、覆盖引擎默认上游的 git 仓库。',
272
+ buildCommandsHelp: '本地构建命令,覆盖引擎默认的检查构建。仅在本地构建模式下显示。',
273
+ dockerTagHelp: 'docker 部署拉取的镜像标签。仅在 docker 构建模式下显示。',
249
274
  providers: '提供方',
250
275
  newProvider: '新增提供方',
251
276
  edit: '编辑',
@@ -262,6 +287,10 @@ window.__ModuleLoader__.load({
262
287
  entryLaunch: '启动',
263
288
  entryStop: '停止',
264
289
  entryLogs: '日志',
290
+ entryPrereqs: '前置条件…',
291
+ entryDownloading: '下载中…',
292
+ entryBuilding: '构建中…',
293
+ entryLaunching: '启动中…',
265
294
  chatTemplatePreset: '聊天模板预设',
266
295
  customChatTemplate: '自定义聊天模板',
267
296
  custom: '自定义',
@@ -429,6 +458,61 @@ window.__ModuleLoader__.load({
429
458
  color: '#fff',
430
459
  border: 'none',
431
460
  });
461
+
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
465
+ // failed (a red Logs). launchNextState is the hardened transition: a step
466
+ // event only advances forward through the rebuild phases (a stale or
467
+ // 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
+ var LAUNCH_STEP_ORDER = { prereqs: 0, download: 1, build: 2, launch: 3 };
472
+ var LAUNCH_PHASE_STATES = ['prereqs', 'download', 'build', 'launch'];
473
+ // The health-poll deadline: the route's poll runs up to two minutes, so a
474
+ // client-side deadline just past that cap forces a hung launch to the red
475
+ // Logs state. It arms only for the launch phase; the rebuild phases
476
+ // (prereqs, download, build) run as long as they need to, so a slow build
477
+ // never reads as a failure.
478
+ var LAUNCH_HEALTH_DEADLINE_MS = 150000;
479
+
480
+ function launchIsPhase(state) {
481
+ return LAUNCH_PHASE_STATES.indexOf(state) !== -1;
482
+ }
483
+
484
+ function launchNextState(current, event) {
485
+ var type = event.type;
486
+ 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') {
492
+ 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';
512
+ }
513
+ return current;
514
+ }
515
+
432
516
  // The deps panel. The install block is a copy/paste target, so it gets a
433
517
  // monospace pre with its own copy button; the dep rows read their status
434
518
  // from a state dot (the DSH StateDot, or its glyph fallback), not a word,
@@ -473,10 +557,10 @@ window.__ModuleLoader__.load({
473
557
  gap: 4,
474
558
  justifyContent: 'flex-end',
475
559
  };
476
- // The circular "?" button in the card header. It toggles the per-field
477
- // descriptions; the preference persists through the save route, so it
478
- // survives a reload. The active state reads as the accent fill; the
479
- // inactive state as the muted outline.
560
+ // The circular header buttons: the "?" help toggle and the refresh
561
+ // action. The active state reads as the accent fill; the inactive state
562
+ // as the muted outline. The help preference persists through the save
563
+ // route, so it survives a reload.
480
564
  var HELP_BUTTON_STYLE = (active) => ({
481
565
  width: 20,
482
566
  height: 20,
@@ -815,13 +899,24 @@ window.__ModuleLoader__.load({
815
899
  // a collapsed card with a status dot (green running, red stopped, yellow
816
900
  // The per-entry Launch control: a card entry's own lifecycle button, shown
817
901
  // alongside its Edit and Delete controls. Idle reads as a Launch button
818
- // that starts the entry's engine; a launch in flight shows a disabled
819
- // control with the Throbber inside; a healthy engine reads as an enabled
820
- // Stop; a launch that timed out reads as a red Logs control that opens the
821
- // entry for editing, the same action as Edit.
902
+ // that starts the entry's engine; a rebuild in flight shows a disabled
903
+ // control with the spinner and the phase label (Prereqs, Downloading,
904
+ // Building, Launching); a healthy engine reads as an enabled Stop; a launch
905
+ // that timed out reads as a red Logs control that opens the entry for
906
+ // editing, the same action as Edit.
822
907
  function renderEntryLaunchButton(react, primitives, provider, opts) {
823
908
  var state = (opts.launchState || {})[provider.id];
824
- if (state !== undefined && state.status === 'launching') {
909
+ var status = state !== undefined ? state.status : 'idle';
910
+ // A rebuild phase in flight: a disabled control with the spinner and the
911
+ // phase label, so the button reads the step that is actually running.
912
+ var phaseLabels = {
913
+ prereqs: opts.prereqsLabel,
914
+ download: opts.downloadingLabel,
915
+ build: opts.buildingLabel,
916
+ launch: opts.launchingLabel,
917
+ };
918
+ var phaseLabel = phaseLabels[status];
919
+ if (phaseLabel !== undefined) {
825
920
  return react.createElement(
826
921
  'button',
827
922
  {
@@ -831,20 +926,17 @@ window.__ModuleLoader__.load({
831
926
  { display: 'inline-flex', alignItems: 'center', gap: 4, opacity: 0.7 },
832
927
  GHOST_BUTTON,
833
928
  ),
834
- 'aria-label': opts.launchingLabel,
929
+ 'aria-label': phaseLabel,
835
930
  },
836
931
  renderThrobber(react, primitives),
837
- react.createElement('span', null, opts.launchingLabel),
932
+ react.createElement('span', null, phaseLabel),
838
933
  );
839
934
  }
840
935
  // The Stop control reads from the provider status dot, not just the
841
936
  // per-entry launch state: the launch state is lost when the user
842
937
  // navigates away from the plugin page and back, so a running engine
843
938
  // (a green dot) must keep the control as Stop, not reset to Launch.
844
- if (
845
- (state !== undefined && state.status === 'running') ||
846
- provider.status === 'running'
847
- ) {
939
+ if (status === 'running' || provider.status === 'running') {
848
940
  return renderButton(
849
941
  react,
850
942
  primitives,
@@ -853,10 +945,10 @@ window.__ModuleLoader__.load({
853
945
  opts.stopLabel,
854
946
  );
855
947
  }
856
- if (state !== undefined && state.status === 'failed') {
857
- // A launch that timed out: a red Logs control that opens the entry for
858
- // editing and clears the failed state, so the control reads as Launch
859
- // again and the user can fix the config and re-launch.
948
+ if (status === 'failed') {
949
+ // A launch that timed out or failed: a red Logs control that opens the
950
+ // entry for editing and clears the failed state, so the control reads
951
+ // as Launch again and the user can fix the config and re-launch.
860
952
  return react.createElement(
861
953
  'button',
862
954
  {
@@ -1011,12 +1103,16 @@ window.__ModuleLoader__.load({
1011
1103
  );
1012
1104
  }
1013
1105
 
1014
- // The DSH Throbber (a spinner) the per-entry Launch button shows while a
1015
- // launch is in flight, with a CSS border-spin as the fallback a host
1016
- // without the seed keeps.
1106
+ // The DSH loading indicator the per-entry Launch button shows while a
1107
+ // launch is in flight: the StateDot ongoing spinner (the primitives'
1108
+ // shared loader) with a CSS border-spin as the fallback a host without the
1109
+ // seed keeps.
1017
1110
  function renderThrobber(react, primitives) {
1018
- if (primitives && typeof primitives.Throbber === 'function') {
1019
- return react.createElement(primitives.Throbber, { size: 'sm' });
1111
+ if (primitives && typeof primitives.StateDot === 'function') {
1112
+ return react.createElement(primitives.StateDot, {
1113
+ state: 'ongoing',
1114
+ size: 12,
1115
+ });
1020
1116
  }
1021
1117
  return react.createElement('span', {
1022
1118
  style: {
@@ -1561,6 +1657,11 @@ window.__ModuleLoader__.load({
1561
1657
  );
1562
1658
  }
1563
1659
 
1660
+ // The Get run's output-idle cap. Two minutes without a streamed line
1661
+ // reads as a stalled build: the card re-enables Launch while the run
1662
+ // keeps streaming in the background.
1663
+ var GET_IDLE_MS = 120000;
1664
+
1564
1665
  // The "System dependencies" panel: the per-dep status list, the copy/paste
1565
1666
  // install block, the Play/Run arrow (only while the trust gate is on),
1566
1667
  // and the "Get" gate that stays disabled until every dep reports
@@ -1689,8 +1790,11 @@ window.__ModuleLoader__.load({
1689
1790
  react,
1690
1791
  primitives,
1691
1792
  'primary',
1692
- { onClick: actions.onGet, disabled: !deps.allSatisfied },
1693
- t.get,
1793
+ {
1794
+ onClick: actions.onGet,
1795
+ disabled: !deps.allSatisfied || actions.getting === true,
1796
+ },
1797
+ actions.getting === true ? t.getting : t.get,
1694
1798
  ),
1695
1799
  ),
1696
1800
  actions.depsNote
@@ -1759,12 +1863,26 @@ window.__ModuleLoader__.load({
1759
1863
  // does.
1760
1864
  var launchingState = react.useState(false);
1761
1865
  var stoppingState = react.useState(false);
1866
+ // The Get step's state. Get runs the git sync, the dependency plan,
1867
+ // and the engine build on the host through /local-infer/get, and the
1868
+ // streamed output lands in getLogs (the Logs section shows it while
1869
+ // the run is in flight). Launch stays disabled while a Get is in
1870
+ // flight; the idle timer re-enables it once GET_IDLE_MS passes with
1871
+ // no output, so a silent build never parks the button.
1872
+ var gettingState = react.useState(false);
1873
+ var getLogsState = react.useState(null);
1874
+ var getIdleTimerRef = react.useRef(null);
1762
1875
  // Whether the draft's engine is confirmed running, so the single
1763
1876
  // Launch/Stop control reads as Stop while the engine is up and Launch
1764
1877
  // while it is down, the same toggle the per-entry control does. A
1765
1878
  // healthy launch sets it, a stop clears it.
1766
1879
  var draftRunningState = react.useState(false);
1767
1880
  var lifecycleNoteState = react.useState('');
1881
+ // The Auto Rebuild checkbox next to the form's Launch button. Checked
1882
+ // by default: a launch runs the git sync (fetch, rebase, rebuild)
1883
+ // before spawning the engine. Unchecked, the launch skips the sync and
1884
+ // starts the checkout as-is.
1885
+ var autoRebuildState = react.useState(true);
1768
1886
  // The model download state. The Download button runs the hf CLI
1769
1887
  // download through the /local-infer/download route, and the note
1770
1888
  // reports the answer the route returned.
@@ -1809,8 +1927,9 @@ window.__ModuleLoader__.load({
1809
1927
  // from the summary's stored template on load; a Save persists it.
1810
1928
  var logPathState = react.useState('');
1811
1929
  // The per-entry launch state: a map from provider id to the entry's
1812
- // lifecycle ('launching', 'running', 'failed') and the start time a
1813
- // launch began, so a card entry can show its Throbber, Stop, or red
1930
+ // launch-state-machine state (the rebuild phases 'prereqs', 'download',
1931
+ // 'build', 'launch', plus 'running' and 'failed') and the start time a
1932
+ // launch began, so a card entry can show its phase label, Stop, or red
1814
1933
  // Logs control. Absent entries read as idle (a plain Launch button).
1815
1934
  var entryLaunchState = react.useState({});
1816
1935
  var data = dataState[0];
@@ -1827,8 +1946,11 @@ window.__ModuleLoader__.load({
1827
1946
  var agentDefault = agentDefaultState[0];
1828
1947
  var launching = launchingState[0];
1829
1948
  var stopping = stoppingState[0];
1949
+ var getting = gettingState[0];
1950
+ var getLogs = getLogsState[0];
1830
1951
  var draftRunning = draftRunningState[0];
1831
1952
  var lifecycleNote = lifecycleNoteState[0];
1953
+ var autoRebuild = autoRebuildState[0];
1832
1954
  var downloading = downloadingState[0];
1833
1955
  var downloadNote = downloadNoteState[0];
1834
1956
  var templateDownloading = templateDownloadingState[0];
@@ -1849,6 +1971,14 @@ window.__ModuleLoader__.load({
1849
1971
  ? 'new'
1850
1972
  : providerFormMode;
1851
1973
  var timerRef = react.useRef(null);
1974
+ // The latest committed draft, mirrored into a ref so the debounced
1975
+ // re-price reads the value the user last typed, not a stale closure.
1976
+ // The effect runs after each render, so the ref settles before the
1977
+ // 300ms timer fires.
1978
+ var draftRef = react.useRef(null);
1979
+ react.useEffect(() => {
1980
+ draftRef.current = draft;
1981
+ }, [draft]);
1852
1982
 
1853
1983
  var fetchSummary = react.useCallback(
1854
1984
  (engine, config) => {
@@ -1867,6 +1997,10 @@ window.__ModuleLoader__.load({
1867
1997
  )
1868
1998
  .then((next) => {
1869
1999
  dataState[1](next);
2000
+ // The streamed Get buffer yields to the persisted last-run
2001
+ // logs the summary now carries, so the Logs section keeps
2002
+ // the run's tail without a second copy.
2003
+ getLogsState[1](null);
1870
2004
  draftState[1]((current) => {
1871
2005
  // The summary's platformConfigs is the host-persisted
1872
2006
  // platform state, while the draft's platform entries are
@@ -1938,6 +2072,25 @@ window.__ModuleLoader__.load({
1938
2072
  [],
1939
2073
  );
1940
2074
 
2075
+ // scheduleReprice resets the re-price timeout on every field change
2076
+ // and fetches the summary (the VRAM Est. meter) and the deps panel
2077
+ // only after typing pauses, so a fast keystroke stream is one round
2078
+ // trip, not one per letter. The ref reads the latest committed draft,
2079
+ // so the fetch prices the value the user last typed.
2080
+ var scheduleReprice = react.useCallback(() => {
2081
+ if (timerRef.current) {
2082
+ clearTimeout(timerRef.current);
2083
+ }
2084
+ timerRef.current = setTimeout(() => {
2085
+ var latest = draftRef.current;
2086
+ if (latest === null || latest === undefined) {
2087
+ return;
2088
+ }
2089
+ fetchSummary(latest.engine, latest.config);
2090
+ fetchDeps(latest.engine, latest.config);
2091
+ }, 300);
2092
+ }, [fetchSummary, fetchDeps]);
2093
+
1941
2094
  var load = react.useCallback(() => {
1942
2095
  fetchSummary(null, null);
1943
2096
  fetchDeps(null, null);
@@ -1949,9 +2102,11 @@ window.__ModuleLoader__.load({
1949
2102
  }
1950
2103
  }, [data, draft, load]);
1951
2104
 
1952
- // A field edit updates the draft config and re-prices the meter and
1953
- // form gating. Debounced so a slider drag is one round trip, not one
1954
- // per tick.
2105
+ // A field edit updates the draft config and schedules a debounced
2106
+ // re-price of the meter and form gating. The draft update is pure;
2107
+ // the re-price (the VRAM Est. visualization) runs only after typing
2108
+ // pauses, so a fast keystroke stream is one round trip, not one per
2109
+ // letter.
1955
2110
  var onValue = react.useCallback(
1956
2111
  (key, value) => {
1957
2112
  noteState[1]('');
@@ -1961,22 +2116,15 @@ window.__ModuleLoader__.load({
1961
2116
  draftState[1]((current) => {
1962
2117
  var config = Object.assign({}, current.config);
1963
2118
  config[key] = value;
1964
- var next = {
2119
+ return {
1965
2120
  engine: current.engine,
1966
2121
  config: config,
1967
2122
  platform: current.platform || {},
1968
2123
  };
1969
- if (timerRef.current) {
1970
- clearTimeout(timerRef.current);
1971
- }
1972
- timerRef.current = setTimeout(() => {
1973
- fetchSummary(next.engine, next.config);
1974
- fetchDeps(next.engine, next.config);
1975
- }, 300);
1976
- return next;
1977
2124
  });
2125
+ scheduleReprice();
1978
2126
  },
1979
- [fetchSummary, fetchDeps],
2127
+ [scheduleReprice],
1980
2128
  );
1981
2129
 
1982
2130
  // The Model Directory field edit. A valid directory (non-blank, no
@@ -2134,7 +2282,9 @@ window.__ModuleLoader__.load({
2134
2282
  // The help toggle: flip the local state and persist the preference
2135
2283
  // through the save route, so it survives a reload. The save response
2136
2284
  // carries the fresh summary, so the data and the draft resync from
2137
- // the same round trip, the way the allow-run toggle does.
2285
+ // the same round trip, the way the allow-run toggle does. The
2286
+ // settings-only flag keeps the route from upserting a provider entry,
2287
+ // so a toggle never mints a duplicate at the top of the list.
2138
2288
  var onToggleHelp = react.useCallback(
2139
2289
  (next) => {
2140
2290
  if (draft === null) {
@@ -2150,6 +2300,7 @@ window.__ModuleLoader__.load({
2150
2300
  allowRunDeps: deps !== null && deps.runAllowed === true,
2151
2301
  platform: draft.platform || {},
2152
2302
  showHelp: next,
2303
+ settingsOnly: true,
2153
2304
  }),
2154
2305
  })
2155
2306
  .then((response) =>
@@ -2258,18 +2409,11 @@ window.__ModuleLoader__.load({
2258
2409
  draftState[1]((_current) => {
2259
2410
  // The platform config is per-engine, so an engine switch keeps
2260
2411
  // the map and only resets the per-engine parameter config.
2261
- var next = { engine: engine, config: {}, platform: _current.platform || {} };
2262
- if (timerRef.current) {
2263
- clearTimeout(timerRef.current);
2264
- }
2265
- timerRef.current = setTimeout(() => {
2266
- fetchSummary(next.engine, next.config);
2267
- fetchDeps(next.engine, next.config);
2268
- }, 300);
2269
- return next;
2412
+ return { engine: engine, config: {}, platform: _current.platform || {} };
2270
2413
  });
2414
+ scheduleReprice();
2271
2415
  },
2272
- [fetchSummary, fetchDeps],
2416
+ [scheduleReprice],
2273
2417
  );
2274
2418
 
2275
2419
  var save = react.useCallback(() => {
@@ -2485,6 +2629,9 @@ window.__ModuleLoader__.load({
2485
2629
  }
2486
2630
  }, [deps, t]);
2487
2631
 
2632
+ // The allow-run toggle persists the trust gate through the save route.
2633
+ // The settings-only flag keeps the route from upserting a provider
2634
+ // entry, so a toggle never mints a duplicate at the top of the list.
2488
2635
  var onAllowRun = react.useCallback(
2489
2636
  (checked) => {
2490
2637
  if (draft === null) {
@@ -2499,6 +2646,7 @@ window.__ModuleLoader__.load({
2499
2646
  allowRunDeps: checked,
2500
2647
  platform: draft.platform || {},
2501
2648
  showHelp: help,
2649
+ settingsOnly: true,
2502
2650
  }),
2503
2651
  })
2504
2652
  .then((response) =>
@@ -2565,14 +2713,78 @@ window.__ModuleLoader__.load({
2565
2713
  }, [deps, draft, installing, t]);
2566
2714
 
2567
2715
  var onGet = react.useCallback(() => {
2568
- if (deps === null || draft === null) {
2716
+ if (deps === null || draft === null || getting) {
2569
2717
  return;
2570
2718
  }
2571
- // "Get" is the gate: it re-checks the live dep state, so the button
2572
- // opens only after a re-check passes.
2573
- fetchDeps(draft.engine, draft.config);
2574
- depsNoteState[1](t.depsSatisfied);
2575
- }, [deps, draft, t, fetchDeps]);
2719
+ // "Get" runs the git sync, the dependency plan, and the build on
2720
+ // the host through /local-infer/get. The route streams its output;
2721
+ // each chunk appends to the Logs section and re-arms the idle
2722
+ // timer, and Launch stays disabled until the run finishes, fails,
2723
+ // or falls silent for GET_IDLE_MS.
2724
+ gettingState[1](true);
2725
+ depsNoteState[1]('');
2726
+ getLogsState[1]((data && data.lastLogs) || '');
2727
+ var append = (text) => {
2728
+ getLogsState[1]((current) => (current === null ? '' : current) + text);
2729
+ };
2730
+ var armIdle = () => {
2731
+ if (getIdleTimerRef.current) {
2732
+ clearTimeout(getIdleTimerRef.current);
2733
+ }
2734
+ // Two minutes without a line reads as a stalled build: release
2735
+ // Launch while the run keeps streaming in the background.
2736
+ getIdleTimerRef.current = setTimeout(() => {
2737
+ gettingState[1](false);
2738
+ }, GET_IDLE_MS);
2739
+ };
2740
+ armIdle();
2741
+ var platform = (draft.platform || {})[draft.engine];
2742
+ fetch('/local-infer/get', {
2743
+ method: 'POST',
2744
+ headers: { 'content-type': 'application/json' },
2745
+ body: JSON.stringify({
2746
+ engine: draft.engine,
2747
+ config: draft.config,
2748
+ platform: platform,
2749
+ }),
2750
+ })
2751
+ .then((response) => {
2752
+ if (!response.ok) {
2753
+ return response.text().then((text) => {
2754
+ throw new Error(text || 'HTTP ' + response.status);
2755
+ });
2756
+ }
2757
+ if (!response.body || typeof response.body.getReader !== 'function') {
2758
+ // A host whose response cannot stream: read the whole body.
2759
+ return response.text().then((text) => append(text));
2760
+ }
2761
+ var reader = response.body.getReader();
2762
+ var decoder = new TextDecoder();
2763
+ var pump = () =>
2764
+ reader.read().then(({ done, value }) => {
2765
+ if (done) {
2766
+ return undefined;
2767
+ }
2768
+ append(decoder.decode(value, { stream: true }));
2769
+ armIdle();
2770
+ return pump();
2771
+ });
2772
+ return pump();
2773
+ })
2774
+ .catch((error) => {
2775
+ append('\n' + t.getFailed + ': ' + (error && error.message ? error.message : '') + '\n');
2776
+ })
2777
+ .then(() => {
2778
+ if (getIdleTimerRef.current) {
2779
+ clearTimeout(getIdleTimerRef.current);
2780
+ getIdleTimerRef.current = null;
2781
+ }
2782
+ gettingState[1](false);
2783
+ // The route persisted the run as the last logs; the summary
2784
+ // re-read hands the Logs section back to the stored value.
2785
+ fetchSummary(draft.engine, draft.config);
2786
+ });
2787
+ }, [deps, draft, getting, data, t, fetchSummary]);
2576
2788
 
2577
2789
  var onRegister = react.useCallback(() => {
2578
2790
  if (draft === null || registering) {
@@ -2671,6 +2883,10 @@ window.__ModuleLoader__.load({
2671
2883
  engine: draft.engine,
2672
2884
  config: draft.config,
2673
2885
  platform: platform,
2886
+ // The Auto Rebuild checkbox: false skips the git sync (fetch,
2887
+ // rebase, rebuild) and launches the checkout as-is. The stop
2888
+ // route ignores the key.
2889
+ autoRebuild: autoRebuild,
2674
2890
  };
2675
2891
  // The provider entry the draft edits: the route persists the launch
2676
2892
  // record (and the stop's clear) on that entry, so a restart re-links
@@ -2692,9 +2908,48 @@ window.__ModuleLoader__.load({
2692
2908
  headers: { 'content-type': 'application/json' },
2693
2909
  body: launchBody(),
2694
2910
  })
2695
- .then((response) => response.json().then((body) => ({ ok: response.ok, body: body })))
2696
- .then(({ ok, body }) => {
2697
- if (ok) {
2911
+ // The launch route streams NDJSON (a step event per phase, then a
2912
+ // final result event). The main form's control does not show the
2913
+ // phase labels, so read the stream and keep only the result event.
2914
+ .then(function (response) {
2915
+ if (response.body === null || typeof response.body.getReader !== 'function') {
2916
+ return response.json().then(function (body) {
2917
+ return { ok: response.ok, result: body };
2918
+ });
2919
+ }
2920
+ var reader = response.body.getReader();
2921
+ var decoder = new TextDecoder();
2922
+ var buffer = '';
2923
+ var resultEvent = null;
2924
+ function pump() {
2925
+ return reader.read().then(function (chunk) {
2926
+ if (chunk.done) {
2927
+ return { ok: response.ok, result: resultEvent };
2928
+ }
2929
+ buffer += decoder.decode(chunk.value, { stream: true });
2930
+ var lines = buffer.split('\n');
2931
+ buffer = lines.pop();
2932
+ for (var i = 0; i < lines.length; i++) {
2933
+ if (lines[i] === '') {
2934
+ continue;
2935
+ }
2936
+ var event;
2937
+ try {
2938
+ event = JSON.parse(lines[i]);
2939
+ } catch {
2940
+ continue;
2941
+ }
2942
+ if (event.result !== undefined) {
2943
+ resultEvent = event.result;
2944
+ }
2945
+ }
2946
+ return pump();
2947
+ });
2948
+ }
2949
+ return pump();
2950
+ })
2951
+ .then(({ ok, result: body }) => {
2952
+ if (ok && body !== null) {
2698
2953
  if (body.healthy) {
2699
2954
  lifecycleNoteState[1](
2700
2955
  t.runningOn + ' ' + (body.plan && body.plan.port ? body.plan.port : ''),
@@ -2706,7 +2961,9 @@ window.__ModuleLoader__.load({
2706
2961
  lifecycleNoteState[1](t.notHealthy + (body.detail ? ': ' + body.detail : ''));
2707
2962
  }
2708
2963
  } else {
2709
- lifecycleNoteState[1](t.launchFailed + (body.error ? ': ' + body.error : ''));
2964
+ lifecycleNoteState[1](
2965
+ t.launchFailed + (body && body.detail ? ': ' + body.detail : ''),
2966
+ );
2710
2967
  }
2711
2968
  })
2712
2969
  .catch((error) => {
@@ -2716,8 +2973,13 @@ window.__ModuleLoader__.load({
2716
2973
  })
2717
2974
  .then(() => {
2718
2975
  launchingState[1](false);
2976
+ // The route persisted the run's launch command line and engine
2977
+ // output as the last logs; the summary re-read hands the Logs
2978
+ // section the new run, the same rule the per-entry launch and
2979
+ // the Get apply when their run ends.
2980
+ fetchSummary(draft.engine, draft.config);
2719
2981
  });
2720
- }, [draft, launching, editingProviderId, t]);
2982
+ }, [draft, launching, editingProviderId, autoRebuild, t, fetchSummary]);
2721
2983
 
2722
2984
  var onStop = react.useCallback(() => {
2723
2985
  if (draft === null || stopping) {
@@ -2752,39 +3014,66 @@ window.__ModuleLoader__.load({
2752
3014
  }, [draft, stopping, editingProviderId, t]);
2753
3015
 
2754
3016
  // The per-entry launch: a card entry's own Launch control drives its
2755
- // own engine, not the form's draft. It records a start time and the
2756
- // 'launching' state (the Throbber), then reads the launch result's
2757
- // healthy flag: a 200 before the route's health-poll timeout reads as
2758
- // 'running' (the control becomes Stop), a timeout reads as 'failed'
2759
- // (the control becomes a red Logs that opens the entry for editing).
3017
+ // own engine, not the form's draft. It starts the launch state machine
3018
+ // at the prereqs phase, then reads the launch stream (an NDJSON step
3019
+ // event as each phase begins, then a final result event) and drives the
3020
+ // machine: each step event advances the phase label (Prereqs,
3021
+ // Downloading, Building, Launching), a healthy result settles to
3022
+ // running (the control becomes Stop), and a failed result or the
3023
+ // health-poll deadline settles to failed (the control becomes a red
3024
+ // Logs that opens the entry for editing).
2760
3025
  var onLaunchEntry = react.useCallback(
2761
3026
  (provider) => {
2762
- var state = entryLaunch[provider.id];
2763
- if (state !== undefined && state.status === 'launching') {
3027
+ var prior = entryLaunch[provider.id];
3028
+ 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.
3031
+ if (status !== 'idle' && status !== 'failed') {
2764
3032
  return;
2765
3033
  }
3034
+ var startStatus = launchNextState(status, { type: 'start' });
2766
3035
  entryLaunchState[1]((current) =>
2767
3036
  Object.assign({}, current, {
2768
- [provider.id]: { status: 'launching', startTime: Date.now() },
3037
+ [provider.id]: { status: startStatus, startTime: Date.now() },
2769
3038
  }),
2770
3039
  );
2771
- // The route's health-poll can run up to two minutes, and a build
2772
- // step before it can run longer still, so a slow or hung route
2773
- // would leave the control on the Throbber forever. A client-side
2774
- // deadline just past the route's cap forces the control to the red
2775
- // Logs state so the entry can be opened and fixed.
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.
3043
+ 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
+ });
3051
+ };
3052
+ // The health-poll deadline: the route's poll runs up to two minutes,
3053
+ // so a client-side deadline just past that cap forces a hung launch
3054
+ // to the red Logs state. It arms only for the launch phase; the
3055
+ // rebuild phases (prereqs, download, build) run as long as they
3056
+ // need to, so a slow build never reads as a failure.
2776
3057
  var settled = false;
2777
- var deadline = setTimeout(function () {
2778
- if (settled) {
3058
+ var deadline = null;
3059
+ var armDeadline = function () {
3060
+ if (deadline !== null) {
2779
3061
  return;
2780
3062
  }
2781
- settled = true;
2782
- entryLaunchState[1]((current) =>
2783
- Object.assign({}, current, {
2784
- [provider.id]: { status: 'failed', startTime: Date.now() },
2785
- }),
2786
- );
2787
- }, 150000);
3063
+ deadline = setTimeout(function () {
3064
+ if (settled) {
3065
+ return;
3066
+ }
3067
+ settled = true;
3068
+ move({ type: 'failed' });
3069
+ }, LAUNCH_HEALTH_DEADLINE_MS);
3070
+ };
3071
+ var clearDeadline = function () {
3072
+ if (deadline !== null) {
3073
+ clearTimeout(deadline);
3074
+ deadline = null;
3075
+ }
3076
+ };
2788
3077
  fetch('/local-infer/launch', {
2789
3078
  method: 'POST',
2790
3079
  headers: { 'content-type': 'application/json' },
@@ -2797,34 +3086,86 @@ window.__ModuleLoader__.load({
2797
3086
  providerId: provider.id,
2798
3087
  }),
2799
3088
  })
2800
- .then((response) => response.json().then((body) => ({ ok: response.ok, body: body })))
2801
- .then(({ ok, body }) => {
2802
- settled = true;
2803
- clearTimeout(deadline);
2804
- var nextStatus = ok && body.healthy ? 'running' : 'failed';
2805
- entryLaunchState[1]((current) =>
2806
- Object.assign({}, current, {
2807
- [provider.id]: { status: nextStatus, startTime: Date.now() },
2808
- }),
2809
- );
2810
- // The provider status dots price the process probe on a
2811
- // summary fetch, so a launch that turned an engine healthy
2812
- // re-fetches the summary to repaint the dot from red to green.
2813
- fetchSummary(provider.engine, provider.config || {});
3089
+ .then(function (response) {
3090
+ // The launch streams NDJSON: a step event as each phase begins,
3091
+ // then a final result event. Read the stream and drive the
3092
+ // state machine; a response with no body (a host without
3093
+ // streaming) settles straight to failed.
3094
+ if (response.body === null || typeof response.body.getReader !== 'function') {
3095
+ settled = true;
3096
+ clearDeadline();
3097
+ move({ type: 'failed' });
3098
+ return Promise.resolve();
3099
+ }
3100
+ var reader = response.body.getReader();
3101
+ var decoder = new TextDecoder();
3102
+ var buffer = '';
3103
+ function pump() {
3104
+ return reader.read().then(function (result) {
3105
+ if (result.done) {
3106
+ // The stream ended without a result event (a host that
3107
+ // cannot stream, a route that broke mid-stream): settle
3108
+ // to failed so the control does not sit on a phase label.
3109
+ if (!settled) {
3110
+ settled = true;
3111
+ clearDeadline();
3112
+ move({ type: 'failed' });
3113
+ }
3114
+ return;
3115
+ }
3116
+ buffer += decoder.decode(result.value, { stream: true });
3117
+ var lines = buffer.split('\n');
3118
+ buffer = lines.pop();
3119
+ for (var i = 0; i < lines.length; i++) {
3120
+ var line = lines[i];
3121
+ if (line === '') {
3122
+ continue;
3123
+ }
3124
+ var event;
3125
+ try {
3126
+ event = JSON.parse(line);
3127
+ } catch {
3128
+ // A malformed line is not a launch event; skip it.
3129
+ continue;
3130
+ }
3131
+ if (event.step !== undefined) {
3132
+ move({ type: 'step', step: event.step });
3133
+ // Arm the deadline only for the launch (health-poll)
3134
+ // step; the rebuild steps run without a timeout.
3135
+ if (event.step === 'launch') {
3136
+ armDeadline();
3137
+ }
3138
+ } else if (event.result !== undefined) {
3139
+ settled = true;
3140
+ clearDeadline();
3141
+ var healthy = event.result.healthy === true;
3142
+ move(healthy ? { type: 'healthy' } : { type: 'failed' });
3143
+ // The provider status dots price the process probe on a
3144
+ // summary fetch, so a launch that turned an engine
3145
+ // healthy re-fetches the summary to repaint the dot
3146
+ // from red to green.
3147
+ fetchSummary(provider.engine, provider.config || {});
3148
+ return;
3149
+ }
3150
+ }
3151
+ return pump();
3152
+ });
3153
+ }
3154
+ return pump();
2814
3155
  })
2815
- .catch(() => {
3156
+ .catch(function () {
2816
3157
  settled = true;
2817
- clearTimeout(deadline);
2818
- entryLaunchState[1]((current) =>
2819
- Object.assign({}, current, {
2820
- [provider.id]: { status: 'failed', startTime: Date.now() },
2821
- }),
2822
- );
3158
+ clearDeadline();
3159
+ move({ type: 'failed' });
2823
3160
  });
2824
3161
  },
2825
3162
  [entryLaunch, fetchSummary],
2826
3163
  );
2827
3164
 
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.
2828
3169
  var onStopEntry = react.useCallback(
2829
3170
  (provider) => {
2830
3171
  fetch('/local-infer/stop', {
@@ -2841,9 +3182,16 @@ window.__ModuleLoader__.load({
2841
3182
  })
2842
3183
  .then(() => {
2843
3184
  entryLaunchState[1]((current) => {
2844
- var next = Object.assign({}, current);
2845
- delete next[provider.id];
2846
- return next;
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;
2847
3195
  });
2848
3196
  })
2849
3197
  .catch(() => {
@@ -2854,17 +3202,24 @@ window.__ModuleLoader__.load({
2854
3202
  );
2855
3203
 
2856
3204
  // The per-entry Logs control: opening the logs loads the entry for
2857
- // editing AND clears the failed launch state, so the per-entry control
2858
- // reads as Launch again. The user fixes the config in the form and
2859
- // re-launches from the same entry, instead of being stuck on a
3205
+ // editing AND resets the launch state machine to idle, so the per-entry
3206
+ // control reads as Launch again. The user fixes the config in the form
3207
+ // and re-launches from the same entry, instead of being stuck on a
2860
3208
  // non-actionable Logs control.
2861
3209
  var onLogsEntry = react.useCallback(
2862
3210
  (provider) => {
2863
3211
  onEditProvider(provider);
2864
3212
  entryLaunchState[1]((current) => {
2865
- var next = Object.assign({}, current);
2866
- delete next[provider.id];
2867
- return next;
3213
+ var entry = current[provider.id];
3214
+ var status = entry !== undefined ? entry.status : 'idle';
3215
+ var next = launchNextState(status, { type: 'reset' });
3216
+ var map = Object.assign({}, current);
3217
+ if (next === 'idle') {
3218
+ delete map[provider.id];
3219
+ } else {
3220
+ map[provider.id] = { status: next, startTime: Date.now() };
3221
+ }
3222
+ return map;
2868
3223
  });
2869
3224
  },
2870
3225
  [onEditProvider],
@@ -2893,7 +3248,7 @@ window.__ModuleLoader__.load({
2893
3248
  return react.createElement(
2894
3249
  'div',
2895
3250
  { style: CARD_STYLE },
2896
- // The keyframes the Throbber fallback spinner animates with. Injected
3251
+ // The keyframes the fallback spinner animates with. Injected
2897
3252
  // once per card so a host without the DSH seed still shows a spin.
2898
3253
  react.createElement(
2899
3254
  'style',
@@ -2949,9 +3304,14 @@ window.__ModuleLoader__.load({
2949
3304
  : null,
2950
3305
  )
2951
3306
  : null,
2952
- // The card header: the section title plus the circular "?" button
2953
- // that shows or hides the per-field descriptions. The preference
2954
- // persists through the save route, so it survives a reload.
3307
+ // The card header: the section title plus the circular refresh and
3308
+ // "?" buttons. The refresh button re-fetches the summary and deps
3309
+ // from the host, so the card re-prices after the harness pauses
3310
+ // the plugin's routes and re-activates them. The refresh glyph is
3311
+ // the primitives seed's IconRefreshOutlineRegular; a host without
3312
+ // the seed keeps the native arrow character. The "?" button shows
3313
+ // or hides the per-field descriptions; the preference persists
3314
+ // through the save route, so it survives a reload.
2955
3315
  react.createElement(
2956
3316
  'div',
2957
3317
  {
@@ -2964,16 +3324,39 @@ window.__ModuleLoader__.load({
2964
3324
  },
2965
3325
  react.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, t.section),
2966
3326
  react.createElement(
2967
- 'button',
3327
+ 'div',
2968
3328
  {
2969
- type: 'button',
2970
- style: HELP_BUTTON_STYLE(help),
2971
- title: t.helpToggle,
2972
- 'aria-label': t.helpToggle,
2973
- 'aria-pressed': help,
2974
- onClick: () => onToggleHelp(!help),
3329
+ style: {
3330
+ display: 'flex',
3331
+ alignItems: 'center',
3332
+ gap: 8,
3333
+ },
2975
3334
  },
2976
- '?',
3335
+ react.createElement(
3336
+ 'button',
3337
+ {
3338
+ type: 'button',
3339
+ style: HELP_BUTTON_STYLE(false),
3340
+ title: t.refresh,
3341
+ 'aria-label': t.refresh,
3342
+ onClick: () => load(),
3343
+ },
3344
+ primitives && typeof primitives.IconRefreshOutlineRegular === 'function'
3345
+ ? react.createElement(primitives.IconRefreshOutlineRegular, { size: 12 })
3346
+ : '↻',
3347
+ ),
3348
+ react.createElement(
3349
+ 'button',
3350
+ {
3351
+ type: 'button',
3352
+ style: HELP_BUTTON_STYLE(help),
3353
+ title: t.helpToggle,
3354
+ 'aria-label': t.helpToggle,
3355
+ 'aria-pressed': help,
3356
+ onClick: () => onToggleHelp(!help),
3357
+ },
3358
+ '?',
3359
+ ),
2977
3360
  ),
2978
3361
  ),
2979
3362
  // The provider list: the saved provider entries, each a collapsed
@@ -3004,7 +3387,10 @@ window.__ModuleLoader__.load({
3004
3387
  launchLabel: t.entryLaunch,
3005
3388
  stopLabel: t.entryStop,
3006
3389
  logsLabel: t.entryLogs,
3007
- launchingLabel: t.launching,
3390
+ launchingLabel: t.entryLaunching,
3391
+ prereqsLabel: t.entryPrereqs,
3392
+ downloadingLabel: t.entryDownloading,
3393
+ buildingLabel: t.entryBuilding,
3008
3394
  })
3009
3395
  : null,
3010
3396
  // The provider form: the Engine/Model config the card prices. It is
@@ -3307,6 +3693,94 @@ window.__ModuleLoader__.load({
3307
3693
  ),
3308
3694
  ),
3309
3695
  help ? renderHelpRow(react, t.engineForkHelp) : null,
3696
+ (() => {
3697
+ // The build-method-dependent platform fields: local build mode
3698
+ // shows the Build commands override, docker build mode shows the
3699
+ // Docker tag override. Only one is visible at a time, keyed off
3700
+ // the same buildMode the Build row prices. The placeholder seeds
3701
+ // the engine's own default (the build step the deploy runs, or the
3702
+ // image tag the docker deploy pulls), so an empty field reads as
3703
+ // "use the default."
3704
+ var engineEntry = (data.engines || []).find(
3705
+ (entry) => entry.id === draft.engine,
3706
+ );
3707
+ var buildModes = [];
3708
+ if (engineEntry && engineEntry.supports.local) buildModes.push('local');
3709
+ if (engineEntry && engineEntry.supports.docker) buildModes.push('docker');
3710
+ var buildMode =
3711
+ typeof draft.config.buildMode === 'string'
3712
+ ? draft.config.buildMode
3713
+ : buildModes[0] || 'local';
3714
+ if (buildMode === 'local') {
3715
+ return react.createElement(
3716
+ 'div',
3717
+ null,
3718
+ react.createElement(
3719
+ 'div',
3720
+ { style: ROW_STYLE },
3721
+ react.createElement('label', { style: LABEL_STYLE }, t.buildCommands),
3722
+ react.createElement(
3723
+ 'input',
3724
+ Object.assign(
3725
+ { style: INPUT_STYLE },
3726
+ {
3727
+ type: 'text',
3728
+ value:
3729
+ typeof enginePlatform.buildCommands === 'string'
3730
+ ? enginePlatform.buildCommands
3731
+ : '',
3732
+ placeholder:
3733
+ engineEntry && engineEntry.defaultBuildCommand
3734
+ ? engineEntry.defaultBuildCommand
3735
+ : '',
3736
+ onChange: (event) =>
3737
+ onPlatform(
3738
+ 'buildCommands',
3739
+ event.target.value === '' ? undefined : event.target.value,
3740
+ ),
3741
+ },
3742
+ ),
3743
+ ),
3744
+ ),
3745
+ help ? renderHelpRow(react, t.buildCommandsHelp) : null,
3746
+ );
3747
+ }
3748
+ if (buildMode === 'docker') {
3749
+ return react.createElement(
3750
+ 'div',
3751
+ null,
3752
+ react.createElement(
3753
+ 'div',
3754
+ { style: ROW_STYLE },
3755
+ react.createElement('label', { style: LABEL_STYLE }, t.dockerTag),
3756
+ react.createElement(
3757
+ 'input',
3758
+ Object.assign(
3759
+ { style: INPUT_STYLE },
3760
+ {
3761
+ type: 'text',
3762
+ value:
3763
+ typeof enginePlatform.dockerTag === 'string'
3764
+ ? enginePlatform.dockerTag
3765
+ : '',
3766
+ placeholder:
3767
+ engineEntry && engineEntry.defaultDockerTag
3768
+ ? engineEntry.defaultDockerTag
3769
+ : '',
3770
+ onChange: (event) =>
3771
+ onPlatform(
3772
+ 'dockerTag',
3773
+ event.target.value === '' ? undefined : event.target.value,
3774
+ ),
3775
+ },
3776
+ ),
3777
+ ),
3778
+ ),
3779
+ help ? renderHelpRow(react, t.dockerTagHelp) : null,
3780
+ );
3781
+ }
3782
+ return null;
3783
+ })(),
3310
3784
  );
3311
3785
  })(),
3312
3786
  hasFields
@@ -3315,17 +3789,28 @@ window.__ModuleLoader__.load({
3315
3789
  'div',
3316
3790
  { key: section.group },
3317
3791
  react.createElement('div', { style: SECTION_STYLE }, section.group),
3318
- section.fields.map((field) =>
3319
- renderField(
3792
+ section.fields.map((field) => {
3793
+ // The field value binds to the local draft, not the server
3794
+ // render model, so a typed keystroke shows in the input
3795
+ // immediately. The server model (field.value) resolves the
3796
+ // spec default and only surfaces after the debounced
3797
+ // re-price settles. A key the user has not edited is
3798
+ // absent from the draft, so it falls back to the model.
3799
+ var draftValue = draft.config[field.key];
3800
+ var effectiveField =
3801
+ draftValue !== undefined
3802
+ ? Object.assign({}, field, { value: draftValue })
3803
+ : field;
3804
+ return renderField(
3320
3805
  react,
3321
- field,
3806
+ effectiveField,
3322
3807
  onValue,
3323
3808
  inlineWarnings[field.key],
3324
3809
  primitives,
3325
3810
  help,
3326
3811
  t,
3327
- ),
3328
- ),
3812
+ );
3813
+ }),
3329
3814
  ),
3330
3815
  )
3331
3816
  : react.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, t.noParameters),
@@ -3401,6 +3886,7 @@ window.__ModuleLoader__.load({
3401
3886
  onAllowRun: onAllowRun,
3402
3887
  onRun: onRun,
3403
3888
  onGet: onGet,
3889
+ getting: getting,
3404
3890
  installing: installing,
3405
3891
  copyNote: copyNote,
3406
3892
  depsNote: depsNote,
@@ -3411,6 +3897,8 @@ window.__ModuleLoader__.load({
3411
3897
  // read-only text area (a launch fills it; the card never edits it),
3412
3898
  // and the log-file location the launch writes to. The location field
3413
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,
3901
+ // dependency, and build output appends there as it lands.
3414
3902
  react.createElement(
3415
3903
  'div',
3416
3904
  { style: { marginTop: 8 } },
@@ -3428,7 +3916,7 @@ window.__ModuleLoader__.load({
3428
3916
  color: 'inherit',
3429
3917
  background: 'transparent',
3430
3918
  },
3431
- value: data.lastLogs || '',
3919
+ value: getLogs !== null ? getLogs : data.lastLogs || '',
3432
3920
  readOnly: true,
3433
3921
  'aria-label': t.logs,
3434
3922
  }),
@@ -3446,10 +3934,14 @@ window.__ModuleLoader__.load({
3446
3934
  // The engine lifecycle row: a single Launch/Stop control that
3447
3935
  // toggles like the per-entry control. It reads as Launch while the
3448
3936
  // draft's engine is down, Stop while a healthy launch set draftRunning,
3449
- // and an in-flight Throbber (disabled) while a launch or stop is in
3937
+ // and an in-flight spinner (disabled) while a launch or stop is in
3450
3938
  // flight. Launch stays disabled while the deps panel reports a
3451
- // missing host dependency, the same gate the panel's "Get" button
3452
- // reads. The note reports the answer the route returned.
3939
+ // missing host dependency or a Get run is in flight, and re-enables
3940
+ // when the build finishes, errors, or falls silent for GET_IDLE_MS.
3941
+ // The Auto Rebuild checkbox sits after the control: checked, a
3942
+ // launch runs the git sync (fetch, rebase, rebuild) first;
3943
+ // unchecked, the launch starts the checkout as-is. The note reports
3944
+ // the answer the route returned.
3453
3945
  react.createElement(
3454
3946
  'div',
3455
3947
  { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
@@ -3484,10 +3976,19 @@ window.__ModuleLoader__.load({
3484
3976
  'primary',
3485
3977
  {
3486
3978
  onClick: onLaunch,
3487
- disabled: deps !== null && deps.allSatisfied !== true,
3979
+ disabled:
3980
+ getting || (deps !== null && deps.allSatisfied !== true),
3488
3981
  },
3489
3982
  t.launch,
3490
3983
  ),
3984
+ // The Auto Rebuild checkbox: checked, a launch runs the git sync
3985
+ // (fetch, rebase, rebuild) before the spawn; unchecked, the
3986
+ // launch starts the checkout as-is.
3987
+ renderCheckbox(react, primitives, {
3988
+ checked: autoRebuild,
3989
+ onChange: (next) => autoRebuildState[1](next),
3990
+ label: t.autoRebuild,
3991
+ }),
3491
3992
  ),
3492
3993
  lifecycleNote
3493
3994
  ? react.createElement('div', { style: { fontSize: 12 } }, lifecycleNote)