dsh-plugin-local-infer 0.1.1 → 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 (38) hide show
  1. package/README.md +4 -1
  2. package/docs/zh/README.md +4 -1
  3. package/dsh/client.js +627 -133
  4. package/package.json +11 -10
  5. package/packages/core/lib/adapter.d.ts +75 -4
  6. package/packages/core/lib/index.d.ts +2 -1
  7. package/packages/core/lib/index.js +1 -0
  8. package/packages/core/lib/launch-command.d.ts +13 -0
  9. package/packages/core/lib/launch-command.js +64 -0
  10. package/packages/core/lib/models.d.ts +2 -1
  11. package/packages/core/lib/models.js +10 -1
  12. package/packages/core/lib/status.d.ts +9 -1
  13. package/packages/core/lib/status.js +6 -1
  14. package/packages/providers/lib/acquire.d.ts +125 -0
  15. package/packages/providers/lib/acquire.js +325 -0
  16. package/packages/providers/lib/adapters.d.ts +42 -5
  17. package/packages/providers/lib/adapters.js +385 -57
  18. package/packages/providers/lib/deps.d.ts +4 -2
  19. package/packages/providers/lib/deps.js +16 -2
  20. package/packages/providers/lib/engines.d.ts +4 -3
  21. package/packages/providers/lib/engines.js +11 -3
  22. package/packages/providers/lib/guidelines.js +16 -0
  23. package/packages/providers/lib/index.d.ts +9 -3
  24. package/packages/providers/lib/index.js +8 -4
  25. package/packages/providers/lib/infernix.d.ts +170 -0
  26. package/packages/providers/lib/infernix.js +671 -0
  27. package/packages/providers/lib/model-defaults.d.ts +5 -3
  28. package/packages/providers/lib/model-defaults.js +27 -3
  29. package/packages/providers/lib/ninfer.d.ts +7 -0
  30. package/packages/providers/lib/ninfer.js +9 -2
  31. package/packages/providers/lib/platform.d.ts +31 -0
  32. package/packages/providers/lib/platform.js +58 -0
  33. package/packages/service/lib/index.d.ts +61 -18
  34. package/packages/service/lib/index.js +278 -42
  35. package/packages/service/lib/types.d.ts +44 -2
  36. package/packages/ui/lib/route.d.ts +83 -12
  37. package/packages/ui/lib/route.js +495 -55
  38. package/packages/ui/package.json +1 -1
