@foggy-projects/deepseek-harness-plugin 0.4.2-rc.2 → 0.4.2-rc.5

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
@@ -7,7 +7,7 @@ are downloaded only when the user selects **Initialize and start** (or the
7
7
  equivalent component action). Existing installations are not silently changed
8
8
  when the plugin package is upgraded.
9
9
 
10
- The `0.4.2-rc.2` compatibility candidate targets DeepSeek Harness `0.1.7-rc.2`
10
+ The `0.4.2-rc.5` compatibility candidate targets DeepSeek Harness `0.1.7-rc.2`
11
11
  and requires a system Node.js `>=24.0.0`. Foggy's Java Launcher uses a system
12
12
  Java 17+. Foggy manages its own pinned Python 3.12 runtime
13
13
  inside the per-user component directory; it does not require a system Python,
@@ -17,7 +17,7 @@ modify `PATH`, or register Python globally. Advanced users may explicitly set
17
17
  ## Local release-candidate installation
18
18
 
19
19
  ```powershell
20
- dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.2-rc.2.tgz
20
+ dsh plugin --profile web add --workspace-root ./foggy-projects-deepseek-harness-plugin-0.4.2-rc.5.tgz
21
21
  ```
22
22
 
23
23
  Restart `dsh web`, use the browser it opens (or the complete printed URL,
@@ -28,11 +28,11 @@ Install the exact DSH 0.1.7-rc.2 compatibility candidate
28
28
  so the stable `beta` channel is not changed:
29
29
 
30
30
  ```powershell
31
- dsh plugin --profile web add --workspace-root @foggy-projects/deepseek-harness-plugin@0.4.2-rc.2
31
+ dsh plugin --profile web add --workspace-root @foggy-projects/deepseek-harness-plugin@0.4.2-rc.5
32
32
  ```
33
33
 
34
34
  The `dsh017` npm dist-tag points to the most recently published DSH 0.1.7
35
- candidate (`0.4.2-rc.2` at this release). Check the resolved version before
35
+ candidate (`0.4.2-rc.5` at this release). Check the resolved version before
36
36
  using this moving tag:
37
37
 
38
38
  ```powershell
@@ -130,6 +130,27 @@ same wildcard binding used by the Java server, so a conflicting application or W
130
130
  port proxy produces an immediate, high-visibility error instead of a readiness timeout.
131
131
  The CLI and Skills resolve the resulting stable Runtime URL from managed state.
132
132
 
133
+ For a successful standard QueryModel DSL call made through the native
134
+ `foggy_query` tool, Harness shows a compact result card below the completed
135
+ reply, outside the folded tool trace. A visible "Generated by Foggy plugin"
136
+ label distinguishes this programmatic record of executed results from the AI
137
+ reply. The card shows the model, namespace, row count, an optional five-row
138
+ result preview with DSL/SQL details, and **View data**. If a turn has more than six
139
+ successful queries, the first six are shown until the user expands the list.
140
+ The detailed DSL and execution record remains in the tool trace. Clicking
141
+ **View data** creates a fresh local preview URL each time; the historical card
142
+ remains usable after an earlier URL expires, as long as the Harness transcript
143
+ and the Runtime/model are still available. Individual URLs are not permanent.
144
+ The preview re-executes the DSL against the current model and data rather than
145
+ freezing the original rows. In the lite Runtime the preview cache is bounded,
146
+ process-local, and lost on restart. CTE calls are not included in this first
147
+ DataViewer integration. DataViewer currently displays model-backed fields and
148
+ may omit ad-hoc aggregate columns from a query; the Harness card retains the
149
+ complete executed result and makes this limitation explicit.
150
+ The DataViewer page can export the current filtered and sorted model-backed
151
+ result to CSV (up to 10,000 rows). It refuses larger exports rather than
152
+ silently truncating them.
153
+
133
154
  This beta remains a local dev/test integration. It does not automatically extend
134
155
  local credentials or approvals into a formal environment. Production model
135
156
  publication should use a separate manual or dedicated deployment workflow with
@@ -139,6 +160,8 @@ rollback plan.
139
160
  See [`docs/PUBLIC-BETA-READINESS.md`](./docs/PUBLIC-BETA-READINESS.md) for the
140
161
  tested public Beta scope, release gates, and stable-release blockers.
141
162
  The DSH 0.1.7-rc.2 compatibility candidate is documented in
163
+ [`docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.4.md`](./docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.4.md).
164
+ The previous DSH 0.1.7-rc.2 candidate is documented in
142
165
  [`docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.2.md`](./docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.2.md).
143
166
  The earlier DSH 0.1.5-rc.2 candidate is documented in
144
167
  [`docs/RELEASE-CANDIDATE-0.4.1-DSH-0.1.5-RC.1.md`](./docs/RELEASE-CANDIDATE-0.4.1-DSH-0.1.5-RC.1.md).
@@ -0,0 +1,34 @@
1
+ # Foggy DeepSeek Harness plugin 0.4.2-rc.3
2
+
3
+ This pre-release targets DeepSeek Harness `0.1.7-rc.2` on the `dsh017` npm
4
+ channel. It keeps the `beta`, `rc`, and `dsh015` channels unchanged.
5
+
6
+ ## Changes since 0.4.2-rc.2
7
+
8
+ - Add a native `foggy_query` tool for bounded, read-only standard QueryModel DSL
9
+ validation and execution. Successful calls render a Harness card with the
10
+ model, namespace, row count, DSL, SQL, and result.
11
+ - Create a DataViewer preview URL only when the card's **Open DataViewer**
12
+ action is clicked. Links are short-lived local development previews, not
13
+ frozen result snapshots. An expired link can be regenerated from the card.
14
+ - Update the managed Launcher to `0.1.22`. Its lite profile serves the existing
15
+ DataViewer UI using a bounded in-memory cache without MongoDB, and its start
16
+ scripts bind to loopback. Runtime restarts invalidate preview links.
17
+ - Do not claim CTE preview support in this release. Existing CTE execution
18
+ remains available through its prior CLI path without a DataViewer card.
19
+
20
+ ## Acceptance scope
21
+
22
+ - Plugin checks: `npm run check`, Node tests, Python onboarding tests, and an
23
+ isolated DSH `0.1.7-rc.2` web boot with the Foggy settings tab visible.
24
+ - Launcher: formal release gate, default and Analytics Console smokes, six
25
+ GitHub assets, and SHA-256 verification.
26
+ - End-to-end DSL: an isolated Java Runtime with SQLite demo data validates and
27
+ executes a QueryModel query. A click-generated DataViewer link shows actual
28
+ columns and six rows in the browser.
29
+
30
+ Install the exact version with:
31
+
32
+ ```powershell
33
+ dsh plugin --profile web add --workspace-root @foggy-projects/deepseek-harness-plugin@0.4.2-rc.3
34
+ ```
@@ -0,0 +1,32 @@
1
+ # Foggy DeepSeek Harness plugin 0.4.2-rc.4
2
+
3
+ This pre-release targets DeepSeek Harness `0.1.7-rc.2` and remains on the
4
+ `dsh017` npm channel. The `beta`, `rc`, `dsh015`, and `latest` channels are
5
+ unchanged.
6
+
7
+ ## Fixes since 0.4.2-rc.3
8
+
9
+ - Register `foggy_query` with Harness's existing tool runtime instead of
10
+ installing a second private `@deepseek-ai/dsh-tools` copy. The duplicate
11
+ runtime caused `Cannot read properties of undefined (reading 'prepare')`
12
+ before the DSL query reached Foggy Runtime.
13
+ - Allow a normal Launcher upgrade to replace a same-named asset only when the
14
+ existing file still matches the SHA-256 recorded for the previous installed
15
+ version. The previous file is backed up; unexpected file changes remain
16
+ fail-closed. Downloads are verified before replacing the existing file.
17
+ - Convert Query DSL `groupBy` and `orderBy` string shorthand into equivalent
18
+ object entries when creating a DataViewer preview. A valid, executed query
19
+ is no longer incorrectly labeled as ineligible for preview.
20
+ - Warn that the current DataViewer table can omit ad-hoc aggregate columns
21
+ even though the query card contains the complete result. This is an engine
22
+ display limitation, not a failed Query DSL execution.
23
+
24
+ The DSL-only DataViewer behavior from rc.3 is unchanged: a preview URL is
25
+ created only when the user clicks the successful query card's button. CTE
26
+ preview is not included.
27
+
28
+ Install the exact version with:
29
+
30
+ ```powershell
31
+ dsh plugin --profile web add --workspace-root @foggy-projects/deepseek-harness-plugin@0.4.2-rc.4
32
+ ```
@@ -0,0 +1,27 @@
1
+ # Foggy DeepSeek Harness plugin 0.4.2-rc.5
2
+
3
+ This pre-release targets DeepSeek Harness `0.1.7-rc.2` and stays on the
4
+ dedicated `dsh017` npm channel. The `beta`, `rc`, `dsh015`, and `latest`
5
+ channels remain unchanged.
6
+
7
+ ## Changes since 0.4.2-rc.4
8
+
9
+ - Pin Runtime Launcher `0.1.23`, which includes the lite-profile DataViewer
10
+ preview page without requiring the Analytics Console or MongoDB.
11
+ - The DataViewer page puts readable query details and the executed DSL side by
12
+ side, supports copying DSL, and places refresh and CSV export below the
13
+ details. Query links are created on demand from successful DSL result cards.
14
+ - The table supports local filtering and exports the current filtered result.
15
+ CTE preview is not included. Preview links are process-local development
16
+ aids, not durable production shares.
17
+
18
+ The plugin keeps the current Harness `0.1.7-rc.2` tool integration. The
19
+ Runtime is updated only when the user selects **Update components** or
20
+ **Update and start** in Foggy settings; package installation does not restart
21
+ an existing Runtime.
22
+
23
+ Install the exact version with:
24
+
25
+ ```powershell
26
+ dsh plugin --profile web add --workspace-root @foggy-projects/deepseek-harness-plugin@0.4.2-rc.5
27
+ ```
@@ -1,7 +1,7 @@
1
1
  # Native Windows Beta acceptance
2
2
 
3
3
  This runbook validates DeepSeek Harness `0.1.7-rc.2` with Foggy plugin
4
- `0.4.2-rc.2` on a clean 64-bit Windows 10 or Windows 11 machine. Use a local
4
+ `0.4.2-rc.5` 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
@@ -29,7 +29,7 @@ Set-Location C:\FoggyAcceptance
29
29
 
30
30
  npx --yes --package=@deepseek-ai/dsh@0.1.7-rc.2 --package=pnpm@11.7.0 `
31
31
  dsh plugin --profile web add --workspace-root `
32
- "@foggy-projects/deepseek-harness-plugin@0.4.2-rc.2"
32
+ "@foggy-projects/deepseek-harness-plugin@0.4.2-rc.5"
33
33
 
34
34
  npx --yes --package=@deepseek-ai/dsh@0.1.7-rc.2 --package=pnpm@11.7.0 dsh web
35
35
  ```
@@ -66,9 +66,9 @@ Expected component state:
66
66
 
67
67
  - Python 3.12.13, source `Foggy private`;
68
68
  - CLI 0.1.23;
69
- - Launcher 0.1.21;
69
+ - Launcher 0.1.23;
70
70
  - analysis/query Skills 0.1.18;
71
- - onboarding Skill 0.4.2-rc.2;
71
+ - onboarding Skill 0.4.2-rc.5;
72
72
  - Java 17+ available;
73
73
  - native DSH Skill registration available.
74
74
 
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  DSH_VERSION="0.1.7-rc.2"
5
5
  PNPM_VERSION="11.7.0"
6
- PLUGIN_VERSION="0.4.2-rc.2"
6
+ PLUGIN_VERSION="0.4.2-rc.5"
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
@@ -17,6 +17,15 @@ window.__ModuleLoader__.load({
17
17
  return { port }
18
18
  },
19
19
  }
20
+ const viewerPreviewInputSchema = {
21
+ parse: (value) => {
22
+ if (!value || typeof value.model !== 'string' || typeof value.namespace !== 'string'
23
+ || !value.payload || typeof value.payload !== 'object' || Array.isArray(value.payload)) {
24
+ throw new Error('Invalid DataViewer preview request')
25
+ }
26
+ return value
27
+ },
28
+ }
20
29
  const descriptor = (method, parameters = []) => ({
21
30
  id: `@foggy-projects/deepseek-harness-plugin#foggyIntegration/${method}`,
22
31
  service: 'foggyIntegration',
@@ -50,6 +59,15 @@ window.__ModuleLoader__.load({
50
59
  create: () => runtimeSettingsInputSchema,
51
60
  },
52
61
  }]),
62
+ descriptor('createViewerPreview', [{
63
+ name: 'input', wire: 'input', source: 'json',
64
+ codec: {
65
+ mode: 'strict',
66
+ typeSymbol: '@foggy-projects/deepseek-harness-plugin#FoggyViewerPreviewInput',
67
+ schema: viewerPreviewInputSchema,
68
+ create: () => viewerPreviewInputSchema,
69
+ },
70
+ }]),
53
71
  ],
