@foggy-projects/deepseek-harness-plugin 0.4.0-beta.13 → 0.4.0-beta.15

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/README.md CHANGED
@@ -14,7 +14,7 @@ modify `PATH`, or register Python globally. Advanced users may explicitly set
14
14
  ## Local beta installation
15
15
 
16
16
  ```powershell
17
- dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.13.tgz
17
+ dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.0-beta.15.tgz
18
18
  ```
19
19
 
20
20
  Restart `dsh web`, use the browser it opens (or the complete printed URL,
@@ -30,9 +30,13 @@ dsh plugin --profile web add --workspace-root @foggy-projects/deepseek-harness-p
30
30
  For development, `FOGGY_ASSET_CACHE_DIRS` can contain platform-delimited verified
31
31
  asset-cache directories. It is not required for a normal install.
32
32
 
33
- Database credentials remain outside the ordinary DSH settings document. The
34
- bundled onboarding Skill drives datasource discovery, semantic drafting,
35
- checkpointed publication, and bounded read-only query verification.
33
+ The bundled onboarding Skill is intentionally development-first. Users may
34
+ provide a local datasource password in the conversation, a user-managed
35
+ connection file, an Agent environment variable, or a compatible Runtime
36
+ Console. Direct passwords are submitted to the already-running Runtime without
37
+ being copied into onboarding state or evidence. The Skill then drives schema
38
+ discovery, semantic drafting, local Bundle registration, and bounded query
39
+ verification without restarting Runtime.
36
40
 
37
41
  ## Native Skill and workspace contract
38
42
 
@@ -42,14 +46,12 @@ are available in every DSH workspace without copying or symlinking `.agents`.
42
46
  The current session `cwd` remains the workspace boundary for semantic drafts and
43
47
  evidence.
44
48
 
45
- Opaque CLI profiles default to the private persistent
46
- `<dataRoot>/cli-profiles` directory. Composite onboarding commands are
49
+ Opaque CLI profiles remain available for users who prefer them and default to
50
+ the private persistent `<dataRoot>/cli-profiles` directory, but they are not a
51
+ prerequisite for ordinary development. Composite onboarding commands are
47
52
  idempotent: unchanged completed phases are resumed rather than re-adding a
48
- datasource or re-registering a published bundle. Settings detects legacy
49
- temporary profiles and offers an explicit, validated migration into the
50
- persistent store. A completed profile can also be bound non-destructively to an
51
- additional DSH workspace when its reviewed connection contract and published
52
- semantic digest are unchanged.
53
+ datasource or re-registering a local Bundle. Settings detects legacy temporary
54
+ profiles and offers an explicit, validated migration into the persistent store.
53
55
 
54
56
  Private Python, CLI, Launcher, the analysis Skill, install state, and Runtime
55
57
  state live in the user-level Foggy component directories. The managed CLI is intentionally isolated
@@ -67,9 +69,17 @@ the managed Runtime directory. Runtime start monitors the Launcher PID during
67
69
  process is verified with `wait-ready` and `capabilities` instead of being treated as a
68
70
  failed second start.
69
71
 
70
- This beta remains a local dev/test integration. The bundled Runtime reports
71
- `securityMode=none-dev-test-only` and must not be exposed to a network or used as
72
- a production service.
72
+ The same settings tab owns a persistent local Runtime port. New installations default
73
+ to `18166`; users can choose another port while Runtime is stopped. Startup checks the
74
+ same wildcard binding used by the Java server, so a conflicting application or Windows
75
+ port proxy produces an immediate, high-visibility error instead of a readiness timeout.
76
+ The CLI and Skills resolve the resulting stable Runtime URL from managed state.
77
+
78
+ This beta remains a local dev/test integration. It does not automatically extend
79
+ local credentials or approvals into a formal environment. Production model
80
+ publication should use a separate manual or dedicated deployment workflow with
81
+ an explicit target, model Git commit/tag, credentials, verification, and
82
+ rollback plan.
73
83
 
74
84
  See [`docs/PUBLIC-BETA-READINESS.md`](./docs/PUBLIC-BETA-READINESS.md) for the
75
85
  tested public Beta scope, release gates, and stable-release blockers.
@@ -1,10 +1,10 @@
1
- # Public beta readiness: 0.4.0-beta.13
1
+ # Public beta readiness: 0.4.0-beta.15
2
2
 
3
3
  Assessment date: 2026-09-04
4
4
 
5
5
  ## Verdict
6
6
 
7
- `0.4.0-beta.13` is suitable for a scoped public beta on Windows 10/11 x64 and
7
+ `0.4.0-beta.15` is suitable for a scoped public beta on Windows 10/11 x64 and
8
8
  Linux/WSL2 x64. It is not a general-availability release.
9
9
 
10
10
  The plugin package stays small and downloads large components only during
@@ -14,7 +14,7 @@ system PATH, registry, or a user's existing Python installation.
14
14
 
15
15
  ## Verified behavior
16
16
 
17
- - The npm tarball is about 67 KB compressed and contains no Python or Java
17
+ - The npm tarball is about 78 KB compressed and contains no Python or Java
18
18
  runtime binary.
19
19
  - Managed CPython assets are pinned by URL, byte size, and SHA256 for Windows,
20
20
  Linux, and macOS on x64 and arm64.
@@ -26,8 +26,22 @@ system PATH, registry, or a user's existing Python installation.
26
26
  - WSL2 reported the existing managed Python, CLI, Launcher, and analysis Skill
27
27
  as ready; an isolated Windows profile correctly reported its fresh component
28
28
  state and rejected the host's Java 12 as below the Java 17 prerequisite.
29
+ - Onboarding is development-first: a direct password or Agent environment
30
+ variable is submitted to the public datasource API of an already-running
31
+ Runtime. The password is excluded from onboarding state and evidence, and
32
+ datasource setup does not trigger Runtime restart. Opaque profiles remain an
33
+ optional advanced path.
34
+ - A real Launcher 0.1.18 smoke on Java 17 accepted a synthetic inline-password
35
+ datasource through the public Runtime API while retaining the same Java PID.
36
+ The isolated registry confirmed the documented local-development plaintext
37
+ persistence boundary; the test datasource was then removed and the temporary
38
+ Runtime stopped.
39
+ - A full isolated `onboard-datasource-run` then created, tested, bound, and
40
+ inspected a SQLite datasource containing one table. Runtime retained the same
41
+ PID, while the persisted onboarding state and command evidence contained no
42
+ copy of the synthetic password.
29
43
  - The UI reported Python 3.12.13 as `Foggy private`, and CLI 0.1.23, Launcher
30
- 0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.13 as installed.
44
+ 0.1.18, analysis Skill 0.1.17, and onboarding Skill 0.4.0-beta.15 as installed.
31
45
  - Runtime startup now reports the current phase, elapsed/timeout seconds, and a
32
46
  bounded progress value. A real Windows launch with Temurin 17 reached readiness
33
47
  and passed capabilities in 18.9 seconds. A controlled post-launch Java exit was
@@ -37,12 +51,20 @@ system PATH, registry, or a user's existing Python installation.
37
51
  error, including the generated local JSON path. Exported failures include
38
52
  bounded sanitized Runtime log tails and reject log paths outside the managed
39
53
  Runtime directory.
54
+ - Runtime port configuration is persisted below the private Foggy data root and
55
+ defaults to `18166`. The settings UI validates `1024–65535`, prevents changes
56
+ while Runtime is active, and shows port conflicts as an assertive, red bordered
57
+ alert. Preflight now checks `0.0.0.0` with exclusive Windows binding semantics,
58
+ matching the Java server closely enough to detect address-specific `portproxy`
59
+ conflicts that a loopback-only probe missed.
40
60
  - Interrupted downloads resume with HTTP Range when the server supports it;
41
61
  downloaded artifacts are not trusted until the complete SHA256 matches.
42
62
  - Windows progress writes tolerate transient sharing violations without losing
43
63
  the previous valid progress document or leaving temporary files behind.
44
- - Automated regression passed on Windows and Linux/WSL2: 17 Node tests and 10
45
- Python tests on each platform, plus shell and PowerShell syntax checks.
64
+ - Current automated regression passes 20 Node tests and 20 Python tests. The
65
+ new datasource tests cover secret-free persisted state, direct Runtime API
66
+ submission, CLI bypass for inline development credentials, and output
67
+ redaction.
46
68
 
47
69
  ## Beta boundaries
48
70
 
@@ -57,6 +79,10 @@ system PATH, registry, or a user's existing Python installation.
57
79
  - Runtime production authentication is outside this beta's default local
58
80
  onboarding mode. The UI and doctor continue to report `productionReady=false`
59
81
  for the development-only no-auth mode.
82
+ - Direct development passwords are stored by the current Runtime in its private
83
+ local datasource registry. Production publication is a separate manual or
84
+ future dedicated deployment workflow and should use the target environment's
85
+ credential controls.
60
86
 
61
87
  ## Release gate after publication
62
88
 
@@ -1,7 +1,7 @@
1
1
  # Native Windows Beta acceptance
2
2
 
3
3
  This runbook validates DeepSeek Harness `0.1.2-rc.1` with Foggy plugin
4
- `0.4.0-beta.13` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
4
+ `0.4.0-beta.15` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
5
5
  directory that is not synchronized by OneDrive.
6
6
 
7
7
  ## Prerequisites
@@ -43,13 +43,18 @@ temporary local access material and do not paste it into chat or diagnostics.
43
43
  Configure an LLM provider, select the `C:\FoggyAcceptance` workspace, then open
44
44
  **Settings → Plugins → Foggy Data Analysis** and choose **Initialize Foggy**.
45
45
 
46
+ Before starting Runtime, confirm the configured port in **Runtime connection
47
+ settings**. A fresh profile defaults to `18166`. To use another port, enter an
48
+ integer from `1024` through `65535` and choose **Save port**. The value remains
49
+ stable across DSH restarts and cannot be changed while Runtime is running.
50
+
46
51
  Expected component state:
47
52
 
48
53
  - Python 3.12.13, source `Foggy private`;
49
54
  - CLI 0.1.23;
50
55
  - Launcher 0.1.18;
51
56
  - analysis Skill 0.1.17;
52
- - onboarding Skill 0.4.0-beta.13;
57
+ - onboarding Skill 0.4.0-beta.15;
53
58
  - Java 17+ available;
54
59
  - native DSH Skill registration available.
55
60
 
@@ -61,23 +66,32 @@ readiness. On failure, export diagnostics before retrying; the report includes
61
66
  the startup phase, PID state, evidence directory, Runtime log locations, and
62
67
  bounded sanitized log tails.
63
68
 
69
+ If the configured port is occupied, startup must fail during preflight instead
70
+ of waiting for the readiness deadline. The settings page displays a red bordered
71
+ alert naming the port conflict and tells the user to save another port or release
72
+ the conflicting application or Windows port proxy. Saving a free port and
73
+ starting again must clear the applicable conflict state.
74
+
64
75
  ## Database and semantic-layer acceptance
65
76
 
66
- Keep database credentials out of this public repository. Create a private CLI
67
- profile outside the DSH conversation, pass only its opaque profile ID and
68
- revision to the onboarding workflow, and use a test-only database account with
69
- read-only permissions.
77
+ Use a development or test database. For this local acceptance run, connection
78
+ details and a password may be supplied directly in the prompt or in a local
79
+ connection JSON file. The onboarding wrapper must configure the datasource
80
+ online without stopping or restarting an already-running Runtime. It must not
81
+ copy the password into onboarding state, evidence, TM/QM files, or Git.
70
82
 
71
83
  Use a natural prompt such as:
72
84
 
73
85
  ```text
74
- 请帮我用 Foggy 分析这个测试数据库。我已经按验收手册创建了私有连接
75
- profile。请先确认连接和表结构,再围绕订单数据创建一个简单的语义层,
76
- 包含订单数量、状态和开单时间,最后执行一个不超过 20 行的只读查询。
77
- 不要修改数据库,也不要在对话或验收文件中输出密码。
86
+ 请帮我用 Foggy 分析这个测试数据库:地址、数据库名、账号和密码分别是
87
+ <连接信息>。直接创建开发数据源并绑定一个合适的 namespace,不要重启
88
+ Runtime。读取表结构后,在当前工作目录创建订单语义模型,包含订单数量、
89
+ 状态和开单时间,调试到模型通过,并执行一个不超过 20 行的只读查询。
78
90
  ```
79
91
 
80
- Pass only when Runtime readiness and capabilities succeed, datasource metadata
81
- can be discovered, TM/QM models validate and publish, the bounded read-only
82
- query succeeds, diagnostics contain no credentials, and stopping Runtime frees
83
- port 18066.
92
+ Pass only when Runtime readiness and capabilities succeed, datasource setup
93
+ does not restart Runtime, metadata can be discovered, TM/QM models validate and
94
+ become effective in the local Runtime, the bounded read-only query succeeds,
95
+ diagnostics contain no credentials, and stopping Runtime frees the configured
96
+ port (default `18166`). Recommend Git management for the completed model files;
97
+ do not treat this acceptance run as production publication.
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  DSH_VERSION="0.1.2-rc.1"
5
5
  PNPM_VERSION="11.7.0"
6
- PLUGIN_VERSION="0.4.0-beta.13"
6
+ PLUGIN_VERSION="0.4.0-beta.15"
7
7
  PLUGIN_REF="v${PLUGIN_VERSION}"
8
8
  PLUGIN_REPOSITORY="https://github.com/foggy-projects/foggy-deepseek-harness-plugin.git"
9
9
 
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 = {
@@ -108,6 +130,21 @@ window.__ModuleLoader__.load({
108
130
  stepSemanticPublished: '发布',
109
131
  stepSemanticVerified: '首次查询',
110
132
  runtimeUrl: 'Runtime 地址',
133
+ runtimeSettingsTitle: 'Runtime 连接设置',
134
+ runtimePort: 'Runtime 端口',
135
+ runtimePortHint: '端口会持久保存,CLI 与 Skill 将使用同一个 Runtime 地址。',
136
+ configuredRuntimeUrl: '下次启动地址',
137
+ saveRuntimePort: '保存端口',
138
+ runtimePortSaved: 'Runtime 端口已保存',
139
+ stopToChangePort: 'Runtime 运行时不能修改端口,请先停止 Runtime。',
140
+ errorTitle: 'Foggy 操作失败',
141
+ portUnavailableTitle: 'Runtime 端口被占用',
142
+ portUnavailableMessage: '已被其他程序或 Windows 端口代理占用,Runtime 无法启动。',
143
+ portUnavailableHelp: '请更换端口后保存并重新启动;也可以先释放占用该端口的程序或 Windows 端口代理。',
144
+ portInvalidTitle: '端口配置无效',
145
+ portInvalidMessage: '当前值无法保存为 Runtime 端口。',
146
+ portInvalidHelp: '请输入 1024 到 65535 之间的整数端口。',
147
+ settingsInvalidTitle: 'Runtime 设置文件异常',
111
148
  nextTitle: '后续配置',
112
149
  nextCopy: 'Skills 已通过 DeepSeek Harness 原生注册表提供给所有工作区;每个会话直接使用自己的工作目录。Runtime 启动成功后,将继续进入数据库连接与语义层向导。',
113
150
  beta: 'Beta',
@@ -192,6 +229,21 @@ window.__ModuleLoader__.load({
192
229
  stepSemanticPublished: 'Publish',
193
230
  stepSemanticVerified: 'First query',
194
231
  runtimeUrl: 'Runtime URL',
232
+ runtimeSettingsTitle: 'Runtime connection settings',
233
+ runtimePort: 'Runtime port',
234
+ runtimePortHint: 'The port is persisted so the CLI and Skills use one stable Runtime URL.',
235
+ configuredRuntimeUrl: 'Next startup URL',
236
+ saveRuntimePort: 'Save port',
237
+ runtimePortSaved: 'Runtime port saved',
238
+ stopToChangePort: 'Stop Runtime before changing its port.',
239
+ errorTitle: 'Foggy operation failed',
240
+ portUnavailableTitle: 'Runtime port is already in use',
241
+ portUnavailableMessage: 'is already used by another application or Windows port proxy, so Runtime cannot start.',
242
+ portUnavailableHelp: 'Save a different port and start again, or release the application or Windows port proxy using this port.',
243
+ portInvalidTitle: 'Invalid port setting',
244
+ portInvalidMessage: 'The current value cannot be saved as the Runtime port.',
245
+ portInvalidHelp: 'Enter an integer port between 1024 and 65535.',
246
+ settingsInvalidTitle: 'Runtime settings file is invalid',
195
247
  nextTitle: 'Next configuration',
196
248
  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.',
197
249
  beta: 'Beta',
@@ -206,8 +258,10 @@ window.__ModuleLoader__.load({
206
258
  .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}
207
259
  .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}
208
260
  .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}
261
+ .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)}
262
+ .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}
209
263
  .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}
210
- @media (width<=680px){.foggy-grid{grid-template-columns:minmax(0,1fr)}}
264
+ @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%}}
211
265
  `
212
266
 
213
267
  if (typeof document !== 'undefined' && !document.querySelector('style[data-foggy-settings]')) {
@@ -347,18 +401,44 @@ window.__ModuleLoader__.load({
347
401
  ] })
348
402
  }
349
403
 
404
+ function errorPanel(title, message, help) {
405
+ if (!message) return null
406
+ return jsxs('div', {
407
+ className: 'foggy-error-panel',
408
+ role: 'alert',
409
+ 'aria-live': 'assertive',
410
+ children: [
411
+ jsxs('div', { className: 'foggy-error-panel-head', children: [
412
+ jsx('span', { className: 'foggy-error-icon', 'aria-hidden': true, children: '!' }),
413
+ jsx('strong', { children: title }),
414
+ ] }),
415
+ jsx('p', { children: message }),
416
+ help ? jsx('p', { className: 'foggy-error-help', children: help }) : null,
417
+ ],
418
+ })
419
+ }
420
+
350
421
  function FoggySettingsTab({ api, t }) {
351
- const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false })
422
+ const [view, setView] = useState({ phase: 'loading', status: null, message: '', error: false, errorCode: null })
423
+ const [portDraft, setPortDraft] = useState('')
424
+ const [portDirty, setPortDirty] = useState(false)
352
425
 
353
426
  const refresh = useCallback(async () => {
354
427
  setView((current) => ({ ...current, phase: current.status ? 'ready' : 'loading', error: false }))
355
428
  try {
356
429
  const status = unwrap(await api.status(), 'status')
357
- setView((current) => ({ ...current, phase: 'ready', status, error: false }))
430
+ setView((current) => ({
431
+ ...current,
432
+ phase: 'ready',
433
+ status,
434
+ error: false,
435
+ message: current.message === t('accepted') && status.operation?.state !== 'running' ? '' : current.message,
436
+ }))
437
+ if (!portDirty) setPortDraft(String(status.runtimeSettings?.port ?? 18166))
358
438
  } catch (error) {
359
439
  setView((current) => ({ ...current, phase: current.status ? 'ready' : 'error', message: String(error.message || error), error: true }))
360
440
  }
361
- }, [api])
441
+ }, [api, portDirty, t])
362
442
 
363
443
  useEffect(() => { refresh() }, [refresh])
364
444
  useEffect(() => {
@@ -368,18 +448,40 @@ window.__ModuleLoader__.load({
368
448
  }, [refresh, view.status?.operation?.state])
369
449
 
370
450
  const run = async (method) => {
371
- setView((current) => ({ ...current, message: t('working'), error: false }))
451
+ setView((current) => ({ ...current, message: t('working'), error: false, errorCode: null }))
372
452
  try {
373
453
  const result = unwrap(await api[method](), method)
374
454
  const message = result.accepted
375
455
  ? t('accepted')
376
456
  : result.path
377
457
  ? `${t('diagnosticsSaved')}: ${result.path}`
378
- : ''
379
- setView((current) => ({ ...current, message, error: result.success === false }))
458
+ : result.error?.message || ''
459
+ setView((current) => ({ ...current, message, error: result.success === false, errorCode: result.error?.code || null }))
380
460
  await refresh()
381
461
  } catch (error) {
382
- setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true }))
462
+ setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true, errorCode: 'REMOTE_ERROR' }))
463
+ }
464
+ }
465
+
466
+ const saveRuntimePort = async () => {
467
+ const port = Number(portDraft)
468
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
469
+ setView((current) => ({ ...current, message: t('portInvalidMessage'), error: true, errorCode: 'RUNTIME_PORT_INVALID' }))
470
+ return
471
+ }
472
+ setView((current) => ({ ...current, message: t('working'), error: false, errorCode: null }))
473
+ try {
474
+ const result = unwrap(await api.saveRuntimeSettings({ port }), 'saveRuntimeSettings')
475
+ if (result.success === false) {
476
+ setView((current) => ({ ...current, message: result.error?.message || t('actionFailed'), error: true, errorCode: result.error?.code || null }))
477
+ return
478
+ }
479
+ setPortDirty(false)
480
+ setPortDraft(String(result.settings.port))
481
+ setView((current) => ({ ...current, message: `${t('runtimePortSaved')}: ${result.settings.runtimeUrl}`, error: false, errorCode: null }))
482
+ await refresh()
483
+ } catch (error) {
484
+ setView((current) => ({ ...current, message: `${t('actionFailed')}: ${String(error.message || error)}`, error: true, errorCode: 'REMOTE_ERROR' }))
383
485
  }
384
486
  }
385
487
 
@@ -391,9 +493,38 @@ window.__ModuleLoader__.load({
391
493
 
392
494
  const status = view.status
393
495
  const busy = status.operation?.state === 'running'
496
+ const portNumber = Number(portDraft)
497
+ const portValid = Number.isInteger(portNumber) && portNumber >= 1024 && portNumber <= 65535
394
498
  const operationError = status.operation?.state === 'failed'
395
499
  ? status.operation.error || status.operation.result?.error?.message || t('actionFailed')
396
500
  : ''
501
+ const operationErrorCode = status.operation?.state === 'failed' ? status.operation.result?.error?.code || null : null
502
+ const settingsError = status.runtimeSettings?.valid === false
503
+ ? status.runtimeSettings.error || t('settingsInvalidTitle')
504
+ : ''
505
+ const portConflict = status.runtimeSettings?.conflictApplies ? status.runtimeSettings.lastConflict : null
506
+ const validationError = portDirty && !portValid ? t('portInvalidMessage') : ''
507
+ const rawAlertMessage = validationError || (view.error ? view.message : '') || operationError || settingsError || portConflict?.message || ''
508
+ const alertCode = validationError
509
+ ? 'RUNTIME_PORT_INVALID'
510
+ : view.error
511
+ ? view.errorCode
512
+ : operationErrorCode || (settingsError ? 'RUNTIME_SETTINGS_INVALID' : portConflict ? 'RUNTIME_PORT_UNAVAILABLE' : null)
513
+ const alertTitle = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
514
+ ? t('portUnavailableTitle')
515
+ : alertCode === 'RUNTIME_PORT_INVALID'
516
+ ? t('portInvalidTitle')
517
+ : alertCode === 'RUNTIME_SETTINGS_INVALID'
518
+ ? t('settingsInvalidTitle')
519
+ : t('errorTitle')
520
+ const alertMessage = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
521
+ ? `${t('runtimePort')} ${portConflict?.port ?? status.runtimeSettings?.port ?? portNumber} ${t('portUnavailableMessage')}`
522
+ : rawAlertMessage
523
+ const alertHelp = alertCode === 'RUNTIME_PORT_UNAVAILABLE'
524
+ ? t('portUnavailableHelp')
525
+ : alertCode === 'RUNTIME_PORT_INVALID'
526
+ ? t('portInvalidHelp')
527
+ : ''
397
528
  const progress = busy ? progressPanel(status.operation?.progress, t) : null
398
529
  const stateLabels = { 'not-installed': 'notInstalled', degraded: 'degraded', ready: 'ready', running: 'running' }
399
530
  return jsxs('section', { className: 'foggy-settings', children: [
@@ -412,10 +543,46 @@ window.__ModuleLoader__.load({
412
543
  status.running ? jsx('button', { className: 'foggy-button', type: 'button', disabled: busy, onClick: () => run('runtimeStop'), children: t('stop') }) : null,
413
544
  ] }),
414
545
  ] }),
546
+ errorPanel(alertTitle, alertMessage, alertHelp),
415
547
  progress,
416
548
  busy ? jsx('p', { className: 'foggy-message', children: t('working') }) : null,
417
- operationError ? jsx('p', { className: 'foggy-message', 'data-error': 'true', children: operationError }) : null,
418
- !busy && view.message ? jsx('p', { className: 'foggy-message', 'data-error': view.error ? 'true' : undefined, children: view.message }) : null,
549
+ !busy && view.message && !view.error && !operationError ? jsx('p', { className: 'foggy-message', children: view.message }) : null,
550
+ jsxs('div', { className: 'foggy-runtime-settings', children: [
551
+ jsx('h4', { children: t('runtimeSettingsTitle') }),
552
+ jsxs('div', { className: 'foggy-runtime-settings-row', children: [
553
+ jsxs('div', { className: 'foggy-field', children: [
554
+ jsx('label', { htmlFor: 'foggy-runtime-port', children: t('runtimePort') }),
555
+ jsx('input', {
556
+ id: 'foggy-runtime-port',
557
+ className: 'foggy-input',
558
+ type: 'number',
559
+ min: 1024,
560
+ max: 65535,
561
+ step: 1,
562
+ value: portDraft,
563
+ disabled: busy || status.running,
564
+ 'aria-invalid': portDirty && !portValid ? 'true' : undefined,
565
+ onChange: (event) => {
566
+ setPortDraft(event.target.value)
567
+ setPortDirty(true)
568
+ setView((current) => ({ ...current, message: '', error: false, errorCode: null }))
569
+ },
570
+ }),
571
+ ] }),
572
+ jsxs('div', { className: 'foggy-setting-meta', children: [
573
+ jsx('span', { children: t('runtimePortHint') }),
574
+ jsx('code', { children: `${t('configuredRuntimeUrl')}: http://127.0.0.1:${portValid ? portNumber : '—'}` }),
575
+ status.running ? jsx('span', { children: t('stopToChangePort') }) : null,
576
+ ] }),
577
+ jsx('button', {
578
+ className: 'foggy-button foggy-button-primary',
579
+ type: 'button',
580
+ disabled: busy || status.running || !portDirty || !portValid,
581
+ onClick: saveRuntimePort,
582
+ children: t('saveRuntimePort'),
583
+ }),
584
+ ] }),
585
+ ] }),
419
586
  jsxs('div', { className: 'foggy-grid', children: [
420
587
  componentCard(t('python'), status.components.python, 'tool', t),
421
588
  componentCard(t('java'), status.components.java, 'tool', t),