package/dsh/client.js CHANGED
@@ -83,6 +83,8 @@ window.__ModuleLoader__.load({
83
83
  run: 'Run',
84
84
  running: 'running...',
85
85
  get: 'Get',
86
+ getting: 'getting...',
87
+ getFailed: 'get failed',
86
88
  allowRun: 'Allow one-click install (sudo)',
87
89
  depsSatisfied: 'All dependencies satisfied.',
88
90
  installRejected: 'one-click install is not allowed; copy the install string instead',
@@ -94,6 +96,7 @@ window.__ModuleLoader__.load({
94
96
  unregistered: 'removed',
95
97
  unregisterFailed: 'remove failed',
96
98
  launch: 'Launch',
99
+ autoRebuild: 'Auto Rebuild',
97
100
  launching: 'Launching...',
98
101
  launchFailed: 'launch failed',
99
102
  runningOn: 'running on port',
@@ -106,6 +109,8 @@ window.__ModuleLoader__.load({
106
109
  enginePlatform: 'Engine platform',
107
110
  engineDirectory: 'Engine Directory',
108
111
  engineFork: 'Engine Fork (Git Repo)',
112
+ buildCommands: 'Build commands',
113
+ dockerTag: 'Docker tag',
109
114
  dirPlaceholder: 'directory path',
110
115
  forkPlaceholder: 'git repo url',
111
116
  hfCliDetected: 'HF CLI detected:',
@@ -115,6 +120,7 @@ window.__ModuleLoader__.load({
115
120
  downloaded: 'download complete',
116
121
  downloadFailed: 'download failed',
117
122
  helpToggle: 'Show or hide field help',
123
+ refresh: 'Re-fetch the card data from the host',
118
124
  engineHelp:
119
125
  'Inference engines have different strengths and weaknesses for specific models and hardware.',
120
126
  modelHelp:
@@ -123,8 +129,12 @@ window.__ModuleLoader__.load({
123
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.',
124
130
  modelDirHelp: 'Where model files are downloaded and served from.',
125
131
  engineDirHelp:
126
- '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.',
127
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.',
128
138
  providers: 'Providers',
129
139
  newProvider: 'New provider',
130
140
  edit: 'Edit',
@@ -142,6 +152,10 @@ window.__ModuleLoader__.load({
142
152
  entryLaunch: 'Launch',
143
153
  entryStop: 'Stop',
144
154
  entryLogs: 'Logs',
155
+ entryPrereqs: 'Prereqs...',
156
+ entryDownloading: 'Downloading...',
157
+ entryBuilding: 'Building...',
158
+ entryLaunching: 'Launching...',
145
159
  chatTemplatePreset: 'Chat template preset',
146
160
  customChatTemplate: 'Custom Chat template',
147
161
  custom: 'Custom',
@@ -210,6 +224,8 @@ window.__ModuleLoader__.load({
210
224
  run: '运行',
211
225
  running: '运行中…',
212
226
  get: '获取',
227
+ getting: '获取中…',
228
+ getFailed: '获取失败',
213
229
  allowRun: '允许一键安装 (sudo)',
214
230
  depsSatisfied: '所有依赖已满足。',
215
231
  installRejected: '未允许一键安装;请复制安装命令。',
@@ -221,6 +237,7 @@ window.__ModuleLoader__.load({
221
237
  unregistered: '已移除',
222
238
  unregisterFailed: '移除失败',
223
239
  launch: '启动',
240
+ autoRebuild: '自动重建',
224
241
  launching: '启动中…',
225
242
  launchFailed: '启动失败',
226
243
  runningOn: '运行于端口',
@@ -233,6 +250,8 @@ window.__ModuleLoader__.load({
233
250
  enginePlatform: '引擎平台',
234
251
  engineDirectory: '引擎目录',
235
252
  engineFork: '引擎 Fork (Git 仓库)',
253
+ buildCommands: '构建命令',
254
+ dockerTag: 'Docker 标签',
236
255
  dirPlaceholder: '目录路径',
237
256
  forkPlaceholder: 'git 仓库地址',
238
257
  hfCliDetected: '检测到 HF CLI:',
@@ -242,13 +261,16 @@ window.__ModuleLoader__.load({
242
261
  downloaded: '下载完成',
243
262
  downloadFailed: '下载失败',
244
263
  helpToggle: '显示或隐藏字段帮助',
264
+ refresh: '从主机重新加载卡片数据',
245
265
  engineHelp: '推理引擎针对特定模型和硬件各有优劣。',
246
266
  modelHelp: '引擎服务的模型。可选项包括引擎默认模型和模型目录中已有的模型文件。',
247
267
  buildHelp:
248
268
  '引擎的部署方式。local 在主机上从源码构建;docker 拉取预构建镜像。依赖计划和硬件准则按所选模式计价。',
249
269
  modelDirHelp: '模型文件的下载和服务目录。',
250
- engineDirHelp: '引擎源码的本地目录。本地构建模式下,部署步骤在此克隆并构建引擎。',
270
+ engineDirHelp: '引擎源码的本地目录。“获取”会在此克隆或更新 git 仓库并构建引擎;未覆盖时,仓库落在模型目录下以引擎命名的子目录中。',
251
271
  engineForkHelp: '用于克隆步骤、覆盖引擎默认上游的 git 仓库。',
272
+ buildCommandsHelp: '本地构建命令,覆盖引擎默认的检查构建。仅在本地构建模式下显示。',
273
+ dockerTagHelp: 'docker 部署拉取的镜像标签。仅在 docker 构建模式下显示。',
252
274
  providers: '提供方',
253
275
  newProvider: '新增提供方',
254
276
  edit: '编辑',
@@ -265,6 +287,10 @@ window.__ModuleLoader__.load({
265
287
  entryLaunch: '启动',
266
288
  entryStop: '停止',
267
289
  entryLogs: '日志',
290
+ entryPrereqs: '前置条件…',
291
+ entryDownloading: '下载中…',
292
+ entryBuilding: '构建中…',
293
+ entryLaunching: '启动中…',
268
294
  chatTemplatePreset: '聊天模板预设',
269
295
  customChatTemplate: '自定义聊天模板',
270
296
  custom: '自定义',
@@ -432,6 +458,61 @@ window.__ModuleLoader__.load({
432
458
  color: '#fff',
433
459
  border: 'none',
434
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
+
435
516
  // The deps panel. The install block is a copy/paste target, so it gets a
436
517
  // monospace pre with its own copy button; the dep rows read their status
437
518
  // from a state dot (the DSH StateDot, or its glyph fallback), not a word,
@@ -476,10 +557,10 @@ window.__ModuleLoader__.load({
476
557
  gap: 4,
477
558
  justifyContent: 'flex-end',
478
559
  };
479
- // The circular "?" button in the card header. It toggles the per-field
480
- // descriptions; the preference persists through the save route, so it
481
- // survives a reload. The active state reads as the accent fill; the
482
- // 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.
483
564
  var HELP_BUTTON_STYLE = (active) => ({
484
565
  width: 20,
485
566
  height: 20,
@@ -818,13 +899,24 @@ window.__ModuleLoader__.load({
818
899
  // a collapsed card with a status dot (green running, red stopped, yellow
819
900
  // The per-entry Launch control: a card entry's own lifecycle button, shown
820
901
  // alongside its Edit and Delete controls. Idle reads as a Launch button
821
- // that starts the entry's engine; a launch in flight shows a disabled
822
- // control with the spinner inside; a healthy engine reads as an enabled
823
- // Stop; a launch that timed out reads as a red Logs control that opens the
824
- // 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.
825
907
  function renderEntryLaunchButton(react, primitives, provider, opts) {
826
908
  var state = (opts.launchState || {})[provider.id];
827
- 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) {
828
920
  return react.createElement(
829
921
  'button',
830
922
  {
@@ -834,20 +926,17 @@ window.__ModuleLoader__.load({
834
926
  { display: 'inline-flex', alignItems: 'center', gap: 4, opacity: 0.7 },
835
927
  GHOST_BUTTON,
836
928
  ),
837
- 'aria-label': opts.launchingLabel,
929
+ 'aria-label': phaseLabel,
838
930
  },
839
931
  renderThrobber(react, primitives),
840
- react.createElement('span', null, opts.launchingLabel),
932
+ react.createElement('span', null, phaseLabel),
841
933
  );
842
934
  }
843
935
  // The Stop control reads from the provider status dot, not just the
844
936
  // per-entry launch state: the launch state is lost when the user
845
937
  // navigates away from the plugin page and back, so a running engine
846
938
  // (a green dot) must keep the control as Stop, not reset to Launch.
847
- if (
848
- (state !== undefined && state.status === 'running') ||
849
- provider.status === 'running'
850
- ) {
939
+ if (status === 'running' || provider.status === 'running') {
851
940
  return renderButton(
852
941
  react,
853
942
  primitives,
@@ -856,10 +945,10 @@ window.__ModuleLoader__.load({
856
945
  opts.stopLabel,
857
946
  );
858
947
  }
859
- if (state !== undefined && state.status === 'failed') {
860
- // A launch that timed out: a red Logs control that opens the entry for
861
- // editing and clears the failed state, so the control reads as Launch
862
- // 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.
863
952
  return react.createElement(
864
953
  'button',
865
954
  {
@@ -1568,6 +1657,11 @@ window.__ModuleLoader__.load({
1568
1657
  );
1569
1658
  }
1570
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
+
1571
1665
  // The "System dependencies" panel: the per-dep status list, the copy/paste
1572
1666
  // install block, the Play/Run arrow (only while the trust gate is on),
1573
1667
  // and the "Get" gate that stays disabled until every dep reports
@@ -1696,8 +1790,11 @@ window.__ModuleLoader__.load({
1696
1790
  react,
1697
1791
  primitives,
1698
1792
  'primary',
1699
- { onClick: actions.onGet, disabled: !deps.allSatisfied },
1700
- t.get,
1793
+ {
1794
+ onClick: actions.onGet,
1795
+ disabled: !deps.allSatisfied || actions.getting === true,
1796
+ },
1797
+ actions.getting === true ? t.getting : t.get,
1701
1798
  ),
1702
1799
  ),
1703
1800
  actions.depsNote
@@ -1766,12 +1863,26 @@ window.__ModuleLoader__.load({
1766
1863
  // does.
1767
1864
  var launchingState = react.useState(false);
1768
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);
1769
1875
  // Whether the draft's engine is confirmed running, so the single
1770
1876
  // Launch/Stop control reads as Stop while the engine is up and Launch
1771
1877
  // while it is down, the same toggle the per-entry control does. A
1772
1878
  // healthy launch sets it, a stop clears it.
1773
1879
  var draftRunningState = react.useState(false);
1774
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);
1775
1886
  // The model download state. The Download button runs the hf CLI
1776
1887
  // download through the /local-infer/download route, and the note
1777
1888
  // reports the answer the route returned.
@@ -1816,8 +1927,9 @@ window.__ModuleLoader__.load({
1816
1927
  // from the summary's stored template on load; a Save persists it.
1817
1928
  var logPathState = react.useState('');
1818
1929
  // The per-entry launch state: a map from provider id to the entry's
1819
- // lifecycle ('launching', 'running', 'failed') and the start time a
1820
- // launch began, so a card entry can show its spinner, 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
1821
1933
  // Logs control. Absent entries read as idle (a plain Launch button).
1822
1934
  var entryLaunchState = react.useState({});
1823
1935
  var data = dataState[0];
@@ -1834,8 +1946,11 @@ window.__ModuleLoader__.load({
1834
1946
  var agentDefault = agentDefaultState[0];
1835
1947
  var launching = launchingState[0];
1836
1948
  var stopping = stoppingState[0];
1949
+ var getting = gettingState[0];
1950
+ var getLogs = getLogsState[0];
1837
1951
  var draftRunning = draftRunningState[0];
1838
1952
  var lifecycleNote = lifecycleNoteState[0];
1953
+ var autoRebuild = autoRebuildState[0];
1839
1954
  var downloading = downloadingState[0];
1840
1955
  var downloadNote = downloadNoteState[0];
1841
1956
  var templateDownloading = templateDownloadingState[0];
@@ -1856,6 +1971,14 @@ window.__ModuleLoader__.load({
1856
1971
  ? 'new'
1857
1972
  : providerFormMode;
1858
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]);
1859
1982
 
1860
1983
  var fetchSummary = react.useCallback(
1861
1984
  (engine, config) => {
@@ -1874,6 +1997,10 @@ window.__ModuleLoader__.load({
1874
1997
  )
1875
1998
  .then((next) => {
1876
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);
1877
2004
  draftState[1]((current) => {
1878
2005
  // The summary's platformConfigs is the host-persisted
1879
2006
  // platform state, while the draft's platform entries are
@@ -1945,6 +2072,25 @@ window.__ModuleLoader__.load({
1945
2072
  [],
1946
2073
  );
1947
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
+
1948
2094
  var load = react.useCallback(() => {
1949
2095
  fetchSummary(null, null);
1950
2096
  fetchDeps(null, null);
@@ -1956,9 +2102,11 @@ window.__ModuleLoader__.load({
1956
2102
  }
1957
2103
  }, [data, draft, load]);
1958
2104
 
1959
- // A field edit updates the draft config and re-prices the meter and
1960
- // form gating. Debounced so a slider drag is one round trip, not one
1961
- // 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.
1962
2110
  var onValue = react.useCallback(
1963
2111
  (key, value) => {
1964
2112
  noteState[1]('');
@@ -1968,22 +2116,15 @@ window.__ModuleLoader__.load({
1968
2116
  draftState[1]((current) => {
1969
2117
  var config = Object.assign({}, current.config);
1970
2118
  config[key] = value;
1971
- var next = {
2119
+ return {
1972
2120
  engine: current.engine,
1973
2121
  config: config,
1974
2122
  platform: current.platform || {},
1975
2123
  };
1976
- if (timerRef.current) {
1977
- clearTimeout(timerRef.current);
1978
- }
1979
- timerRef.current = setTimeout(() => {
1980
- fetchSummary(next.engine, next.config);
1981
- fetchDeps(next.engine, next.config);
1982
- }, 300);
1983
- return next;
1984
2124
  });
2125
+ scheduleReprice();
1985
2126
  },
1986
- [fetchSummary, fetchDeps],
2127
+ [scheduleReprice],
1987
2128
  );
1988
2129
 
1989
2130
  // The Model Directory field edit. A valid directory (non-blank, no
@@ -2141,7 +2282,9 @@ window.__ModuleLoader__.load({
2141
2282
  // The help toggle: flip the local state and persist the preference
2142
2283
  // through the save route, so it survives a reload. The save response
2143
2284
  // carries the fresh summary, so the data and the draft resync from
2144
- // 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.
2145
2288
  var onToggleHelp = react.useCallback(
2146
2289
  (next) => {
2147
2290
  if (draft === null) {
@@ -2157,6 +2300,7 @@ window.__ModuleLoader__.load({
2157
2300
  allowRunDeps: deps !== null && deps.runAllowed === true,
2158
2301
  platform: draft.platform || {},
2159
2302
  showHelp: next,
2303
+ settingsOnly: true,
2160
2304
  }),
2161
2305
  })
2162
2306
  .then((response) =>
@@ -2265,18 +2409,11 @@ window.__ModuleLoader__.load({
2265
2409
  draftState[1]((_current) => {
2266
2410
  // The platform config is per-engine, so an engine switch keeps
2267
2411
  // the map and only resets the per-engine parameter config.
2268
- var next = { engine: engine, config: {}, platform: _current.platform || {} };
2269
- if (timerRef.current) {
2270
- clearTimeout(timerRef.current);
2271
- }
2272
- timerRef.current = setTimeout(() => {
2273
- fetchSummary(next.engine, next.config);
2274
- fetchDeps(next.engine, next.config);
2275
- }, 300);
2276
- return next;
2412
+ return { engine: engine, config: {}, platform: _current.platform || {} };
2277
2413
  });
2414
+ scheduleReprice();
2278
2415
  },
2279
- [fetchSummary, fetchDeps],
2416
+ [scheduleReprice],
2280
2417
  );
2281
2418
 
2282
2419
  var save = react.useCallback(() => {
@@ -2492,6 +2629,9 @@ window.__ModuleLoader__.load({
2492
2629
  }
2493
2630
  }, [deps, t]);
2494
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.
2495
2635
  var onAllowRun = react.useCallback(
2496
2636
  (checked) => {
2497
2637
  if (draft === null) {
@@ -2506,6 +2646,7 @@ window.__ModuleLoader__.load({
2506
2646
  allowRunDeps: checked,
2507
2647
  platform: draft.platform || {},
2508
2648
  showHelp: help,
2649
+ settingsOnly: true,
2509
2650
  }),
2510
2651
  })
2511
2652
  .then((response) =>
@@ -2572,14 +2713,78 @@ window.__ModuleLoader__.load({
2572
2713
  }, [deps, draft, installing, t]);
2573
2714
 
2574
2715
  var onGet = react.useCallback(() => {
2575
- if (deps === null || draft === null) {
2716
+ if (deps === null || draft === null || getting) {
2576
2717
  return;
2577
2718
  }
2578
- // "Get" is the gate: it re-checks the live dep state, so the button
2579
- // opens only after a re-check passes.
2580
- fetchDeps(draft.engine, draft.config);
2581
- depsNoteState[1](t.depsSatisfied);
2582
- }, [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]);
2583
2788
 
2584
2789
  var onRegister = react.useCallback(() => {
2585
2790
  if (draft === null || registering) {
@@ -2678,6 +2883,10 @@ window.__ModuleLoader__.load({
2678
2883
  engine: draft.engine,
2679
2884
  config: draft.config,
2680
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,
2681
2890
  };
2682
2891
  // The provider entry the draft edits: the route persists the launch
2683
2892
  // record (and the stop's clear) on that entry, so a restart re-links
@@ -2699,9 +2908,48 @@ window.__ModuleLoader__.load({
2699
2908
  headers: { 'content-type': 'application/json' },
2700
2909
  body: launchBody(),
2701
2910
  })
2702
- .then((response) => response.json().then((body) => ({ ok: response.ok, body: body })))
2703
- .then(({ ok, body }) => {
2704
- 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) {
2705
2953
  if (body.healthy) {
2706
2954
  lifecycleNoteState[1](
2707
2955
  t.runningOn + ' ' + (body.plan && body.plan.port ? body.plan.port : ''),
@@ -2713,7 +2961,9 @@ window.__ModuleLoader__.load({
2713
2961
  lifecycleNoteState[1](t.notHealthy + (body.detail ? ': ' + body.detail : ''));
2714
2962
  }
2715
2963
  } else {
2716
- lifecycleNoteState[1](t.launchFailed + (body.error ? ': ' + body.error : ''));
2964
+ lifecycleNoteState[1](
2965
+ t.launchFailed + (body && body.detail ? ': ' + body.detail : ''),
2966
+ );
2717
2967
  }
2718
2968
  })
2719
2969
  .catch((error) => {
@@ -2723,8 +2973,13 @@ window.__ModuleLoader__.load({
2723
2973
  })
2724
2974
  .then(() => {
2725
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);
2726
2981
  });
2727
- }, [draft, launching, editingProviderId, t]);
2982
+ }, [draft, launching, editingProviderId, autoRebuild, t, fetchSummary]);
2728
2983
 
2729
2984
  var onStop = react.useCallback(() => {
2730
2985
  if (draft === null || stopping) {
@@ -2759,39 +3014,66 @@ window.__ModuleLoader__.load({
2759
3014
  }, [draft, stopping, editingProviderId, t]);
2760
3015
 
2761
3016
  // The per-entry launch: a card entry's own Launch control drives its
2762
- // own engine, not the form's draft. It records a start time and the
2763
- // 'launching' state (the spinner), then reads the launch result's
2764
- // healthy flag: a 200 before the route's health-poll timeout reads as
2765
- // 'running' (the control becomes Stop), a timeout reads as 'failed'
2766
- // (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).
2767
3025
  var onLaunchEntry = react.useCallback(
2768
3026
  (provider) => {
2769
- var state = entryLaunch[provider.id];
2770
- 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') {
2771
3032
  return;
2772
3033
  }
3034
+ var startStatus = launchNextState(status, { type: 'start' });
2773
3035
  entryLaunchState[1]((current) =>
2774
3036
  Object.assign({}, current, {
2775
- [provider.id]: { status: 'launching', startTime: Date.now() },
3037
+ [provider.id]: { status: startStatus, startTime: Date.now() },
2776
3038
  }),
2777
3039
  );
2778
- // The route's health-poll can run up to two minutes, and a build
2779
- // step before it can run longer still, so a slow or hung route
2780
- // would leave the control on the spinner forever. A client-side
2781
- // deadline just past the route's cap forces the control to the red
2782
- // 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.
2783
3057
  var settled = false;
2784
- var deadline = setTimeout(function () {
2785
- if (settled) {
3058
+ var deadline = null;
3059
+ var armDeadline = function () {
3060
+ if (deadline !== null) {
2786
3061
  return;
2787
3062
  }
2788
- settled = true;
2789
- entryLaunchState[1]((current) =>
2790
- Object.assign({}, current, {
2791
- [provider.id]: { status: 'failed', startTime: Date.now() },
2792
- }),
2793
- );
2794
- }, 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
+ };
2795
3077
  fetch('/local-infer/launch', {
2796
3078
  method: 'POST',
2797
3079
  headers: { 'content-type': 'application/json' },
@@ -2804,34 +3086,86 @@ window.__ModuleLoader__.load({
2804
3086
  providerId: provider.id,
2805
3087
  }),
2806
3088
  })
2807
- .then((response) => response.json().then((body) => ({ ok: response.ok, body: body })))
2808
- .then(({ ok, body }) => {
2809
- settled = true;
2810
- clearTimeout(deadline);
2811
- var nextStatus = ok && body.healthy ? 'running' : 'failed';
2812
- entryLaunchState[1]((current) =>
2813
- Object.assign({}, current, {
2814
- [provider.id]: { status: nextStatus, startTime: Date.now() },
2815
- }),
2816
- );
2817
- // The provider status dots price the process probe on a
2818
- // summary fetch, so a launch that turned an engine healthy
2819
- // re-fetches the summary to repaint the dot from red to green.
2820
- 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();
2821
3155
  })
2822
- .catch(() => {
3156
+ .catch(function () {
2823
3157
  settled = true;
2824
- clearTimeout(deadline);
2825
- entryLaunchState[1]((current) =>
2826
- Object.assign({}, current, {
2827
- [provider.id]: { status: 'failed', startTime: Date.now() },
2828
- }),
2829
- );
3158
+ clearDeadline();
3159
+ move({ type: 'failed' });
2830
3160
  });
2831
3161
  },
2832
3162
  [entryLaunch, fetchSummary],
2833
3163
  );
2834
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.
2835
3169
  var onStopEntry = react.useCallback(
2836
3170
  (provider) => {
2837
3171
  fetch('/local-infer/stop', {
@@ -2848,9 +3182,16 @@ window.__ModuleLoader__.load({
2848
3182
  })
2849
3183
  .then(() => {
2850
3184
  entryLaunchState[1]((current) => {
2851
- var next = Object.assign({}, current);
2852
- delete next[provider.id];
2853
- 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;
2854
3195
  });
2855
3196
  })
2856
3197
  .catch(() => {
@@ -2861,17 +3202,24 @@ window.__ModuleLoader__.load({
2861
3202
  );
2862
3203
 
2863
3204
  // The per-entry Logs control: opening the logs loads the entry for
2864
- // editing AND clears the failed launch state, so the per-entry control
2865
- // reads as Launch again. The user fixes the config in the form and
2866
- // 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
2867
3208
  // non-actionable Logs control.
2868
3209
  var onLogsEntry = react.useCallback(
2869
3210
  (provider) => {
2870
3211
  onEditProvider(provider);
2871
3212
  entryLaunchState[1]((current) => {
2872
- var next = Object.assign({}, current);
2873
- delete next[provider.id];
2874
- 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;
2875
3223
  });
2876
3224
  },
2877
3225
  [onEditProvider],
@@ -2956,9 +3304,14 @@ window.__ModuleLoader__.load({
2956
3304
  : null,
2957
3305
  )
2958
3306
  : null,
2959
- // The card header: the section title plus the circular "?" button
2960
- // that shows or hides the per-field descriptions. The preference
2961
- // 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.
2962
3315
  react.createElement(
2963
3316
  'div',
2964
3317
  {
@@ -2971,16 +3324,39 @@ window.__ModuleLoader__.load({
2971
3324
  },
2972
3325
  react.createElement('div', { style: { fontWeight: 600, fontSize: 14 } }, t.section),
2973
3326
  react.createElement(
2974
- 'button',
3327
+ 'div',
2975
3328
  {
2976
- type: 'button',
2977
- style: HELP_BUTTON_STYLE(help),
2978
- title: t.helpToggle,
2979
- 'aria-label': t.helpToggle,
2980
- 'aria-pressed': help,
2981
- onClick: () => onToggleHelp(!help),
3329
+ style: {
3330
+ display: 'flex',
3331
+ alignItems: 'center',
3332
+ gap: 8,
3333
+ },
2982
3334
  },
2983
- '?',
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
+ ),
2984
3360
  ),
2985
3361
  ),
2986
3362
  // The provider list: the saved provider entries, each a collapsed
@@ -3011,7 +3387,10 @@ window.__ModuleLoader__.load({
3011
3387
  launchLabel: t.entryLaunch,
3012
3388
  stopLabel: t.entryStop,
3013
3389
  logsLabel: t.entryLogs,
3014
- launchingLabel: t.launching,
3390
+ launchingLabel: t.entryLaunching,
3391
+ prereqsLabel: t.entryPrereqs,
3392
+ downloadingLabel: t.entryDownloading,
3393
+ buildingLabel: t.entryBuilding,
3015
3394
  })
3016
3395
  : null,
3017
3396
  // The provider form: the Engine/Model config the card prices. It is
@@ -3314,6 +3693,94 @@ window.__ModuleLoader__.load({
3314
3693
  ),
3315
3694
  ),
3316
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
+ })(),
3317
3784
  );
3318
3785
  })(),
3319
3786
  hasFields
@@ -3322,17 +3789,28 @@ window.__ModuleLoader__.load({
3322
3789
  'div',
3323
3790
  { key: section.group },
3324
3791
  react.createElement('div', { style: SECTION_STYLE }, section.group),
3325
- section.fields.map((field) =>
3326
- 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(
3327
3805
  react,
3328
- field,
3806
+ effectiveField,
3329
3807
  onValue,
3330
3808
  inlineWarnings[field.key],
3331
3809
  primitives,
3332
3810
  help,
3333
3811
  t,
3334
- ),
3335
- ),
3812
+ );
3813
+ }),
3336
3814
  ),
3337
3815
  )
3338
3816
  : react.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, t.noParameters),
@@ -3408,6 +3886,7 @@ window.__ModuleLoader__.load({
3408
3886
  onAllowRun: onAllowRun,
3409
3887
  onRun: onRun,
3410
3888
  onGet: onGet,
3889
+ getting: getting,
3411
3890
  installing: installing,
3412
3891
  copyNote: copyNote,
3413
3892
  depsNote: depsNote,
@@ -3418,6 +3897,8 @@ window.__ModuleLoader__.load({
3418
3897
  // read-only text area (a launch fills it; the card never edits it),
3419
3898
  // and the log-file location the launch writes to. The location field
3420
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.
3421
3902
  react.createElement(
3422
3903
  'div',
3423
3904
  { style: { marginTop: 8 } },
@@ -3435,7 +3916,7 @@ window.__ModuleLoader__.load({
3435
3916
  color: 'inherit',
3436
3917
  background: 'transparent',
3437
3918
  },
3438
- value: data.lastLogs || '',
3919
+ value: getLogs !== null ? getLogs : data.lastLogs || '',
3439
3920
  readOnly: true,
3440
3921
  'aria-label': t.logs,
3441
3922
  }),
@@ -3455,8 +3936,12 @@ window.__ModuleLoader__.load({
3455
3936
  // draft's engine is down, Stop while a healthy launch set draftRunning,
3456
3937
  // and an in-flight spinner (disabled) while a launch or stop is in
3457
3938
  // flight. Launch stays disabled while the deps panel reports a
3458
- // missing host dependency, the same gate the panel's "Get" button
3459
- // 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.
3460
3945
  react.createElement(
3461
3946
  'div',
3462
3947
  { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
@@ -3491,10 +3976,19 @@ window.__ModuleLoader__.load({
3491
3976
  'primary',
3492
3977
  {
3493
3978
  onClick: onLaunch,
3494
- disabled: deps !== null && deps.allSatisfied !== true,
3979
+ disabled:
3980
+ getting || (deps !== null && deps.allSatisfied !== true),
3495
3981
  },
3496
3982
  t.launch,
3497
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
+ }),
3498
3992
  ),
3499
3993
  lifecycleNote
3500
3994
  ? react.createElement('div', { style: { fontSize: 12 } }, lifecycleNote)