54
72
  }
55
73
 
@@ -149,6 +167,28 @@ window.__ModuleLoader__.load({
149
167
  queryWarningSummary: 'Runtime 在 WARN 模式下忽略了未知的查询属性,查询仍已执行。请检查告警代码与证据后决定是否修正模型或请求。',
150
168
  queryWarningCodes: '告警代码',
151
169
  queryWarningEvidence: '证据目录',
170
+ queryCardTitle: 'Foggy DSL 查询',
171
+ queryCardRunning: '正在校验并执行…',
172
+ queryCardDone: '校验与查询成功',
173
+ queryCardFailed: '查询失败;可展开查看错误',
174
+ queryCardRows: '返回行数',
175
+ queryCardDetails: '查看 DSL 与执行结果',
176
+ queryCardOpen: '查看数据',
177
+ queryCardOpening: '正在生成预览链接…',
178
+ queryCardPreviewHint: '每次点击都会生成新的预览链接。',
179
+ queryCardComputedHint: 'DataViewer 可能不显示临时聚合列;请以本次执行结果为准。',
180
+ queryCardUnsupported: '当前查询不支持 DataViewer 预览,可继续查看查询结果。',
181
+ queryCardResults: 'Foggy 查询结果',
182
+ queryCardProvenance: 'Foggy 插件生成',
183
+ queryCardSource: '来自实际执行结果,非 AI 撰写',
184
+ queryCardShowAll: '显示全部 {count} 条查询',
185
+ queryCardShowLess: '收起至前 6 条',
186
+ queryCardShowResult: '查看结果',
187
+ queryCardHideResult: '收起结果',
188
+ queryCardRowUnit: '行',
189
+ queryCardDsl: 'DSL / SQL',
190
+ queryCardNoRows: '没有返回数据',
191
+ queryCardTableMore: '仅预览前 {count} 行',
152
192
  stepPlanned: '规划',
153
193
  stepDatasourceConfigured: '数据库连接',
154
194
  stepDatasourceVerified: '连接验证',
@@ -274,6 +314,28 @@ window.__ModuleLoader__.load({
274
314
  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.',
275
315
  queryWarningCodes: 'Warning codes',
276
316
  queryWarningEvidence: 'Evidence directory',
317
+ queryCardTitle: 'Foggy DSL query',
318
+ queryCardRunning: 'Validating and executing…',
319
+ queryCardDone: 'Validation and query succeeded',
320
+ queryCardFailed: 'Query failed; expand for details',
321
+ queryCardRows: 'Rows',
322
+ queryCardDetails: 'Show DSL and execution result',
323
+ queryCardOpen: 'View data',
324
+ queryCardOpening: 'Creating preview link…',
325
+ queryCardPreviewHint: 'A fresh preview link is created on every click.',
326
+ queryCardComputedHint: 'DataViewer may omit ad-hoc aggregate columns; use this execution result as the source of truth.',
327
+ queryCardUnsupported: 'DataViewer preview is unavailable for this query; the query result remains available.',
328
+ queryCardResults: 'Foggy query results',
329
+ queryCardProvenance: 'Generated by Foggy plugin',
330
+ queryCardSource: 'From executed queries, not AI-written',
331
+ queryCardShowAll: 'Show all {count} queries',
332
+ queryCardShowLess: 'Show first 6',
333
+ queryCardShowResult: 'View result',
334
+ queryCardHideResult: 'Hide result',
335
+ queryCardRowUnit: 'rows',
336
+ queryCardDsl: 'DSL / SQL',
337
+ queryCardNoRows: 'No rows returned',
338
+ queryCardTableMore: 'Previewing first {count} rows',
277
339
  stepPlanned: 'Plan',
278
340
  stepDatasourceConfigured: 'Database connection',
279
341
  stepDatasourceVerified: 'Connection test',
@@ -317,6 +379,8 @@ window.__ModuleLoader__.load({
317
379
  .foggy-resources{border-top:1px solid var(--dsw-alias-border-l2);padding-top:14px}.foggy-resources h4{margin:0 0 9px;font-size:13px}.foggy-resource-links{display:flex;gap:8px;flex-wrap:wrap}.foggy-resource-link{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 10px;color:var(--dsw-alias-label-secondary);font-size:12px;text-decoration:none}.foggy-resource-link:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.foggy-resource-link:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
318
380
  .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}
319
381
  .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}
382
+ .foggy-turn-results{margin:10px 0 14px;max-width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);overflow:hidden;color:var(--dsw-alias-label-primary)}.foggy-turn-results-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;font-weight:600;flex-wrap:wrap}.foggy-turn-results-provenance{padding:2px 7px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-size:11px;font-weight:600}.foggy-turn-results-source{color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:400}.foggy-turn-results-count{font-weight:400;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}.foggy-turn-result{padding:9px 12px}.foggy-turn-result+.foggy-turn-result{border-top:1px solid var(--dsw-alias-border-l2)}.foggy-turn-result-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.foggy-turn-result-name{min-width:0;overflow-wrap:anywhere;font-size:12px;font-weight:600}.foggy-turn-result-meta{margin-left:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400}.foggy-turn-result-actions{display:flex;align-items:center;gap:8px}.foggy-turn-result-actions button,.foggy-turn-results-toggle{border:0;background:none;color:var(--dsw-alias-state-business-primary);font:inherit;font-size:12px;cursor:pointer;padding:3px 0}.foggy-turn-result-actions button:disabled{opacity:.55;cursor:not-allowed}.foggy-turn-result-actions button:focus-visible,.foggy-turn-results-toggle:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.foggy-turn-result-detail{margin-top:9px;overflow:auto;max-height:300px}.foggy-turn-result-detail table{border-collapse:collapse;width:100%;min-width:max-content;font-size:11px}.foggy-turn-result-detail th,.foggy-turn-result-detail td{border:1px solid var(--dsw-alias-border-l2);padding:5px 7px;text-align:left;max-width:240px;overflow-wrap:anywhere}.foggy-turn-result-detail th{background:var(--dsw-alias-bg-layer-2);font-weight:600}.foggy-turn-result-note{margin:6px 0 0;color:var(--dsw-alias-label-tertiary);font-size:11px}.foggy-turn-result-error{margin:6px 0 0;color:var(--dsw-alias-state-error-primary);font-size:11px}.foggy-turn-results-toggle{display:block;width:100%;padding:9px 12px;border-top:1px solid var(--dsw-alias-border-l2);text-align:left}
383
+ .foggy-turn-result-code{margin-top:9px;font-size:11px}.foggy-turn-result-code summary{cursor:pointer;color:var(--dsw-alias-label-secondary)}.foggy-turn-result-code pre{max-height:220px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;margin:7px 0;padding:8px;border-radius:6px;background:var(--dsw-alias-bg-layer-2)}
320
384
  @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%}.foggy-toolbar-main{width:100%}.foggy-toolbar-main .foggy-button{flex:1}.foggy-resource-link{min-height:36px}}
