@foggy-projects/deepseek-harness-plugin 0.4.0-beta.8 → 0.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -8,25 +8,47 @@ window.__ModuleLoader__.load({
8
8
 
9
9
  const NS = 'settings.foggy'
10
10
  const passthroughSchema = { parse: (value) => value }
11
+ const runtimeSettingsInputSchema = {
12
+ parse: (value) => {
13
+ const port = value?.port
14
+ if (!Number.isInteger(port) || port < 1024 || port > 65535 || Object.keys(value || {}).some((key) => key !== 'port')) {
15
+ throw new Error('Runtime port must be an integer between 1024 and 65535')
16
+ }
17
+ return { port }
18
+ },
19
+ }
20
+ const descriptor = (method, parameters = []) => ({
21
+ id: `@foggy-projects/deepseek-harness-plugin#foggyIntegration/${method}`,
22
+ service: 'foggyIntegration',
23
+ namespace: 'foggyIntegration',
24
+ method,
25
+ invocation: { kind: 'direct' },
26
+ parameters,
27
+ result: {
28
+ mode: 'strict',
29
+ typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyIntegrationResult',
30
+ schema: passthroughSchema,
31
+ },
32
+ sourceLocation: { file: 'lib/index.js', line: 1, column: 1 },
33
+ })
11
34
  const TYPERT_REMOTE = {
12
35
  package: '@foggy-projects/deepseek-harness-plugin',
13
36
  descriptors: [
37
+ ...[
14
38
  'status', 'plan', 'initialize', 'repair', 'repairPython', 'repairCli', 'repairLauncher', 'repairAnalysisSkill',
15
39
  'migrateProfiles', 'diagnostics', 'runtimeStart', 'runtimeStop',
16
- ].map((method) => ({
17
- id: `@foggy-projects/deepseek-harness-plugin#foggyIntegration/${method}`,
18
- service: 'foggyIntegration',
19
- namespace: 'foggyIntegration',
20
- method,
21
- invocation: { kind: 'direct' },
22
- parameters: [],
23
- result: {
24
- mode: 'strict',
25
- typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyIntegrationResult',
26
- schema: passthroughSchema,
27
- },
28
- sourceLocation: { file: 'lib/index.js', line: 1, column: 1 },
29
- })),
40
+ ].map((method) => descriptor(method)),
41
+ descriptor('saveRuntimeSettings', [{
42
+ name: 'input',
43
+ wire: 'input',
44
+ source: 'json',
45
+ codec: {
46
+ mode: 'strict',
47
+ typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyRuntimeSettingsInput',
48
+ schema: runtimeSettingsInputSchema,
49
+ },
50
+ }]),
51
+ ],
30
52
  }
31
53
 
32
54
  const zh = {
@@ -73,7 +95,16 @@ window.__ModuleLoader__.load({
73
95
  progressSkills: '注册 DSH Skills',
74
96
  progressState: '写入安装状态',
75
97
  progressComplete: '初始化完成',
98
+ progressRuntimePreflight: '检查 Runtime 状态',
99
+ progressRuntimeLauncher: '启动 Java Launcher',
100
+ progressRuntimeReadiness: '等待 Runtime 就绪',
101
+ progressRuntimeCapabilities: '验证 Runtime 能力',
102
+ progressRuntimeState: '保存 Runtime 状态',
103
+ progressRuntimeComplete: 'Runtime 已就绪',
76
104
  progressFiles: '文件',
105
+ progressElapsed: '已等待',
106
+ progressTimeout: '最长',
107
+ progressSeconds: '秒',
77
108
  managedPrivate: 'Foggy 私有',
78
109
  configuredOverride: '外部指定',
79
110
  accepted: '操作已开始;页面会自动刷新状态。',
@@ -90,6 +121,10 @@ window.__ModuleLoader__.load({
90
121
  noOnboarding: '尚无数据库引导记录;请在对话中调用 Foggy onboarding Skill。',
91
122
  projectRoot: '工作区',
92
123
  resumeInChat: '未完成步骤应回到对应工作区,在对话中继续。',
124
+ queryWarningTitle: '查询输入已自动兼容',
125
+ queryWarningSummary: 'Runtime 在 WARN 模式下忽略了未知的查询属性,查询仍已执行。请检查告警代码与证据后决定是否修正模型或请求。',
126
+ queryWarningCodes: '告警代码',
127
+ queryWarningEvidence: '证据目录',
93
128
  stepPlanned: '规划',
94
129
  stepDatasourceConfigured: '数据库连接',
95
130
  stepDatasourceVerified: '连接验证',
@@ -99,6 +134,21 @@ window.__ModuleLoader__.load({
99
134
  stepSemanticPublished: '发布',
100
135
  stepSemanticVerified: '首次查询',
101
136
  runtimeUrl: 'Runtime 地址',
137
+ runtimeSettingsTitle: 'Runtime 连接设置',
138
+ runtimePort: 'Runtime 端口',
139
+ runtimePortHint: '端口会持久保存,CLI 与 Skill 将使用同一个 Runtime 地址。',
140
+ configuredRuntimeUrl: '下次启动地址',
141
+ saveRuntimePort: '保存端口',
142
+ runtimePortSaved: 'Runtime 端口已保存',
143
+ stopToChangePort: 'Runtime 运行时不能修改端口,请先停止 Runtime。',
144
+ errorTitle: 'Foggy 操作失败',
145
+ portUnavailableTitle: 'Runtime 端口被占用',
146
+ portUnavailableMessage: '已被其他程序或 Windows 端口代理占用,Runtime 无法启动。',
147
+ portUnavailableHelp: '请更换端口后保存并重新启动;也可以先释放占用该端口的程序或 Windows 端口代理。',
148
+ portInvalidTitle: '端口配置无效',
149
+ portInvalidMessage: '当前值无法保存为 Runtime 端口。',
150
+ portInvalidHelp: '请输入 1024 到 65535 之间的整数端口。',
151
+ settingsInvalidTitle: 'Runtime 设置文件异常',
102
152
  nextTitle: '后续配置',
103
153
  nextCopy: 'Skills 已通过 DeepSeek Harness 原生注册表提供给所有工作区;每个会话直接使用自己的工作目录。Runtime 启动成功后,将继续进入数据库连接与语义层向导。',
104
154
  beta: 'Beta',
@@ -148,7 +198,16 @@ window.__ModuleLoader__.load({
148
198
  progressSkills: 'Registering DSH Skills',
149
199
  progressState: 'Writing install state',
150
200
  progressComplete: 'Initialization complete',
201
+ progressRuntimePreflight: 'Checking Runtime state',
202
+ progressRuntimeLauncher: 'Starting Java Launcher',
203
+ progressRuntimeReadiness: 'Waiting for Runtime readiness',
204
+ progressRuntimeCapabilities: 'Verifying Runtime capabilities',
205
+ progressRuntimeState: 'Saving Runtime state',
206
+ progressRuntimeComplete: 'Runtime ready',
151
207
  progressFiles: 'files',
208
+ progressElapsed: 'elapsed',
209
+ progressTimeout: 'timeout',
210
+ progressSeconds: 's',
152
211
  managedPrivate: 'Foggy private',
153
212
  configuredOverride: 'Configured override',
154
213
  accepted: 'The operation started; this page will refresh automatically.',
@@ -165,6 +224,10 @@ window.__ModuleLoader__.load({
165
224
  noOnboarding: 'No database onboarding record yet. Invoke the Foggy onboarding Skill in a conversation.',
166
225
  projectRoot: 'Workspace',
167
226
  resumeInChat: 'Resume incomplete steps from a conversation in the matching workspace.',
227
+ queryWarningTitle: 'Query input was normalized',
228
+ queryWarningSummary: 'In WARN mode, Runtime ignored unknown query properties and continued. Review the warning codes and evidence before deciding whether to change the model or request.',
229
+ queryWarningCodes: 'Warning codes',
230
+ queryWarningEvidence: 'Evidence directory',
168
231
  stepPlanned: 'Plan',
169
232
  stepDatasourceConfigured: 'Database connection',
170
233
  stepDatasourceVerified: 'Connection test',
@@ -174,6 +237,21 @@ window.__ModuleLoader__.load({
174
237
  stepSemanticPublished: 'Publish',
175
238
  stepSemanticVerified: 'First query',
176
239
  runtimeUrl: 'Runtime URL',
240
+ runtimeSettingsTitle: 'Runtime connection settings',
241
+ runtimePort: 'Runtime port',
242
+ runtimePortHint: 'The port is persisted so the CLI and Skills use one stable Runtime URL.',
243
+ configuredRuntimeUrl: 'Next startup URL',
244
+ saveRuntimePort: 'Save port',
245
+ runtimePortSaved: 'Runtime port saved',
246
+ stopToChangePort: 'Stop Runtime before changing its port.',
247
+ errorTitle: 'Foggy operation failed',
248
+ portUnavailableTitle: 'Runtime port is already in use',
249
+ portUnavailableMessage: 'is already used by another application or Windows port proxy, so Runtime cannot start.',
250
+ portUnavailableHelp: 'Save a different port and start again, or release the application or Windows port proxy using this port.',
251
+ portInvalidTitle: 'Invalid port setting',
252
+ portInvalidMessage: 'The current value cannot be saved as the Runtime port.',
253
+ portInvalidHelp: 'Enter an integer port between 1024 and 65535.',
254
+ settingsInvalidTitle: 'Runtime settings file is invalid',
177
255
  nextTitle: 'Next configuration',
178
256
  nextCopy: 'Skills are provided to every workspace through the native DeepSeek Harness registry, and each session uses its own working directory. After Runtime starts, the database connection and semantic-layer wizard comes next.',
179
257
  beta: 'Beta',
@@ -188,8 +266,10 @@ window.__ModuleLoader__.load({
188
266
  .foggy-actions{display:flex;gap:8px;flex-wrap:wrap}.foggy-button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:7px 12px;cursor:pointer}.foggy-button:hover{background:var(--dsw-alias-interactive-bg-hover)}.foggy-button:disabled{opacity:.55;cursor:not-allowed}.foggy-button-primary{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary);color:white}
189
267
  .foggy-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.foggy-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-card-head{display:flex;justify-content:space-between;gap:8px}.foggy-card strong{font-size:13px}.foggy-badge{font-size:11px;color:var(--dsw-alias-label-secondary)}.foggy-card code{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;overflow-wrap:anywhere}
190
268
  .foggy-progress{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:13px 14px}.foggy-progress-head{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px}.foggy-progress-head strong{font-weight:600}.foggy-progress-percent{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}.foggy-progress-track{height:8px;margin-top:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.foggy-progress-fill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.foggy-progress-meta{display:flex;justify-content:space-between;gap:12px;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.foggy-progress-file{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
191
- .foggy-message{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}.foggy-message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.foggy-paths,.foggy-next,.foggy-section{border-top:1px solid var(--dsw-alias-border-l2);padding-top:14px}.foggy-paths h4,.foggy-next h4,.foggy-section h4{margin:0 0 8px;font-size:13px}.foggy-paths dl{display:grid;grid-template-columns:100px minmax(0,1fr);gap:6px 10px;margin:0}.foggy-paths dt{color:var(--dsw-alias-label-tertiary);font-size:12px}.foggy-paths dd{margin:0;min-width:0;overflow-wrap:anywhere;font-family:var(--ds-font-family-code);font-size:11px}.foggy-next p,.foggy-section p{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}.foggy-section-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.foggy-onboarding-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.foggy-onboarding-head code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.foggy-step-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;margin-top:10px}.foggy-step{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.foggy-step[data-state=completed]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,var(--dsw-alias-border-l2));color:var(--dsw-alias-state-success-primary)}.foggy-step[data-state=failed],.foggy-step[data-state=invalid]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.foggy-warning{border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary) 55%,var(--dsw-alias-border-l2));background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);border-radius:10px;padding:12px}
192
- @media (width<=680px){.foggy-grid{grid-template-columns:minmax(0,1fr)}}
269
+ .foggy-runtime-settings{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:14px}.foggy-runtime-settings h4{margin:0;font-size:13px}.foggy-runtime-settings-row{display:grid;grid-template-columns:minmax(150px,220px) minmax(0,1fr) auto;align-items:end;gap:10px;margin-top:10px}.foggy-field{display:flex;flex-direction:column;gap:6px}.foggy-field label{font-size:12px;font-weight:600}.foggy-input{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-variant-numeric:tabular-nums;padding:8px 10px;outline:none}.foggy-input:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 18%,transparent)}.foggy-input[aria-invalid=true]{border-color:var(--dsw-alias-state-error-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-error-primary) 14%,transparent)}.foggy-setting-meta{display:flex;flex-direction:column;gap:4px;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:11px}.foggy-setting-meta code{overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary)}
270
+ .foggy-error-panel{border:2px solid var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,var(--dsw-alias-bg-layer-3));border-radius:10px;padding:13px 14px;box-shadow:0 6px 18px color-mix(in srgb,var(--dsw-alias-state-error-primary) 13%,transparent)}.foggy-error-panel-head{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-state-error-primary);font-size:14px;font-weight:700}.foggy-error-icon{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--dsw-alias-state-error-primary);color:white;font-size:14px}.foggy-error-panel p{margin:7px 0 0;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;overflow-wrap:anywhere}.foggy-error-help{color:var(--dsw-alias-state-error-primary)!important;font-weight:600}
271
+ .foggy-message{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}.foggy-message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.foggy-paths,.foggy-next,.foggy-section{border-top:1px solid var(--dsw-alias-border-l2);padding-top:14px}.foggy-paths h4,.foggy-next h4,.foggy-section h4{margin:0 0 8px;font-size:13px}.foggy-paths dl{display:grid;grid-template-columns:100px minmax(0,1fr);gap:6px 10px;margin:0}.foggy-paths dt{color:var(--dsw-alias-label-tertiary);font-size:12px}.foggy-paths dd{margin:0;min-width:0;overflow-wrap:anywhere;font-family:var(--ds-font-family-code);font-size:11px}.foggy-next p,.foggy-section p{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}.foggy-section-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.foggy-onboarding-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.foggy-onboarding-head code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.foggy-step-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;margin-top:10px}.foggy-step{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.foggy-step[data-state=completed]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,var(--dsw-alias-border-l2));color:var(--dsw-alias-state-success-primary)}.foggy-step[data-state=failed],.foggy-step[data-state=invalid]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.foggy-warning{border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary) 55%,var(--dsw-alias-border-l2));background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);border-radius:10px;padding:12px}.foggy-query-warning{margin-top:10px}.foggy-query-warning p+p{margin-top:5px}.foggy-query-warning code{overflow-wrap:anywhere}
272
+ @media (width<=680px){.foggy-grid{grid-template-columns:minmax(0,1fr)}.foggy-runtime-settings-row{grid-template-columns:minmax(0,1fr)}.foggy-runtime-settings-row .foggy-button{width:100%}}
193
273
  `
194
274
 
195
275
  if (typeof document !== 'undefined' && !document.querySelector('style[data-foggy-settings]')) {
@@ -247,21 +327,33 @@ window.__ModuleLoader__.load({
247
327
  'workspace-skills': 'progressSkills',
248
328
  state: 'progressState',
249
329
  complete: 'progressComplete',
330
+ 'runtime-preflight': 'progressRuntimePreflight',
331
+ 'runtime-launcher': 'progressRuntimeLauncher',
332
+ 'runtime-readiness': 'progressRuntimeReadiness',
333
+ 'runtime-capabilities': 'progressRuntimeCapabilities',
334
+ 'runtime-state': 'progressRuntimeState',
335
+ 'runtime-complete': 'progressRuntimeComplete',
250
336
  }
251
337
  const phase = t(phaseKeys[progress.phase] || 'progressWorking')
338
+ const headline = String(progress.phase || '').startsWith('runtime-') ? phase : progress.message || phase
252
339
  const step = progress.step?.index && progress.step?.total
253
340
  ? `${progress.step.index}/${progress.step.total}`
254
341
  : ''
255
342
  const files = progress.files?.total
256
343
  ? `${progress.files.completed || 0}/${progress.files.total} ${t('progressFiles')}`
257
344
  : ''
345
+ const elapsed = Number(progress.timing?.elapsedSeconds)
346
+ const timeout = Number(progress.timing?.timeoutSeconds)
347
+ const timing = Number.isFinite(elapsed)
348
+ ? `${t('progressElapsed')} ${elapsed}${t('progressSeconds')}${Number.isFinite(timeout) ? ` / ${t('progressTimeout')} ${timeout}${t('progressSeconds')}` : ''}`
349
+ : ''
258
350
  return jsxs('div', {
259
351
  className: 'foggy-progress',
260
352
  role: 'status',
261
353
  'aria-live': 'polite',
262
354
  children: [
263
355
  jsxs('div', { className: 'foggy-progress-head', children: [
264
- jsx('strong', { children: progress.message || phase }),
356
+ jsx('strong', { children: headline }),
265
357
  jsx('span', { className: 'foggy-progress-percent', children: `${percent}%` }),
266
358
  ] }),
267
359
  jsx('div', {
@@ -275,7 +367,7 @@ window.__ModuleLoader__.load({
275
367
  }),
276
368
  jsxs('div', { className: 'foggy-progress-meta', children: [
277
369
  jsx('span', { children: phase }),
278
- jsx('span', { children: [step, files].filter(Boolean).join(' · ') }),
370
+ jsx('span', { children: [step, files, timing].filter(Boolean).join(' · ') }),
279
371
  ] }),
280
372
  progress.currentFile ? jsx('div', { className: 'foggy-progress-meta foggy-progress-file', title: progress.currentFile, children: progress.currentFile }) : null,
281
373
  ],
@@ -313,22 +405,54 @@ window.__ModuleLoader__.load({
313
405
  title: profile.steps?.[name]?.status || 'pending',
314
406
  children: t(label),
315
407
  }, name)) }),
408
+ profile.warningCount > 0 ? jsxs('div', { className: 'foggy-warning foggy-query-warning', role: 'status', children: [
409
+ jsx('h4', { children: `${t('queryWarningTitle')} · ${profile.warningCount}` }),
410
+ jsx('p', { children: t('queryWarningSummary') }),
411
+ profile.warningCodes?.length ? jsxs('p', { children: [`${t('queryWarningCodes')}: `, jsx('code', { children: profile.warningCodes.join(', ') })] }) : null,
412
+ profile.warningEvidence ? jsxs('p', { children: [`${t('queryWarningEvidence')}: `, jsx('code', { children: profile.warningEvidence })] }) : null,
413
+ ] }) : null,
316
414
  profile.next?.status !== 'completed' ? jsx('p', { style: { marginTop: '9px' }, children: t('resumeInChat') }) : null,
317
415
  ] })
318
416
  }
319
417
 
418
+ function errorPanel(title, message, help) {
419
+ if (!message) return null
420
+ return jsxs('div', {
421
+ className: 'foggy-error-panel',
422
+ role: 'alert',
423
+ 'aria-live': 'assertive',
424
+ children: [
425
+ jsxs('div', { className: 'foggy-error-panel-head', children: [
426
+ jsx('span', { className: 'foggy-error-icon', 'aria-hidden': true, children: '!' }),
427
+ jsx('strong', { children: title }),
428
+ ] }),
429
+ jsx('p', { children: message }),
430
+ help ? jsx('p', { className: 'foggy-error-help', children: help }) : null,
431
+ ],
432
+ })
433
+ }
434
+
320
435
  function FoggySettingsTab({ api, t }) {
321
- const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false })
436
+ const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false, errorCode: null })
437
+ const [portDraft, setPortDraft] = useState('')
438
+ const [portDirty, setPortDirty] = useState(false)
322
439
 
323
440
  const refresh = useCallback(async () => {
324
441
  setView((current) => ({ ...current, phase: current.status ? 'ready' : 'loading', error: false }))
325
442
  try {
326
443
  const status = unwrap(await api.status(), 'status')
327
- setView((current) => ({ ...current, phase: 'ready', status, error: false }))
444
+ setView((current) => ({
445
+ ...current,
446
+ phase: 'ready',
447
+ status,
448
+ error: false,
449
+ message: current.message === t('accepted') && status.operation?.state !== 'running' ? '' : current.message,
450
+ }))
451
+ if (!portDirty) setPortDraft(String(status.runtimeSettings?.port ?? 18166))
328
452
  } catch (error) {
329
453
  setView((current) => ({ ...current, phase: current.status ? 'ready' : 'error', message: String(error.message || error), error: true }))
330
454
  }
331
- }, [api])
455
+ }, [api, portDirty, t])
332
456
 
333
457
  useEffect(() => { refresh() }, [refresh])
334
458
  useEffect(() => {
@@ -338,18 +462,40 @@ window.__ModuleLoader__.load({
338
462
  }, [refresh, view.status?.operation?.state])
339
463
 
340
464
  const run = async (method) => {
341
- setView((current) => ({ ...current, message: t('working'), error: false }))
465
+ setView((current) => ({ ...current, message: t('working'), error: false, errorCode: null }))
342
466
  try {
343
467
  const result = unwrap(await api[method](), method)
344
468
  const message = result.accepted
345
469
  ? t('accepted')
346
470
  : result.path
347
471
  ? `${t('diagnosticsSaved')}: ${result.path}`
348
- : ''
349
- setView((current) => ({ ...current, message, error: result.success === false }))
472
+ : result.error?.message || ''
473
+ setView((current) => ({ ...current, message, error: result.success === false, errorCode: result.error?.code || null }))
350
474
  await refresh()
351
475
  } catch (error) {
352
- setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true }))
476
+ setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true, errorCode: 'REMOTE_ERROR' }))
477
+ }
478
+ }
479
+
480
+ const saveRuntimePort = async () => {
481
+ const port = Number(portDraft)
482
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
483
+ setView((current) => ({ ...current, message: t('portInvalidMessage'), error: true, errorCode: 'RUNTIME_PORT_INVALID' }))
484
+ return
485
+ }
486
+ setView((current) => ({ ...current, message: t('working'), error: false, errorCode: null }))
487
+ try {
488
+ const result = unwrap(await api.saveRuntimeSettings({ port }), 'saveRuntimeSettings')
489
+ if (result.success === false) {
490
+ setView((current) => ({ ...current, message: result.error?.message || t('actionFailed'), error: true, errorCode: result.error?.code || null }))
491
+ return
492
+ }
493
+ setPortDirty(false)
494
+ setPortDraft(String(result.settings.port))
495
+ setView((current) => ({ ...current, message: `${t('runtimePortSaved')}: ${result.settings.runtimeUrl}`, error: false, errorCode: null }))
496
+ await refresh()
497
+ } catch (error) {
498
+ setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true, errorCode: 'REMOTE_ERROR' }))
353
499
  }
354
500
  }
355
501
 
@@ -361,9 +507,38 @@ window.__ModuleLoader__.load({
361
507
 
362
508
  const status = view.status
363
509
  const busy = status.operation?.state === 'running'
510
+ const portNumber = Number(portDraft)
511
+ const portValid = Number.isInteger(portNumber) && portNumber >= 1024 && portNumber <= 65535
364
512
  const operationError = status.operation?.state === 'failed'
365
513
  ? status.operation.error || status.operation.result?.error?.message || t('actionFailed')
366
514
  : ''
515
+ const operationErrorCode = status.operation?.state === 'failed' ? status.operation.result?.error?.code || null : null
516
+ const settingsError = status.runtimeSettings?.valid === false
517
+ ? status.runtimeSettings.error || t('settingsInvalidTitle')
518
+ : ''
519
+ const portConflict = status.runtimeSettings?.conflictApplies ? status.runtimeSettings.lastConflict : null
520
+ const validationError = portDirty && !portValid ? t('portInvalidMessage') : ''
521
+ const rawAlertMessage = validationError || (view.error ? view.message : '') || operationError || settingsError || portConflict?.message || ''
522
+ const alertCode = validationError
523
+ ? 'RUNTIME_PORT_INVALID'
524
+ : view.error
525
+ ? view.errorCode
526
+ : operationErrorCode || (settingsError ? 'RUNTIME_SETTINGS_INVALID' : portConflict ? 'RUNTIME_PORT_UNAVAILABLE' : null)
527
+ const alertTitle = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
528
+ ? t('portUnavailableTitle')
529
+ : alertCode === 'RUNTIME_PORT_INVALID'
530
+ ? t('portInvalidTitle')
531
+ : alertCode === 'RUNTIME_SETTINGS_INVALID'
532
+ ? t('settingsInvalidTitle')
533
+ : t('errorTitle')
534
+ const alertMessage = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
535
+ ? `${t('runtimePort')} ${portConflict?.port ?? status.runtimeSettings?.port ?? portNumber} ${t('portUnavailableMessage')}`
536
+ : rawAlertMessage
537
+ const alertHelp = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
538
+ ? t('portUnavailableHelp')
539
+ : alertCode === 'RUNTIME_PORT_INVALID'
540
+ ? t('portInvalidHelp')
541
+ : ''
367
542
  const progress = busy ? progressPanel(status.operation?.progress, t) : null
368
543
  const stateLabels = { 'not-installed': 'notInstalled', degraded: 'degraded', ready: 'ready', running: 'running' }
369
544
  return jsxs('section', { className: 'foggy-settings', children: [
@@ -382,8 +557,46 @@ window.__ModuleLoader__.load({
382
557
  status.running ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('runtimeStop'), children: t('stop') }) : null,
383
558
  ] }),
384
559
  ] }),
560
+ errorPanel(alertTitle, alertMessage, alertHelp),
385
561
  progress,
386
- busy || operationError || view.message ? jsx('p', { className: 'foggy-message', 'data-error': operationError || view.error ? 'true' : undefined, children: busy ? t('working') : operationError || view.message }) : null,
562
+ busy ? jsx('p', { className: 'foggy-message', children: t('working') }) : null,
563
+ !busy && view.message && !view.error && !operationError ? jsx('p', { className: 'foggy-message', children: view.message }) : null,
564
+ jsxs('div', { className: 'foggy-runtime-settings', children: [
565
+ jsx('h4', { children: t('runtimeSettingsTitle') }),
566
+ jsxs('div', { className: 'foggy-runtime-settings-row', children: [
567
+ jsxs('div', { className: 'foggy-field', children: [
568
+ jsx('label', { htmlFor: 'foggy-runtime-port', children: t('runtimePort') }),
569
+ jsx('input', {
570
+ id: 'foggy-runtime-port',
571
+ className: 'foggy-input',
572
+ type: 'number',
573
+ min: 1024,
574
+ max: 65535,
575
+ step: 1,
576
+ value: portDraft,
577
+ disabled: busy || status.running,
578
+ 'aria-invalid': portDirty && !portValid ? 'true' : undefined,
579
+ onChange: (event) => {
580
+ setPortDraft(event.target.value)
581
+ setPortDirty(true)
582
+ setView((current) => ({ ...current, message: '', error: false, errorCode: null }))
583
+ },
584
+ }),
585
+ ] }),
586
+ jsxs('div', { className: 'foggy-setting-meta', children: [
587
+ jsx('span', { children: t('runtimePortHint') }),
588
+ jsx('code', { children: `${t('configuredRuntimeUrl')}: http://127.0.0.1:${portValid ? portNumber : '—'}` }),
589
+ status.running ? jsx('span', { children: t('stopToChangePort') }) : null,
590
+ ] }),
591
+ jsx('button', {
592
+ className: 'foggy-button foggy-button-primary',
593
+ type: 'button',
594
+ disabled: busy || status.running || !portDirty || !portValid,
595
+ onClick: saveRuntimePort,
596
+ children: t('saveRuntimePort'),
597
+ }),
598
+ ] }),
599
+ ] }),
387
600
  jsxs('div', { className: 'foggy-grid', children: [
388
601
  componentCard(t('python'), status.components.python, 'tool', t),
389
602
  componentCard(t('java'), status.components.java, 'tool', t),
@@ -0,0 +1,86 @@
1
+ import { open } from 'node:fs/promises'
2
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path'
3
+
4
+ const DEFAULT_MAX_BYTES = 32 * 1024
5
+ const DEFAULT_MAX_LINES = 120
6
+ const ANSI_ESCAPE = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g
7
+ const BEARER_SECRET = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi
8
+ const API_KEY_SECRET = /\bsk-[A-Za-z0-9._-]{12,}/g
9
+ const JDBC_URL = /\bjdbc:[^\s"'<>]+/gi
10
+ const URL_CREDENTIALS = /(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi
11
+ const SENSITIVE_QUERY_VALUE = /([?&](?:password|passwd|pwd|secret|token|api[_-]?key|auth[_-]?code)=)[^&#\s]*/gi
12
+ const SENSITIVE_ASSIGNMENT = /\b([A-Za-z0-9_.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|authorization|auth[_-]?code)[A-Za-z0-9_.-]*)(\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi
13
+
14
+ export function sanitizeDiagnosticText(value) {
15
+ return String(value ?? '')
16
+ .replace(ANSI_ESCAPE, '')
17
+ .replace(BEARER_SECRET, 'Bearer [REDACTED]')
18
+ .replace(API_KEY_SECRET, '[REDACTED_API_KEY]')
19
+ .replace(JDBC_URL, 'jdbc:[REDACTED_CONNECTION_URL]')
20
+ .replace(URL_CREDENTIALS, '$1[REDACTED]@')
21
+ .replace(SENSITIVE_QUERY_VALUE, '$1[REDACTED]')
22
+ .replace(SENSITIVE_ASSIGNMENT, '$1$2[REDACTED]')
23
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
24
+ }
25
+
26
+ function isInside(root, candidate) {
27
+ const value = relative(resolve(root), resolve(candidate))
28
+ return value !== '' && value !== '..' && !value.startsWith(`..${sep}`) && !isAbsolute(value)
29
+ }
30
+
31
+ export async function readDiagnosticLogTail(path, {
32
+ allowedRoot,
33
+ maxBytes = DEFAULT_MAX_BYTES,
34
+ maxLines = DEFAULT_MAX_LINES,
35
+ } = {}) {
36
+ if (!path || !allowedRoot || !isInside(allowedRoot, path)) {
37
+ return { sanitizedTail: '', tailTruncated: false, tailError: 'LOG_PATH_OUTSIDE_RUNTIME_ROOT' }
38
+ }
39
+
40
+ let handle
41
+ try {
42
+ handle = await open(path, 'r')
43
+ const details = await handle.stat()
44
+ const bytesToRead = Math.min(details.size, maxBytes)
45
+ const buffer = Buffer.alloc(bytesToRead)
46
+ if (bytesToRead > 0) await handle.read(buffer, 0, bytesToRead, details.size - bytesToRead)
47
+ let text = buffer.toString('utf8')
48
+ let truncated = details.size > bytesToRead
49
+ if (truncated) {
50
+ const firstLineEnd = text.indexOf('\n')
51
+ if (firstLineEnd >= 0) text = text.slice(firstLineEnd + 1)
52
+ }
53
+ const lines = text.split(/\r?\n/)
54
+ if (lines.length > maxLines) {
55
+ text = lines.slice(-maxLines).join('\n')
56
+ truncated = true
57
+ }
58
+ return {
59
+ sanitizedTail: sanitizeDiagnosticText(text).trim(),
60
+ tailTruncated: truncated,
61
+ tailBytesRead: bytesToRead,
62
+ }
63
+ } catch (error) {
64
+ return {
65
+ sanitizedTail: '',
66
+ tailTruncated: false,
67
+ tailError: error?.code || 'LOG_READ_FAILED',
68
+ }
69
+ } finally {
70
+ await handle?.close()
71
+ }
72
+ }
73
+
74
+ export async function enrichRuntimeStartFailure(failure, dataRoot) {
75
+ if (!failure || typeof failure !== 'object') return failure
76
+ const runtimeRoot = join(dataRoot, 'runtime')
77
+ const logs = Array.isArray(failure.logs)
78
+ ? await Promise.all(failure.logs.map(async (entry) => ({
79
+ ...entry,
80
+ ...(entry?.exists && entry?.path
81
+ ? await readDiagnosticLogTail(entry.path, { allowedRoot: runtimeRoot })
82
+ : {}),
83
+ })))
84
+ : []
85
+ return { ...failure, logs }
86
+ }