321
385
  `
322
386
 
@@ -779,7 +843,145 @@ window.__ModuleLoader__.load({
779
843
  ] })
780
844
  }
781
845
 
782
- const inject = ['slots', 'locale', 'remote']
846
+ function queryPreviewSupported(args, result) {
847
+ const payload = args?.payload
848
+ return Boolean(result?.execution?.success) && payload && Array.isArray(payload.columns) && payload.columns.length > 0
849
+ && !('executable_plan' in payload) && !('executablePlan' in payload) && payload.route !== 'DSL_CTE'
850
+ && Object.keys(payload).every((key) => ['columns', 'slice', 'groupBy', 'orderBy', 'calculatedFields', 'extData', 'start', 'limit', 'returnTotal'].includes(key))
851
+ && ['slice', 'calculatedFields'].every((key) => !payload[key] || Array.isArray(payload[key]) && payload[key].every((item) => item && typeof item === 'object' && !Array.isArray(item)))
852
+ && ['groupBy', 'orderBy'].every((key) => !payload[key] || Array.isArray(payload[key]) && payload[key].every((item) => typeof item === 'string' && item.trim() || item && typeof item === 'object' && !Array.isArray(item)))
853
+ }
854
+
855
+ function useViewerPreview(api) {
856
+ const [preview, setPreview] = useState({ loading: false, error: '' })
857
+ async function openViewer(args) {
858
+ const pendingTab = window.open('about:blank', '_blank')
859
+ setPreview({ loading: true, error: '' })
860
+ try {
861
+ const value = unwrap(await api.createViewerPreview({ model: args.model, namespace: args.namespace, payload: args.payload }), 'createViewerPreview')
862
+ if (pendingTab) pendingTab.location.replace(value.url)
863
+ else throw new Error('Browser blocked the DataViewer tab')
864
+ setPreview({ loading: false, error: '' })
865
+ } catch (error) {
866
+ if (pendingTab) pendingTab.close()
867
+ setPreview({ loading: false, error: String(error?.message || error) })
868
+ }
869
+ }
870
+ return { preview, openViewer }
871
+ }
872
+
873
+ function readSuccessfulQuery(block) {
874
+ if (block?.kind !== 'tool-result' || block.isError || block.call?.name !== 'foggy_query') return null
875
+ try {
876
+ const args = JSON.parse(block.call.argsRaw)
877
+ const text = block.content?.find((item) => item.type === 'text')?.text
878
+ const result = JSON.parse(text)
879
+ if (!result?.execution?.success || !args?.model || !args?.namespace || !Array.isArray(result.execution.data?.items)) return null
880
+ return { callId: block.callId, args, result }
881
+ } catch { return null }
882
+ }
883
+
884
+ function cellText(value) {
885
+ const raw = value === null || value === undefined ? '—' : typeof value === 'object' ? JSON.stringify(value) : String(value)
886
+ return raw.length > 160 ? `${raw.slice(0, 157)}…` : raw
887
+ }
888
+
889
+ function FoggyTurnQuery({ query, api, t }) {
890
+ const [expanded, setExpanded] = useState(false)
891
+ const { preview, openViewer } = useViewerPreview(api)
892
+ const rows = query.result.execution.data.items
893
+ const fields = rows.length && rows[0] && typeof rows[0] === 'object' ? Object.keys(rows[0]) : []
894
+ return jsxs('div', { className: 'foggy-turn-result', children: [
895
+ jsxs('div', { className: 'foggy-turn-result-head', children: [
896
+ jsxs('div', { className: 'foggy-turn-result-name', children: [
897
+ query.args.model,
898
+ jsx('span', { className: 'foggy-turn-result-meta', children: `${query.args.namespace} · ${rows.length} ${t('queryCardRowUnit')}` }),
899
+ ] }),
900
+ jsxs('div', { className: 'foggy-turn-result-actions', children: [
901
+ jsx('button', { type: 'button', 'aria-expanded': expanded, onClick: () => setExpanded((current) => !current), children: t(expanded ? 'queryCardHideResult' : 'queryCardShowResult') }),
902
+ queryPreviewSupported(query.args, query.result) ? jsx('button', { type: 'button', disabled: preview.loading, onClick: () => openViewer(query.args), children: preview.loading ? t('queryCardOpening') : t('queryCardOpen') }) : null,
903
+ ] }),
904
+ ] }),
905
+ expanded ? jsxs('div', { className: 'foggy-turn-result-detail', children: [
906
+ rows.length && fields.length ? jsxs('table', { children: [
907
+ jsx('thead', { children: jsx('tr', { children: fields.map((field) => jsx('th', { scope: 'col', children: field }, field)) }) }),
908
+ jsx('tbody', { children: rows.slice(0, 5).map((row, index) => jsx('tr', { children: fields.map((field) => jsx('td', { children: cellText(row[field]) }, field)) }, index)) }),
909
+ ] }) : jsx('p', { className: 'foggy-turn-result-note', children: t('queryCardNoRows') }),
910
+ rows.length > 5 ? jsx('p', { className: 'foggy-turn-result-note', children: t('queryCardTableMore', { count: 5 }) }) : null,
911
+ jsxs('details', { className: 'foggy-turn-result-code', children: [
912
+ jsx('summary', { children: t('queryCardDsl') }),
913
+ jsx('pre', { children: JSON.stringify(query.args.payload, null, 2) }),
914
+ query.result.execution.data.debug?.extra?.sql ? jsx('pre', { children: query.result.execution.data.debug.extra.sql }) : null,
915
+ ] }),
916
+ ] }) : null,
917
+ preview.error ? jsx('p', { className: 'foggy-turn-result-error', role: 'alert', children: preview.error }) : null,
918
+ ] })
919
+ }
920
+
921
+ function FoggyTurnResults({ turn, useQueryCalls, api, t }) {
922
+ const [showAll, setShowAll] = useState(false)
923
+ const calls = useQueryCalls(String(turn.turn)) || []
924
+ const queries = calls.map((item) => readSuccessfulQuery(item.root)).filter(Boolean)
925
+ if (!queries.length) return null
926
+ const visible = showAll ? queries : queries.slice(0, 6)
927
+ return jsxs('section', { className: 'foggy-turn-results', 'aria-label': t('queryCardResults'), children: [
928
+ jsxs('div', { className: 'foggy-turn-results-head', children: [
929
+ t('queryCardResults'), jsx('span', { className: 'foggy-turn-results-count', children: queries.length }),
930
+ jsx('span', { className: 'foggy-turn-results-provenance', children: t('queryCardProvenance') }),
931
+ jsx('span', { className: 'foggy-turn-results-source', children: t('queryCardSource') }),
932
+ ] }),
933
+ ...visible.map((query) => jsx(FoggyTurnQuery, { query, api, t }, query.callId)),
934
+ queries.length > 6 ? jsx('button', {
935
+ className: 'foggy-turn-results-toggle', type: 'button', 'aria-expanded': showAll,
936
+ onClick: () => setShowAll((current) => !current),
937
+ children: showAll ? t('queryCardShowLess') : t('queryCardShowAll', { count: queries.length }),
938
+ }) : null,
939
+ ] })
940
+ }
941
+
942
+ function FoggyQueryCard({ phase, block, api, t }) {
943
+ const { preview, openViewer } = useViewerPreview(api)
944
+ let args = null
945
+ let result = null
946
+ let outputText = ''
947
+ try {
948
+ const raw = phase === 'result' ? block.call?.argsRaw : phase === 'start' ? block.argsRaw : null
949
+ if (raw) args = JSON.parse(raw)
950
+ if (phase === 'result') {
951
+ outputText = block.content?.find((item) => item.type === 'text')?.text || ''
952
+ if (outputText) result = JSON.parse(outputText)
953
+ }
954
+ } catch {}
955
+ const success = phase === 'result' && !block.isError && Boolean(result?.execution?.success)
956
+ const rows = result?.execution?.data?.items?.length
957
+ const sql = result?.execution?.data?.debug?.extra?.sql
958
+ const payload = args?.payload
959
+ const hasAdHocColumns = payload?.columns?.some((column) => typeof column === 'string' && /\bas\b|\(/i.test(column))
960
+ const supported = success && queryPreviewSupported(args, result)
961
+ const box = { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 10, padding: 12, margin: '8px 0', fontSize: 13 }
962
+ return jsxs('section', { style: box, children: [
963
+ jsx('strong', { children: `${t('queryCardTitle')}${args?.model ? ` · ${args.model}` : ''}` }),
964
+ jsx('p', { style: { margin: '6px 0', color: 'var(--dsw-alias-label-secondary)' }, children: success ? `${t('queryCardDone')}${Number.isInteger(rows) ? ` · ${t('queryCardRows')} ${rows}` : ''}` : phase === 'result' ? t('queryCardFailed') : t('queryCardRunning') }),
965
+ args?.namespace ? jsx('code', { children: `namespace: ${args.namespace}` }) : null,
966
+ success ? jsxs('details', { style: { marginTop: 8 }, children: [
967
+ jsx('summary', { style: { cursor: 'pointer' }, children: t('queryCardDetails') }),
968
+ sql ? jsx('pre', { style: { maxHeight: 180, overflow: 'auto', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: sql }) : null,
969
+ jsx('pre', { style: { maxHeight: 260, overflow: 'auto', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: JSON.stringify({ dsl: payload, validation: result.validation, execution: result.execution }, null, 2).slice(0, 30000) }),
970
+ ] }) : null,
971
+ phase === 'result' && !success ? jsxs('details', { style: { marginTop: 8 }, children: [
972
+ jsx('summary', { style: { cursor: 'pointer' }, children: t('queryCardFailed') }),
973
+ jsx('pre', { style: { maxHeight: 260, overflow: 'auto', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: (outputText || JSON.stringify(block)).slice(0, 6000) }),
974
+ ] }) : null,
975
+ supported ? jsxs('div', { style: { marginTop: 10 }, children: [
976
+ jsx('button', { type: 'button', disabled: preview.loading, onClick: () => openViewer(args), children: preview.loading ? t('queryCardOpening') : t('queryCardOpen') }),
977
+ jsx('p', { style: { margin: '5px 0', color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 }, children: t('queryCardPreviewHint') }),
978
+ hasAdHocColumns ? jsx('p', { style: { margin: '5px 0', color: 'var(--dsw-alias-state-warning-primary)', fontSize: 11 }, children: t('queryCardComputedHint') }) : null,
979
+ preview.error ? jsx('p', { role: 'alert', style: { color: 'var(--dsw-alias-state-error-primary)' }, children: preview.error }) : null,
980
+ ] }) : success ? jsx('p', { style: { margin: '8px 0 0', color: 'var(--dsw-alias-label-tertiary)' }, children: t('queryCardUnsupported') }) : null,
981
+ ] })
982
+ }
983
+
984
+ const inject = ['slots', 'locale', 'remote', 'sessions', 'uiConversation']
783
985
 
784
986
  async function apply(ctx) {
785
987
  const disposeRemote = await ctx.remote.$mount(TYPERT_REMOTE)
@@ -788,6 +990,28 @@ window.__ModuleLoader__.load({
788
990
  const t = ctx.locale.bind(NS)
789
991
  ctx.inject(['remote.foggyIntegration'], (remoteCtx) => {
790
992
  const api = remoteCtx.remote.foggyIntegration
993
+ remoteCtx.slots.inject('tool.call.toolview', () => remoteCtx.slots.register({
994
+ name: 'tool.call.toolview',
995
+ key: 'foggy_query',
996
+ inject: () => ({ api, t }),
997
+ }, FoggyQueryCard))
998
+ remoteCtx.slots.inject('conversation.chat.turnTail', () => remoteCtx.slots.register({
999
+ name: 'conversation.chat.turnTail',
1000
+ id: 'foggy-query-results',
1001
+ inject: (sessionId) => {
1002
+ const binding = remoteCtx.sessions.binding(sessionId)
1003
+ if (!binding) throw new Error(`Foggy: unknown session ${sessionId}`)
1004
+ const chat = remoteCtx.uiConversation.binding(binding).target('chat')
1005
+ return {
1006
+ api, t,
1007
+ keyedHooks: { queryCalls: (turn) => {
1008
+ const snapshot = chat.getSnapshot()
1009
+ if (!snapshot) throw new Error('Foggy: chat target is unavailable')
1010
+ return snapshot.nodes.turnDataSource(Number(turn), 'tool-call')
1011
+ } },
1012
+ }
1013
+ },
1014
+ }, FoggyTurnResults))
791
1015
  remoteCtx.slots.inject('settings.plugins.tab', () => remoteCtx.slots.register({
792
1016
  name: 'settings.plugins.tab',
793
1017
  id: 'foggy',
package/lib/index.js CHANGED
@@ -10,6 +10,8 @@ import { writeJsonAtomic } from './atomic-json.js'
10
10
  import { ensurePythonRuntime, probePythonRuntime } from './python-runtime.js'
11
11
  import { compatible, compatibleNode } from './version.js'
12
12
  import { enrichRuntimeStartFailure } from './diagnostics.js'
13
+ import { createFoggyQueryTool, managedConnection } from './query-tool.js'
14
+ import { createViewerPreview } from './viewer-preview.js'
13
15
  import {
14
16
  DEFAULT_RUNTIME_PORT,
15
17
  readRuntimeSettings,
@@ -210,7 +212,7 @@ function operationView(operation) {
210
212
  }
211
213
 
212
214
  export class FoggyIntegrationGateway extends TypertRemoteService {
213
- static inject = ['skills']
215
+ static inject = ['skills', 'tools']
214
216
 
215
217
  operation = null
216
218
  skillProviderControl = null
@@ -222,9 +224,15 @@ export class FoggyIntegrationGateway extends TypertRemoteService {
222
224
  this.skillProviderControl = control
223
225
  return createFoggySkillProvider({ installRoot: roots.installRoot, versionsFile })
224
226
  })
227
+ ctx.tools.register(createFoggyQueryTool(roots))
225
228
  for (const initialize of markerInitializers) initialize.call(this)
226
229
  }
227
230
 
231
+ async createViewerPreview(input) {
232
+ const { runtimeUrl } = await managedConnection(defaultRoots())
233
+ return createViewerPreview(input, runtimeUrl)
234
+ }
235
+
228
236
  async status() {
229
237
  const roots = defaultRoots()
230
238
  const statePath = join(roots.installRoot, 'install-state.json')
@@ -672,6 +680,7 @@ for (const method of [
672
680
  'status', 'plan', 'initialize', 'initializeAndStart', 'repair', 'updateComponents', 'repairPython', 'repairCli', 'repairLauncher', 'repairAnalysisSkill', 'repairSemanticQuerySkill',
673
681
  'migrateProfiles', 'diagnostics', 'runtimeStart', 'runtimeStop',
674
682
  'saveRuntimeSettings',
683
+ 'createViewerPreview',
675
684
  ]) {
676
685
  Remote(method)(FoggyIntegrationGateway.prototype[method], {
677
686
  kind: 'method',
@@ -0,0 +1,131 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { mkdtemp, readFile, rmdir, unlink, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { promisify } from 'node:util'
6
+
7
+ const execFileAsync = promisify(execFile)
8
+ const identifier = /^[A-Za-z0-9_.-]{1,128}$/
9
+
10
+ export function assertStandardDsl(payload) {
11
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('Query DSL must be a JSON object')
12
+ if (payload.route === 'DSL_CTE' || 'executable_plan' in payload || 'executablePlan' in payload) {
13
+ throw new Error('CTE preview is not supported in this release; use a standard QM DSL query')
14
+ }
15
+ if (!Array.isArray(payload.columns) || payload.columns.length === 0 || !payload.columns.every((item) => typeof item === 'string')) {
16
+ throw new Error('Standard QM DSL requires non-empty columns')
17
+ }
18
+ }
19
+
20
+ export function previewPayload(payload) {
21
+ assertStandardDsl(payload)
22
+ const supported = new Set(['columns', 'slice', 'groupBy', 'orderBy', 'calculatedFields', 'extData'])
23
+ const unsupported = Object.keys(payload).filter((key) => !supported.has(key) && !['start', 'limit', 'returnTotal'].includes(key))
24
+ if (unsupported.length) throw new Error(`DataViewer preview does not support: ${unsupported.join(', ')}`)
25
+ for (const key of ['slice', 'calculatedFields']) {
26
+ if (payload[key] !== undefined && (!Array.isArray(payload[key]) || payload[key].some((item) => !item || typeof item !== 'object' || Array.isArray(item)))) {
27
+ throw new Error(`DataViewer preview requires object-form ${key} entries`)
28
+ }
29
+ }
30
+ const result = Object.fromEntries([...supported].filter((key) => payload[key] !== undefined).map((key) => [key, payload[key]]))
31
+ if (payload.groupBy !== undefined) {
32
+ if (!Array.isArray(payload.groupBy) || payload.groupBy.some((item) => typeof item !== 'string' && (!item || typeof item !== 'object' || Array.isArray(item)))) {
33
+ throw new Error('DataViewer preview requires field names or object-form groupBy entries')
34
+ }
35
+ result.groupBy = payload.groupBy.map((item) => typeof item === 'string' ? { field: item } : item)
36
+ }
37
+ if (payload.orderBy !== undefined) {
38
+ if (!Array.isArray(payload.orderBy) || payload.orderBy.some((item) => typeof item !== 'string' && (!item || typeof item !== 'object' || Array.isArray(item)))) {
39
+ throw new Error('DataViewer preview requires field names or object-form orderBy entries')
40
+ }
41
+ result.orderBy = payload.orderBy.map((item) => {
42
+ if (typeof item !== 'string') return item
43
+ const shorthand = item.trim().match(/^(-)?(.+?)(?:\s+(asc|desc))?$/i)
44
+ if (!shorthand || !shorthand[2].trim()) throw new Error('DataViewer preview requires a valid orderBy field')
45
+ return { field: shorthand[2].trim(), dir: shorthand[1] ? 'desc' : (shorthand[3] || 'asc').toLowerCase() }
46
+ })
47
+ }
48
+ return result
49
+ }
50
+
51
+ export function safeRuntimeUrl(value) {
52
+ const url = new URL(value)
53
+ if (url.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)
54
+ || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
55
+ throw new Error('Managed Runtime URL must be a plain loopback HTTP origin')
56
+ }
57
+ return url.origin
58
+ }
59
+
60
+ export async function managedConnection(roots) {
61
+ const install = JSON.parse(await readFile(join(roots.installRoot, 'install-state.json'), 'utf8'))
62
+ const runtime = JSON.parse(await readFile(join(roots.dataRoot, 'runtime-state.json'), 'utf8'))
63
+ if (!install.cli?.command) throw new Error('Foggy CLI is not installed')
64
+ return { cli: install.cli.command, runtimeUrl: safeRuntimeUrl(runtime.runtimeUrl) }
65
+ }
66
+
67
+ async function cliJson(cli, runtimeUrl, namespace, command, signal) {
68
+ const { stdout } = await execFileAsync(cli, ['--base-url', runtimeUrl, '--namespace', namespace, '--output', 'json', ...command], {
69
+ signal,
70
+ windowsHide: true,
71
+ timeout: 120_000,
72
+ maxBuffer: 8 * 1024 * 1024,
73
+ })
74
+ const value = JSON.parse(stdout.trim())
75
+ if (value?.success !== true) throw new Error(value?.error?.message || value?.error?.code || 'Foggy query failed')
76
+ return value
77
+ }
78
+
79
+ export function createFoggyQueryTool(roots) {
80
+ return {
81
+ name: 'foggy_query',
82
+ description: 'Validate and execute a bounded, read-only standard Foggy QueryModel DSL query against the managed local Runtime. On success Harness shows a query card; its DataViewer preview link is created only if the user clicks Open DataViewer. CTE is not supported here.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ model: { type: 'string', description: 'Published Foggy QueryModel name' },
87
+ namespace: { type: 'string', description: 'Foggy namespace' },
88
+ payload: { type: 'object', additionalProperties: true, description: 'Standard QueryModel DSL, including explicit columns and bounded limit' },
89
+ },
90
+ required: ['model', 'namespace', 'payload'],
91
+ },
92
+ output: {
93
+ schema: {
94
+ type: 'object',
95
+ additionalProperties: false,
96
+ properties: {
97
+ model: { type: 'string' },
98
+ namespace: { type: 'string' },
99
+ validation: {},
100
+ execution: {},
101
+ },
102
+ required: ['model', 'namespace', 'validation', 'execution'],
103
+ },
104
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
105
+ },
106
+ timeoutMs: 150_000,
107
+ async execute(args, exec) {
108
+ const { model, namespace, payload } = args || {}
109
+ if (!identifier.test(model) || !identifier.test(namespace)) throw new Error('Invalid model or namespace')
110
+ assertStandardDsl(payload)
111
+ if (!Number.isInteger(payload.limit) || payload.limit < 1 || payload.limit > 100) {
112
+ throw new Error('Query DSL limit must be an integer from 1 to 100')
113
+ }
114
+ const { cli, runtimeUrl } = await managedConnection(roots)
115
+ const directory = await mkdtemp(join(tmpdir(), 'foggy-dsh-query-'))
116
+ const payloadPath = join(directory, 'query.json')
117
+ try {
118
+ await writeFile(payloadPath, JSON.stringify(payload), { encoding: 'utf8', flag: 'wx' })
119
+ const validation = await cliJson(cli, runtimeUrl, namespace, ['query', 'validate', model, '--payload', payloadPath], exec?.signal)
120
+ const execution = await cliJson(cli, runtimeUrl, namespace, ['query', 'execute', model, '--payload', payloadPath], exec?.signal)
121
+ return { model, namespace, validation, execution }
122
+ } finally {
123
+ await unlink(payloadPath).catch(() => {})
124
+ await rmdir(directory).catch(() => {})
125
+ }
126
+ },
127
+ presentCall({ model }) {
128
+ return { card: 'generic', title: `Foggy DSL · ${model}`, kind: 'read' }
129
+ },
130
+ }
131
+ }
@@ -4,6 +4,11 @@ const resultSchema = z.unknown()
4
4
  const runtimeSettingsInputSchema = z.object({
5
5
  port: z.number().int().min(1024).max(65535),
6
6
  }).strict()
7
+ const viewerPreviewInputSchema = z.object({
8
+ model: z.string(),
9
+ namespace: z.string(),
10
+ payload: z.record(z.string(), z.unknown()),
11
+ }).strict()
7
12
 
8
13
  function strictCodec(typeSymbol, schema) {
9
14
  return {
@@ -45,4 +50,10 @@ export const descriptors = [
45
50
  source: 'json',
46
51
  codec: strictCodec('@foggy-projects/deepseek-harness-plugin#FoggyRuntimeSettingsInput', runtimeSettingsInputSchema),
47
52
  }]),
53
+ descriptor('createViewerPreview', [{
54
+ name: 'input',
55
+ wire: 'input',
56
+ source: 'json',
57
+ codec: strictCodec('@foggy-projects/deepseek-harness-plugin#FoggyViewerPreviewInput', viewerPreviewInputSchema),
58
+ }]),
48
59
  ]
@@ -0,0 +1,27 @@
1
+ import { previewPayload, safeRuntimeUrl } from './query-tool.js'
2
+
3
+ const identifier = /^[A-Za-z0-9_.-]{1,128}$/
4
+
5
+ export async function createViewerPreview({ model, namespace, payload }, runtimeUrl, fetcher = fetch) {
6
+ if (!identifier.test(model) || !identifier.test(namespace)) throw new Error('Invalid model or namespace')
7
+ const origin = safeRuntimeUrl(runtimeUrl)
8
+ const body = { model, namespace, title: `${model} · Harness preview`, payload: previewPayload(payload) }
9
+ const response = await fetcher(`${origin}/data-viewer/api/query/create`, {
10
+ method: 'POST',
11
+ headers: { 'content-type': 'application/json' },
12
+ body: JSON.stringify(body),
13
+ signal: AbortSignal.timeout(12_000),
14
+ })
15
+ if (!response.ok) throw new Error(`DataViewer unavailable (HTTP ${response.status}); update and restart the managed Runtime`)
16
+ const envelope = await response.json()
17
+ const result = envelope?.data
18
+ if (envelope?.code !== 200 || result?.success !== true || typeof result.viewerUrl !== 'string') {
19
+ throw new Error(envelope?.msg || result?.error || 'DataViewer did not return a preview link')
20
+ }
21
+ const url = new URL(result.viewerUrl, origin)
22
+ if (url.origin !== origin || !url.pathname.startsWith(`/data-viewer/view/${encodeURIComponent(model)}/`)
23
+ || url.username || url.password || url.search || url.hash) {
24
+ throw new Error('DataViewer returned an unexpected preview URL')
25
+ }
26
+ return { success: true, url: url.href, queryId: result.queryId }
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foggy-projects/deepseek-harness-plugin",
3
- "version": "0.4.2-rc.2",
3
+ "version": "0.4.2-rc.5",
4
4
  "description": "Foggy Java data analysis engine integration for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -41,6 +41,9 @@
41
41
  "docs/RELEASE-CANDIDATE-0.4.0-RC.3.md",
42
42
  "docs/RELEASE-CANDIDATE-0.4.1-DSH-0.1.5-RC.1.md",
43
43
  "docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.2.md",
44
+ "docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.3.md",
45
+ "docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.4.md",
46
+ "docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.5.md",
44
47
  "docs/RELEASE-CANDIDATE-0.4.2-DSH-0.1.7-RC.1.md",
45
48
  "docs/COMMUNITY-MARKETPLACES.md",
46
49
  "docs/WINDOWS-BETA-ACCEPTANCE.md",
@@ -61,7 +64,11 @@
61
64
  "@deepseek-ai/dsh-api-remotes",
62
65
  "@deepseek-ai/dsh-client-locale",
63
66
  "@deepseek-ai/dsh-client-ui-settings",
64
- "@deepseek-ai/dsh-client-ui-settings-plugins"
67
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
68
+ "@deepseek-ai/dsh-client-ui-tool",
69
+ "@deepseek-ai/dsh-client-ui-conversation",
70
+ "@deepseek-ai/dsh-client-ui-chat",
71
+ "@deepseek-ai/dsh-api-session-controller"
65
72
  ]
66
73
  }
67
74
  },
@@ -74,11 +81,15 @@
74
81
  "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
75
82
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
76
83
  "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.7-rc.2",
84
+ "@deepseek-ai/dsh-client-ui-tool": "^0.1.7-rc.2",
85
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.7-rc.2",
86
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.7-rc.2",
87
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.7-rc.2",
77
88
  "@deepseek-ai/dsh-skill": "^0.1.7-rc.2",
78
89
  "@deepseek-ai/dsh-typert-protocol": "^0.1.7-rc.2"
79
90
  },
80
91
  "scripts": {
81
- "check": "node --check lib/index.js && node --check lib/atomic-json.js && node --check lib/diagnostics.js && node --check lib/python-runtime.js && node --check lib/runtime-settings.js && node --check lib/skill-provider.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
82
- "test": "node --test test/package.test.js && node test/run-onboarding-tests.js"
92
+ "check": "node --check lib/index.js && node --check lib/query-tool.js && node --check lib/viewer-preview.js && node --check lib/atomic-json.js && node --check lib/diagnostics.js && node --check lib/python-runtime.js && node --check lib/runtime-settings.js && node --check lib/skill-provider.js && node --check lib/client.js && node --check lib/typert.js && node --check lib/remote.js",
93
+ "test": "node --test test/package.test.js test/viewer-preview.test.js && node test/run-onboarding-tests.js"
83
94
  }
84
95
  }
@@ -60,6 +60,12 @@ are authoritative; do not copy the Skill into the current workspace.
60
60
  6. Iterate through model validation, Bundle registration/update, model refresh/describe, query
61
61
  validation, and bounded query execution. Development publication means making the local model
62
62
  directory effective in this local Runtime; it is not a production release.
63
+ For the final user-facing standard DSL query in Harness, use the native `foggy_query` tool when
64
+ available (explicit namespace, published QM, `columns`, and `limit` of 1–100). It validates and
65
+ executes, then displays a query card. The card creates a DataViewer preview link only if the user
66
+ clicks its button; never pre-create links for every query. This first version does not support
67
+ `DSL_CTE` or `executable_plan` in the native tool. Continue using the documented CLI route for
68
+ CTE queries and do not claim they have a DataViewer preview.
63
69
  A successful response may still contain Runtime `warnings`. Preserve and report their stable facts
64
70
  (`code`, `path`, `message`, `suggestedNextAction`, `safeToAutoRepair`, `normalizedFragment`,
65
71
  `docsRef`, and `details.allowedProperties`)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "foggy-deepseek-onboarding-versions/v1",
3
- "packageVersion": "0.4.2-rc.2",
4
- "validatedAt": "2026-09-27",
3
+ "packageVersion": "0.4.2-rc.5",
4
+ "validatedAt": "2026-09-28",
5
5
  "components": {
6
6
  "deepseekHarness": {
7
7
  "version": "0.1.7-rc.2",
@@ -66,39 +66,39 @@
66
66
  }
67
67
  },
68
68
  "launcher": {
69
- "version": "0.1.21",
69
+ "version": "0.1.23",
70
70
  "minimumJavaVersion": "17.0.0",
71
71
  "runtimeApiContract": "foggy-runtime-api/v1",
72
72
  "assets": [
73
73
  {
74
- "file": "foggy-runtime-launcher-0.1.21.jar",
75
- "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.21/foggy-runtime-launcher-0.1.21.jar",
76
- "sha256": "0b5002e40633f4606b0f5d3adbcd734ad4a11d8204decab162bb53b6ab28b3b3"
74
+ "file": "foggy-runtime-launcher-0.1.23.jar",
75
+ "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.23/foggy-runtime-launcher-0.1.23.jar",
76
+ "sha256": "e3575c51e457d6b88740ef5949d7e206d5d6046d960fc8ef0884765bf3be8c66"
77
77
  },
78
78
  {
79
79
  "file": "start-foggy-runtime.ps1",
80
- "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.21/start-foggy-runtime.ps1",
81
- "sha256": "8609f8c60bad9e56460b132e1bd6c699d4ebf47cf591c3e6bc37bc8563d7de5a"
80
+ "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.23/start-foggy-runtime.ps1",
81
+ "sha256": "e6e2ac995a2a84c3de53878e8e0db4d701eab8b3a7ca6dbe9d4fb4ad15b00ae4"
82
82
  },
83
83
  {
84
84
  "file": "start-foggy-runtime.sh",
85
- "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.21/start-foggy-runtime.sh",
86
- "sha256": "fa844f422473ba00a31cf801092b0fe4ef93fe8697656a9063e0ec27dacef715"
85
+ "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.23/start-foggy-runtime.sh",
86
+ "sha256": "562ef5614e02c4635c21d5fa9d6c2edc0dfb69d8da0ae57c4247c0c8202d50ae"
87
87
  },
88
88
  {
89
89
  "file": "README-foggy-runtime-launcher.md",
90
- "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.21/README-foggy-runtime-launcher.md",
91
- "sha256": "6b63cafe798e19c255a043e15e64109f6fb3252aee3fb9c37ab6a66d9adbf3cf"
90
+ "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.23/README-foggy-runtime-launcher.md",
91
+ "sha256": "759e3be46eb97a8f62b7ffb1dc5a977c52d0b91d1d31860e4e799125ce25e376"
92
92
  },
93
93
  {
94
94
  "file": "runtime-launcher-manifest.json",
95
- "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.21/runtime-launcher-manifest.json",
96
- "sha256": "1591804035bbf589912907195c74746bf97edff3b9e6ebd7d569ecc0e5aebed7"
95
+ "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.23/runtime-launcher-manifest.json",
96
+ "sha256": "d25a9a0026495fd47e6ca03c957bf9999df9cffe73c6d6b0fc5c7dfc3931d317"
97
97
  },
98
98
  {
99
99
  "file": "SHA256SUMS",
100
- "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.21/SHA256SUMS",
101
- "sha256": "6e03f8861176d73be22c4c7c6f6345a68e49aacfb5375cd5cf2c54f1dc4cbe5d"
100
+ "url": "https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/download/foggy-runtime-launcher-v0.1.23/SHA256SUMS",
101
+ "sha256": "49354e281c9f854651074b292aa003710334ae2f5494b3c07eafddf1d08a7dac"
102
102
  }
103
103
  ]
104
104
  },
@@ -506,28 +506,27 @@ def materialize(
506
506
  progress_total: int = 1,
507
507
  progress_message: str = "Downloading and verifying asset",
508
508
  replace_corrupt: bool = False,
509
+ previous_sha256: str | None = None,
509
510
  ) -> dict:
510
511
  destination.parent.mkdir(parents=True, exist_ok=True)
512
+ previous_valid = False
511
513
  if destination.is_file():
512
514
  try:
513
515
  verify_asset(destination, asset["sha256"])
514
516
  return {"file": asset["file"], "path": str(destination), "source": "existing", "sha256": asset["sha256"]}
515
517
  except OnboardingError:
516
- if not replace_corrupt:
518
+ previous_valid = bool(previous_sha256 and sha256(destination).lower() == previous_sha256.lower())
519
+ if not previous_valid and not replace_corrupt:
517
520
  raise
518
- quarantine = destination.with_name(
519
- destination.name + f".corrupt-{dt.datetime.now().strftime('%Y%m%d-%H%M%S-%f')}"
520
- )
521
- destination.replace(quarantine)
522
- cached = cached_asset(asset["file"], asset["sha256"], cache_dirs)
523
- if cached:
524
- shutil.copy2(cached, destination)
525
- source = "cache"
526
- else:
527
- temporary = destination.with_name(destination.name + ".download")
528
- if temporary.exists():
529
- temporary.unlink()
530
- try:
521
+ temporary = destination.with_name(destination.name + ".download")
522
+ if temporary.exists():
523
+ temporary.unlink()
524
+ try:
525
+ cached = cached_asset(asset["file"], asset["sha256"], cache_dirs)
526
+ if cached:
527
+ shutil.copy2(cached, temporary)
528
+ source = "cache"
529
+ else:
531
530
  def reporthook(block_count: int, block_size: int, total_size: int) -> None:
532
531
  if progress is None or not progress_phase or total_size <= 0:
533
532
  return
@@ -540,12 +539,18 @@ def materialize(
540
539
  )
541
540
 
542
541
  urllib.request.urlretrieve(asset["url"], temporary, reporthook=reporthook)
543
- verify_asset(temporary, asset["sha256"])
544
- os.replace(temporary, destination)
545
- finally:
546
- if temporary.exists():
547
- temporary.unlink()
548
- source = "network"
542
+ source = "network"
543
+ verify_asset(temporary, asset["sha256"])
544
+ if destination.is_file():
545
+ suffix = "previous" if previous_valid else "corrupt"
546
+ backup = destination.with_name(
547
+ destination.name + f".{suffix}-{dt.datetime.now().strftime('%Y%m%d-%H%M%S-%f')}"
548
+ )
549
+ shutil.copy2(destination, backup)
550
+ os.replace(temporary, destination)
551
+ finally:
552
+ if temporary.exists():
553
+ temporary.unlink()
549
554
  verify_asset(destination, asset["sha256"])
550
555
  return {"file": asset["file"], "path": str(destination), "source": source, "sha256": asset["sha256"]}
551
556
 
@@ -1345,6 +1350,16 @@ def install_command(args: argparse.Namespace) -> dict:
1345
1350
  progress.update("cli", 2, "CLI ready", fraction=1.0)
1346
1351
 
1347
1352
  launcher_dir = install_root / "launcher"
1353
+ previous_state = read_install_state(install_root, required=False)
1354
+ previous_launcher_assets = {}
1355
+ if previous_state and previous_state.get("launcher", {}).get("version") != components["launcher"]["version"]:
1356
+ previous_launcher_assets = {
1357
+ item["file"]: item["sha256"]
1358
+ for item in previous_state.get("verifiedAssets", [])
1359
+ if item.get("file") and item.get("sha256")
1360
+ and item.get("path")
1361
+ and normalized(item["path"]) == normalized(launcher_dir / item["file"])
1362
+ }
1348
1363
  launcher_assets = components["launcher"]["assets"]
1349
1364
  for index, asset in enumerate(launcher_assets):
1350
1365
  progress.update(
@@ -1358,6 +1373,7 @@ def install_command(args: argparse.Namespace) -> dict:
1358
1373
  progress_index=index, progress_total=len(launcher_assets),
1359
1374
  progress_message="Downloading and verifying Launcher",
1360
1375
  replace_corrupt=repair_component == "launcher",
1376
+ previous_sha256=previous_launcher_assets.get(asset["file"]),
1361
1377
  ))
1362
1378
  progress.update(
1363
1379
  "launcher", 3, "Downloading and verifying Launcher",