@ifc-lite/viewer 1.23.0 → 1.25.0
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/.turbo/turbo-build.log +34 -31
- package/CHANGELOG.md +96 -0
- package/dist/assets/{basketViewActivator-Dn_bHUl2.js → basketViewActivator-CU8_toGq.js} +7 -7
- package/dist/assets/{bcf-B9SFl84i.js → bcf-DXGDhw56.js} +23 -23
- package/dist/assets/{deflate-yMpdCIqk.js → deflate-Bb1_H2Yf.js} +1 -1
- package/dist/assets/{exporters-D-BvrNIg.js → exporters-DZhLN0ux.js} +1861 -1658
- package/dist/assets/geometry-controller.worker-DQOSYqtw.js +7 -0
- package/dist/assets/geometry.worker-B62e03Ao.js +1 -0
- package/dist/assets/{geotiff-D1tvcDCb.js → geotiff-y0ZxbRJd.js} +10 -10
- package/dist/assets/{ids-DZLs0snJ.js → ids-DruUNtfD.js} +4 -4
- package/dist/assets/ifc-lite-Ch2T9pP9.js +7 -0
- package/dist/assets/{ifc-lite_bg-DyHX37GQ.wasm → ifc-lite_bg-D7O1WHgP.wasm} +0 -0
- package/dist/assets/{ifc-lite_bg-BIryVCXQ.wasm → ifc-lite_bg-iH_07wf8.wasm} +0 -0
- package/dist/assets/index-Bws3UAkj.css +1 -0
- package/dist/assets/{index-CXSBhkcJ.js → index-Dr88ZlSY.js} +64100 -47030
- package/dist/assets/{jpeg-DUMcZp24.js → jpeg-B3_loqFe.js} +1 -1
- package/dist/assets/lens-PYsLu_MA.js +1 -0
- package/dist/assets/{lerc-IN4uWojP.js → lerc-nkwS8ZUe.js} +1 -1
- package/dist/assets/{lzw-Cnw0hH-m.js → lzw-D3cW5Wpg.js} +1 -1
- package/dist/assets/{native-bridge-BVf2uzoH.js → native-bridge-BcYJooq8.js} +2 -2
- package/dist/assets/{packbits-BskJCwk0.js → packbits-DDN4xzB5.js} +1 -1
- package/dist/assets/{parser.worker-BdtkkaGf.js → parser.worker-BW1IMUed.js} +3 -3
- package/dist/assets/raw-CoIXstQ-.js +1 -0
- package/dist/assets/{sandbox-VLI_y7cl.js → sandbox-DETNEyQb.js} +498 -470
- package/dist/assets/{server-client-BLcKaWQB.js → server-client-CmzJOeS7.js} +1 -1
- package/dist/assets/{wasm-bridge-BAfZh7YT.js → wasm-bridge-CT7mK9W0.js} +1 -1
- package/dist/assets/{webimage-Db2xzze3.js → webimage-CBjgg4up.js} +1 -1
- package/dist/assets/{workerHelpers--sAYm9yN.js → workerHelpers-IEQDo8r3.js} +1 -1
- package/dist/assets/{zstd-BDToOQyD.js → zstd-C8oQ6qdS.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +11 -9
- package/src/App.tsx +5 -2
- package/src/components/extensions/AuditLogPanel.tsx +259 -0
- package/src/components/extensions/BundlePreview.tsx +102 -0
- package/src/components/extensions/CapabilityReview.tsx +333 -0
- package/src/components/extensions/ExtensionDockHost.tsx +192 -0
- package/src/components/extensions/ExtensionToolbarSlot.tsx +106 -0
- package/src/components/extensions/ExtensionsPanel.tsx +481 -0
- package/src/components/extensions/FlavorDialog.tsx +398 -0
- package/src/components/extensions/FlavorImportPreview.tsx +79 -0
- package/src/components/extensions/FlavorIndicator.tsx +81 -0
- package/src/components/extensions/FlavorListView.tsx +318 -0
- package/src/components/extensions/FlavorMergeDialog.tsx +326 -0
- package/src/components/extensions/HelpHint.tsx +182 -0
- package/src/components/extensions/IdeasPanel.tsx +344 -0
- package/src/components/extensions/PlanCard.tsx +227 -0
- package/src/components/extensions/PrivacyPanel.tsx +312 -0
- package/src/components/extensions/PromoteToolDialog.tsx +313 -0
- package/src/components/extensions/RepairQueuePanel.tsx +222 -0
- package/src/components/extensions/icon-registry.ts +92 -0
- package/src/components/extensions/toast-helpers.ts +49 -0
- package/src/components/extensions/widget/WidgetErrorBoundary.tsx +62 -0
- package/src/components/extensions/widget/WidgetRenderer.tsx +428 -0
- package/src/components/viewer/ChatPanel.tsx +251 -3
- package/src/components/viewer/CommandPalette.tsx +74 -4
- package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
- package/src/components/viewer/EntityContextMenu.tsx +70 -0
- package/src/components/viewer/ExportDialog.tsx +9 -1
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +21 -6
- package/src/components/viewer/LensPanel.tsx +50 -0
- package/src/components/viewer/MainToolbar.tsx +170 -87
- package/src/components/viewer/ScriptPanel.tsx +105 -1
- package/src/components/viewer/Section2DPanel.tsx +58 -2
- package/src/components/viewer/StatusBar.tsx +18 -0
- package/src/components/viewer/ViewerLayout.tsx +53 -4
- package/src/components/viewer/Viewport.tsx +72 -0
- package/src/hooks/useActionLogger.test.ts +161 -0
- package/src/hooks/useActionLogger.ts +141 -0
- package/src/hooks/useForkExtension.ts +51 -0
- package/src/hooks/useIfcFederation.ts +7 -1
- package/src/hooks/useInstalledExtensions.ts +43 -0
- package/src/hooks/usePrivacyDisclosure.ts +48 -0
- package/src/hooks/useRunExtensionTests.ts +67 -0
- package/src/hooks/useSlotContributions.ts +38 -0
- package/src/hooks/useSymbolicAnnotations.test.ts +124 -0
- package/src/hooks/useSymbolicAnnotations.ts +776 -0
- package/src/lib/desktop-product.ts +7 -1
- package/src/lib/lens/adapter.ts +14 -0
- package/src/lib/llm/prompt-cache.ts +77 -0
- package/src/lib/llm/stream-client.ts +20 -2
- package/src/lib/llm/stream-direct.ts +11 -1
- package/src/lib/llm/system-prompt.ts +42 -0
- package/src/lib/safe-mode.ts +30 -0
- package/src/sdk/ExtensionHostProvider.tsx +103 -0
- package/src/services/extensions/flavor-service.ts +183 -0
- package/src/services/extensions/host-commands.ts +112 -0
- package/src/services/extensions/host-installer.ts +289 -0
- package/src/services/extensions/host.ts +514 -0
- package/src/services/extensions/idb-flavor-storage.test.ts +140 -0
- package/src/services/extensions/idb-flavor-storage.ts +241 -0
- package/src/services/extensions/idb-log-storage.test.ts +110 -0
- package/src/services/extensions/idb-log-storage.ts +171 -0
- package/src/services/extensions/idb-storage.ts +228 -0
- package/src/services/extensions/runtime-errors.ts +26 -0
- package/src/services/extensions/sandbox-factory.ts +217 -0
- package/src/store/constants.ts +48 -6
- package/src/store/index.ts +6 -1
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/extensionsSlice.ts +90 -0
- package/src/store/slices/lensSlice.ts +28 -0
- package/src/store/slices/visibilitySlice.test.ts +6 -0
- package/src/store/slices/visibilitySlice.ts +17 -8
- package/src/store/types.ts +2 -0
- package/dist/assets/geometry-controller.worker-Cm5pvyR6.js +0 -7
- package/dist/assets/geometry.worker-ClNvXIrj.js +0 -1
- package/dist/assets/ifc-lite-BDg0iIbj.js +0 -7
- package/dist/assets/index-DS_xJQfP.css +0 -1
- package/dist/assets/lens-CpjUdqpw.js +0 -1
- package/dist/assets/raw-DzTtEZIY.js +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `PromoteToolDialog` — turn a saved script into a persistent tool.
|
|
7
|
+
*
|
|
8
|
+
* Reads the script source, infers a minimal capability set via
|
|
9
|
+
* `inferCapabilities`, lets the user pick a name / category / icon /
|
|
10
|
+
* hotkey, then routes through `CapabilityReview` for the security
|
|
11
|
+
* gate before installing.
|
|
12
|
+
*
|
|
13
|
+
* Spec: docs/architecture/ai-customization/01-extension-model.md +
|
|
14
|
+
* `09-implementation-plan.md` P1.T11 / T12.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useMemo, useState } from 'react';
|
|
18
|
+
import { ChevronRight, Sparkles, X } from 'lucide-react';
|
|
19
|
+
import {
|
|
20
|
+
inferCapabilities,
|
|
21
|
+
packBundle,
|
|
22
|
+
sha256Hex,
|
|
23
|
+
type Bundle,
|
|
24
|
+
type ExtensionManifest,
|
|
25
|
+
} from '@ifc-lite/extensions';
|
|
26
|
+
import { ICON_CHOICES } from './icon-registry';
|
|
27
|
+
import { cn } from '@/lib/utils';
|
|
28
|
+
import {
|
|
29
|
+
Dialog,
|
|
30
|
+
DialogContent,
|
|
31
|
+
DialogDescription,
|
|
32
|
+
DialogFooter,
|
|
33
|
+
DialogHeader,
|
|
34
|
+
DialogTitle,
|
|
35
|
+
} from '@/components/ui/dialog';
|
|
36
|
+
import { Button } from '@/components/ui/button';
|
|
37
|
+
import { Input } from '@/components/ui/input';
|
|
38
|
+
import { Label } from '@/components/ui/label';
|
|
39
|
+
import { CapabilityReview } from './CapabilityReview';
|
|
40
|
+
import { useExtensionHost } from '@/sdk/ExtensionHostProvider';
|
|
41
|
+
import type { ExtensionInstallSummary } from '@/services/extensions/host';
|
|
42
|
+
import { ExtensionInstallError } from '@/services/extensions/host';
|
|
43
|
+
import { toast } from '@/components/ui/toast';
|
|
44
|
+
|
|
45
|
+
interface PromoteToolDialogProps {
|
|
46
|
+
open: boolean;
|
|
47
|
+
/** The script source the user is promoting. */
|
|
48
|
+
source: string;
|
|
49
|
+
/** Initial label the user can edit. */
|
|
50
|
+
initialName?: string;
|
|
51
|
+
onClose(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Icon choices live in ./icon-registry — a single source of truth
|
|
55
|
+
// shared with ExtensionToolbarSlot so the icon the user picks here
|
|
56
|
+
// is the icon that lands in the menubar.
|
|
57
|
+
|
|
58
|
+
export function PromoteToolDialog({ open, source, initialName, onClose }: PromoteToolDialogProps) {
|
|
59
|
+
const host = useExtensionHost();
|
|
60
|
+
const [name, setName] = useState(initialName ?? 'My tool');
|
|
61
|
+
const [hotkey, setHotkey] = useState('');
|
|
62
|
+
const [icon, setIcon] = useState<string>('sparkles');
|
|
63
|
+
const [pending, setPending] = useState<{ bytes: Uint8Array; summary: ExtensionInstallSummary } | null>(null);
|
|
64
|
+
const [busy, setBusy] = useState(false);
|
|
65
|
+
|
|
66
|
+
const inference = useMemo(() => inferCapabilities(source), [source]);
|
|
67
|
+
|
|
68
|
+
const handlePromote = async () => {
|
|
69
|
+
if (busy) return;
|
|
70
|
+
setBusy(true);
|
|
71
|
+
try {
|
|
72
|
+
const { bytes, summary } = await synthesiseBundle({
|
|
73
|
+
name,
|
|
74
|
+
source,
|
|
75
|
+
hotkey,
|
|
76
|
+
icon,
|
|
77
|
+
capabilities: inference.capabilities,
|
|
78
|
+
});
|
|
79
|
+
setPending({ bytes, summary });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
toast.error(`Failed to package tool: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
} finally {
|
|
83
|
+
setBusy(false);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleApprove = async (grants: string[]) => {
|
|
88
|
+
if (!pending || busy) return;
|
|
89
|
+
setBusy(true);
|
|
90
|
+
try {
|
|
91
|
+
const status = await host.installFromBytes(pending.bytes, grants);
|
|
92
|
+
// Point the user at the payoff — the actual button. The
|
|
93
|
+
// synthesised manifest puts the command on `toolbar.right`,
|
|
94
|
+
// so it shows up as an icon button at the top-right of the
|
|
95
|
+
// toolbar. Mention the hotkey too if they set one.
|
|
96
|
+
const where = hotkey.trim()
|
|
97
|
+
? `It's now a button in the toolbar (top-right) — or press ${hotkey.trim()}.`
|
|
98
|
+
: `It's now a button in the toolbar (top-right).`;
|
|
99
|
+
toast.success(`Installed "${name}". ${where}`);
|
|
100
|
+
setPending(null);
|
|
101
|
+
onClose();
|
|
102
|
+
void status;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof ExtensionInstallError) {
|
|
105
|
+
toast.error(`Install rejected: ${err.validationErrors[0]?.message ?? err.message}`);
|
|
106
|
+
} else {
|
|
107
|
+
toast.error(`Install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
setBusy(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (pending) {
|
|
115
|
+
return (
|
|
116
|
+
<CapabilityReview
|
|
117
|
+
open
|
|
118
|
+
summary={pending.summary}
|
|
119
|
+
onApprove={handleApprove}
|
|
120
|
+
onCancel={() => setPending(null)}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
|
127
|
+
<DialogContent className="max-w-lg">
|
|
128
|
+
<DialogHeader>
|
|
129
|
+
<div className="flex items-center gap-2">
|
|
130
|
+
<Sparkles className="h-5 w-5 text-primary" />
|
|
131
|
+
<DialogTitle>Promote script to a tool</DialogTitle>
|
|
132
|
+
</div>
|
|
133
|
+
<DialogDescription>
|
|
134
|
+
Turn this saved script into a persistent, sandboxed tool. The tool
|
|
135
|
+
appears in the command palette and on the toolbar. It runs in the
|
|
136
|
+
same sandbox as your scripts with only the capabilities you grant.
|
|
137
|
+
</DialogDescription>
|
|
138
|
+
</DialogHeader>
|
|
139
|
+
|
|
140
|
+
<div className="space-y-4">
|
|
141
|
+
<div className="space-y-2">
|
|
142
|
+
<Label htmlFor="tool-name">Name</Label>
|
|
143
|
+
<Input
|
|
144
|
+
id="tool-name"
|
|
145
|
+
value={name}
|
|
146
|
+
onChange={(e) => setName(e.target.value)}
|
|
147
|
+
placeholder="Fire-rating report"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
<Label htmlFor="tool-hotkey">Hotkey (optional)</Label>
|
|
152
|
+
<Input
|
|
153
|
+
id="tool-hotkey"
|
|
154
|
+
value={hotkey}
|
|
155
|
+
onChange={(e) => setHotkey(e.target.value)}
|
|
156
|
+
placeholder="Ctrl+Alt+F"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
<Label>Icon</Label>
|
|
162
|
+
<div
|
|
163
|
+
role="radiogroup"
|
|
164
|
+
aria-label="Pick a toolbar icon"
|
|
165
|
+
className="grid grid-cols-10 gap-1.5 p-2 rounded-md border bg-muted/40"
|
|
166
|
+
>
|
|
167
|
+
{ICON_CHOICES.map(({ key, Icon, label }) => {
|
|
168
|
+
const selected = icon === key;
|
|
169
|
+
return (
|
|
170
|
+
<button
|
|
171
|
+
key={key}
|
|
172
|
+
type="button"
|
|
173
|
+
role="radio"
|
|
174
|
+
aria-checked={selected}
|
|
175
|
+
aria-label={label}
|
|
176
|
+
title={label}
|
|
177
|
+
onClick={() => setIcon(key)}
|
|
178
|
+
className={cn(
|
|
179
|
+
'flex items-center justify-center h-8 w-8 rounded border transition-colors',
|
|
180
|
+
selected
|
|
181
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
182
|
+
: 'border-transparent text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<Icon className="h-4 w-4" />
|
|
186
|
+
</button>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="rounded-md border bg-muted/40 p-3">
|
|
193
|
+
<div className="text-xs font-semibold mb-2">Inferred capabilities</div>
|
|
194
|
+
{inference.capabilities.length === 0 ? (
|
|
195
|
+
<div className="text-xs text-muted-foreground">
|
|
196
|
+
No `bim.*` calls detected. The tool will request only
|
|
197
|
+
<code className="font-mono ml-1">model.read</code>.
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<ul className="space-y-1">
|
|
201
|
+
{inference.capabilities.map((cap) => (
|
|
202
|
+
<li key={cap} className="text-xs flex items-center gap-2">
|
|
203
|
+
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
|
204
|
+
<code className="font-mono">{cap}</code>
|
|
205
|
+
</li>
|
|
206
|
+
))}
|
|
207
|
+
</ul>
|
|
208
|
+
)}
|
|
209
|
+
{inference.observations.some((o) => o.unknown) && (
|
|
210
|
+
<div className="mt-2 text-[11px] text-amber-600 dark:text-amber-400">
|
|
211
|
+
Unknown `bim.*` calls detected — review the source before approving.
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
{inference.parseErrors.length > 0 && (
|
|
215
|
+
<div className="mt-2 text-[11px] text-destructive">
|
|
216
|
+
Script does not parse cleanly — promotion may fail.
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<DialogFooter>
|
|
223
|
+
<Button variant="ghost" onClick={onClose}>
|
|
224
|
+
<X className="mr-1 h-4 w-4" />
|
|
225
|
+
Cancel
|
|
226
|
+
</Button>
|
|
227
|
+
<Button onClick={handlePromote} disabled={busy || name.trim().length === 0}>
|
|
228
|
+
<Sparkles className="mr-1 h-4 w-4" />
|
|
229
|
+
Review & install
|
|
230
|
+
</Button>
|
|
231
|
+
</DialogFooter>
|
|
232
|
+
</DialogContent>
|
|
233
|
+
</Dialog>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface SynthArgs {
|
|
238
|
+
name: string;
|
|
239
|
+
source: string;
|
|
240
|
+
hotkey: string;
|
|
241
|
+
icon: string;
|
|
242
|
+
capabilities: string[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function synthesiseBundle(
|
|
246
|
+
args: SynthArgs,
|
|
247
|
+
): Promise<{ bytes: Uint8Array; summary: ExtensionInstallSummary }> {
|
|
248
|
+
const slug = slugFromName(args.name);
|
|
249
|
+
const id = `com.local.tools.${slug}`;
|
|
250
|
+
const commandId = `${id}.run`;
|
|
251
|
+
const caps = args.capabilities.length > 0 ? args.capabilities : ['model.read'];
|
|
252
|
+
// Engine range MUST match the running SDK or the loader skips the
|
|
253
|
+
// bundle on install — the tool then never appears as a toolbar
|
|
254
|
+
// button. Pin to ">=<currentSdk>" using the live app version
|
|
255
|
+
// instead of a hardcoded guess.
|
|
256
|
+
const sdkVersion =
|
|
257
|
+
typeof __APP_VERSION__ === 'string' && __APP_VERSION__.length > 0
|
|
258
|
+
? __APP_VERSION__
|
|
259
|
+
: '1.0.0';
|
|
260
|
+
const manifest: ExtensionManifest = {
|
|
261
|
+
manifestVersion: 1,
|
|
262
|
+
id,
|
|
263
|
+
name: args.name,
|
|
264
|
+
description: `Promoted from a saved script.`,
|
|
265
|
+
version: '0.1.0',
|
|
266
|
+
engines: { ifcLiteSdk: `>=${sdkVersion}` },
|
|
267
|
+
capabilities: caps,
|
|
268
|
+
activation: [`onCommand:${commandId}`],
|
|
269
|
+
contributes: {
|
|
270
|
+
commands: [{ id: commandId, title: args.name, icon: args.icon }],
|
|
271
|
+
toolbar: [{ command: commandId, slot: 'toolbar.right' }],
|
|
272
|
+
...(args.hotkey
|
|
273
|
+
? { keybindings: [{ command: commandId, key: args.hotkey.trim() }] }
|
|
274
|
+
: {}),
|
|
275
|
+
},
|
|
276
|
+
entry: { commands: { [commandId]: 'src/commands/run.js' } },
|
|
277
|
+
};
|
|
278
|
+
const handler = wrapScriptAsCommand(args.source);
|
|
279
|
+
const files = new Map<string, { path: string; bytes: Uint8Array; text?: string }>();
|
|
280
|
+
const manifestText = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
281
|
+
files.set('manifest.json', { path: 'manifest.json', bytes: new TextEncoder().encode(manifestText), text: manifestText });
|
|
282
|
+
files.set('src/commands/run.js', { path: 'src/commands/run.js', bytes: new TextEncoder().encode(handler), text: handler });
|
|
283
|
+
const bundle: Bundle = { manifest, files, source: { kind: 'memory' } };
|
|
284
|
+
const bytes = packBundle(bundle);
|
|
285
|
+
const hash = await sha256Hex(bytes);
|
|
286
|
+
return {
|
|
287
|
+
bytes,
|
|
288
|
+
summary: { id, version: '0.1.0', bundleHash: hash, capabilities: caps, bundle, signed: false },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function slugFromName(name: string): string {
|
|
293
|
+
return name
|
|
294
|
+
.toLowerCase()
|
|
295
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
296
|
+
.replace(/^-+|-+$/g, '')
|
|
297
|
+
|| `tool-${Math.random().toString(36).slice(2, 8)}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function wrapScriptAsCommand(source: string): string {
|
|
301
|
+
// `run` is intentionally NOT async. The bim.* SDK is fully
|
|
302
|
+
// synchronous, and a promoted script is plain top-level code. An
|
|
303
|
+
// `async` wrapper would return a pending promise whose body only
|
|
304
|
+
// runs once the QuickJS job queue is drained — so the tool would
|
|
305
|
+
// silently do nothing (0 logs, instant "success"). A sync function
|
|
306
|
+
// runs to completion inside evalCode, exactly like the one-shot.
|
|
307
|
+
return `/* Promoted from a saved script. */
|
|
308
|
+
function run(ctx) {
|
|
309
|
+
const bim = ctx.bim;
|
|
310
|
+
${source.trim().split('\n').map((line) => ` ${line}`).join('\n')}
|
|
311
|
+
}
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `RepairQueuePanel` — surface SDK-update revalidation results.
|
|
7
|
+
*
|
|
8
|
+
* Runs `ExtensionHostService.revalidateForSdk(currentSdk)` on mount,
|
|
9
|
+
* lists each extension with compatibility status + test outcome, and
|
|
10
|
+
* lets the user trigger an AI-assisted repair for items in the
|
|
11
|
+
* `needsRepair` bucket. Repair routing seeds the chat with a fix
|
|
12
|
+
* prompt; the chat panel then drives the regular authoring loop.
|
|
13
|
+
*
|
|
14
|
+
* Spec: docs/architecture/ai-customization/06-self-improvement.md §5.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useState } from 'react';
|
|
18
|
+
import { CheckCircle2, RefreshCcw, ShieldAlert, Wrench, X } from 'lucide-react';
|
|
19
|
+
import type { RevalidationItem, RevalidationSummary } from '@ifc-lite/extensions';
|
|
20
|
+
import { Button } from '@/components/ui/button';
|
|
21
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
22
|
+
import { useExtensionHost } from '@/sdk/ExtensionHostProvider';
|
|
23
|
+
import { useViewerStore } from '@/store';
|
|
24
|
+
import { toast } from '@/components/ui/toast';
|
|
25
|
+
import { HelpHint } from './HelpHint';
|
|
26
|
+
|
|
27
|
+
interface RepairQueuePanelProps {
|
|
28
|
+
/** SDK version to revalidate against. Defaults to APP_VERSION. */
|
|
29
|
+
sdkVersion?: string;
|
|
30
|
+
onClose?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function RepairQueuePanel({ sdkVersion, onClose }: RepairQueuePanelProps) {
|
|
34
|
+
const host = useExtensionHost();
|
|
35
|
+
const queueChatPrompt = useViewerStore((s) => s.queueChatPrompt);
|
|
36
|
+
const setChatPanelVisible = useViewerStore((s) => s.setChatPanelVisible);
|
|
37
|
+
const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible);
|
|
38
|
+
const [summary, setSummary] = useState<RevalidationSummary | undefined>();
|
|
39
|
+
const [busy, setBusy] = useState(false);
|
|
40
|
+
// SDK version comes from the Vite-injected __APP_VERSION__ define.
|
|
41
|
+
// We deliberately do NOT fall back to '0.0.0' on miss — a fake low
|
|
42
|
+
// version would flag every range as outdated and produce a wave of
|
|
43
|
+
// false-positive repair prompts.
|
|
44
|
+
const version =
|
|
45
|
+
sdkVersion
|
|
46
|
+
?? (typeof __APP_VERSION__ === 'string' && __APP_VERSION__.length > 0 ? __APP_VERSION__ : undefined);
|
|
47
|
+
|
|
48
|
+
const run = useCallback(async () => {
|
|
49
|
+
if (!version) return;
|
|
50
|
+
setBusy(true);
|
|
51
|
+
try {
|
|
52
|
+
const next = await host.revalidateForSdk(version);
|
|
53
|
+
setSummary(next);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
toast.error(`Revalidation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
} finally {
|
|
57
|
+
setBusy(false);
|
|
58
|
+
}
|
|
59
|
+
}, [host, version]);
|
|
60
|
+
|
|
61
|
+
// Eager-run is gated behind an explicit user click. Mounting alone
|
|
62
|
+
// shouldn't spin up sandboxes for every installed extension — that
|
|
63
|
+
// can be expensive when many extensions are installed and outdated.
|
|
64
|
+
|
|
65
|
+
const repairItem = (item: RevalidationItem) => {
|
|
66
|
+
if (!version) return;
|
|
67
|
+
queueChatPrompt(buildRepairPrompt(item, version));
|
|
68
|
+
setChatPanelVisible(true);
|
|
69
|
+
setScriptPanelVisible(true);
|
|
70
|
+
toast.success(`Routing repair for ${item.extensionId}…`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex flex-col h-full">
|
|
75
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<Wrench className="h-4 w-4" />
|
|
78
|
+
<h2 className="text-sm font-semibold">Repair queue</h2>
|
|
79
|
+
{summary && (
|
|
80
|
+
<span className="text-[11px] text-muted-foreground">
|
|
81
|
+
SDK {summary.sdk} · {summary.needsRepair.length} need fixing
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
<HelpHint label="Repair queue">
|
|
85
|
+
<p>
|
|
86
|
+
When the viewer SDK bumps, extensions whose declared
|
|
87
|
+
<code> engines.ifcLiteSdk</code> range no longer matches
|
|
88
|
+
are flagged here.
|
|
89
|
+
</p>
|
|
90
|
+
<p>
|
|
91
|
+
<strong>Run check</strong> spins up a sandbox for each
|
|
92
|
+
outdated extension and runs its manifest tests against
|
|
93
|
+
the new SDK. Failing tests get a <strong>Repair</strong>
|
|
94
|
+
button that seeds chat with a fix prompt — the AI
|
|
95
|
+
authoring loop produces the patched bundle.
|
|
96
|
+
</p>
|
|
97
|
+
<p>
|
|
98
|
+
Doesn't run automatically (each check spawns sandboxes).
|
|
99
|
+
</p>
|
|
100
|
+
</HelpHint>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="flex items-center gap-1">
|
|
103
|
+
<Button size="sm" variant="ghost" onClick={() => void run()} disabled={busy || !version}>
|
|
104
|
+
<RefreshCcw className="mr-1 h-3.5 w-3.5" />
|
|
105
|
+
Re-run
|
|
106
|
+
</Button>
|
|
107
|
+
{onClose && (
|
|
108
|
+
<Button size="icon" variant="ghost" onClick={onClose} aria-label="Close">
|
|
109
|
+
<X className="h-3.5 w-3.5" />
|
|
110
|
+
</Button>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<ScrollArea className="flex-1">
|
|
116
|
+
{!version ? (
|
|
117
|
+
<div className="px-6 py-12 text-center text-sm text-rose-600 dark:text-rose-400">
|
|
118
|
+
SDK version unknown — cannot revalidate. Set <code className="font-mono">__APP_VERSION__</code> via Vite define.
|
|
119
|
+
</div>
|
|
120
|
+
) : !summary ? (
|
|
121
|
+
<div className="px-6 py-12 text-center text-sm text-muted-foreground space-y-3">
|
|
122
|
+
<div>No compatibility check has run for this session.</div>
|
|
123
|
+
<Button size="sm" variant="outline" onClick={() => void run()} disabled={busy}>
|
|
124
|
+
<RefreshCcw className="mr-1 h-3.5 w-3.5" />
|
|
125
|
+
Run check
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
) : summary.items.length === 0 ? (
|
|
129
|
+
<div className="flex flex-col items-center justify-center gap-2 px-6 py-12 text-center">
|
|
130
|
+
<CheckCircle2 className="h-8 w-8 text-emerald-500" />
|
|
131
|
+
<div className="text-sm font-medium">No installed extensions</div>
|
|
132
|
+
</div>
|
|
133
|
+
) : (
|
|
134
|
+
<ul className="divide-y">
|
|
135
|
+
{summary.items.map((item) => (
|
|
136
|
+
<RepairRow key={item.extensionId} item={item} onRepair={() => repairItem(item)} />
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
)}
|
|
140
|
+
</ScrollArea>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function RepairRow({
|
|
146
|
+
item,
|
|
147
|
+
onRepair,
|
|
148
|
+
}: {
|
|
149
|
+
item: RevalidationItem;
|
|
150
|
+
onRepair: () => void;
|
|
151
|
+
}) {
|
|
152
|
+
const tone =
|
|
153
|
+
item.outcome === 'pass'
|
|
154
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
155
|
+
: item.outcome === 'skipped'
|
|
156
|
+
? 'text-muted-foreground'
|
|
157
|
+
: 'text-rose-600 dark:text-rose-400';
|
|
158
|
+
return (
|
|
159
|
+
<li className="px-4 py-3">
|
|
160
|
+
<div className="flex items-start justify-between gap-3">
|
|
161
|
+
<div className="flex-1 min-w-0">
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
{item.outcome === 'pass' ? (
|
|
164
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />
|
|
165
|
+
) : (
|
|
166
|
+
<ShieldAlert className={`h-3.5 w-3.5 ${tone}`} />
|
|
167
|
+
)}
|
|
168
|
+
<code className="text-xs font-mono break-all">{item.extensionId}</code>
|
|
169
|
+
<span className={`text-[10px] uppercase tracking-wide font-semibold ${tone}`}>
|
|
170
|
+
{item.outcome}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
174
|
+
Range <code className="font-mono">{item.compatibility.declared}</code> · {item.compatibility.reason}
|
|
175
|
+
</div>
|
|
176
|
+
{item.tests && item.tests.failed > 0 && (
|
|
177
|
+
<div className="mt-1 text-[11px] text-rose-600 dark:text-rose-400">
|
|
178
|
+
{item.tests.failed} test{item.tests.failed === 1 ? '' : 's'} failed:
|
|
179
|
+
{' '}
|
|
180
|
+
{item.tests.results.find((r) => !r.passed)?.error}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
{itemNeedsRepair(item) && (
|
|
185
|
+
<Button size="sm" variant="outline" onClick={onRepair}>
|
|
186
|
+
<Wrench className="mr-1 h-3.5 w-3.5" />
|
|
187
|
+
Repair
|
|
188
|
+
</Button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</li>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Whether a row should show a Repair button. Mirrors the
|
|
197
|
+
* `needsRepair` filter in `revalidateAgainstSdk` exactly — a failed
|
|
198
|
+
* test OR a skipped extension whose declared range is outdated — so
|
|
199
|
+
* the header count and the actionable rows never disagree.
|
|
200
|
+
*/
|
|
201
|
+
function itemNeedsRepair(item: RevalidationItem): boolean {
|
|
202
|
+
return (
|
|
203
|
+
item.outcome === 'fail'
|
|
204
|
+
|| (item.outcome === 'skipped' && item.compatibility.status === 'outdated')
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildRepairPrompt(item: RevalidationItem, sdk: string): string {
|
|
209
|
+
const failures = item.tests?.results.filter((r) => !r.passed) ?? [];
|
|
210
|
+
return [
|
|
211
|
+
`Repair extension ${item.extensionId} for SDK ${sdk}.`,
|
|
212
|
+
'',
|
|
213
|
+
`The declared engine range was \`${item.compatibility.declared}\` (status: ${item.compatibility.status}).`,
|
|
214
|
+
'',
|
|
215
|
+
failures.length > 0
|
|
216
|
+
? `${failures.length} test${failures.length === 1 ? '' : 's'} failed under the new SDK:`
|
|
217
|
+
: 'No failing tests were captured; revalidation flagged compatibility only.',
|
|
218
|
+
...failures.map((f) => `- ${f.name}: ${f.error ?? 'unknown'}`),
|
|
219
|
+
'',
|
|
220
|
+
'Update the bundle so tests pass against the new SDK while keeping the same user-visible behaviour. Bump engines.ifcLiteSdk as appropriate.',
|
|
221
|
+
].join('\n');
|
|
222
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared icon registry for extension commands.
|
|
7
|
+
*
|
|
8
|
+
* The picker (`PromoteToolDialog`) and the renderers
|
|
9
|
+
* (`ExtensionToolbarSlot`, command palette, context menu) all read
|
|
10
|
+
* from this single source so the icon a user picks actually shows up
|
|
11
|
+
* everywhere their command appears.
|
|
12
|
+
*
|
|
13
|
+
* Manifests record only the string key — never a component reference —
|
|
14
|
+
* so the bundle stays serialisable and portable.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
AlertTriangle, Beaker, Box, Brain, Calculator, Camera, CheckCircle2,
|
|
19
|
+
ClipboardList, Download, Eye, FileBarChart, FileText, Filter, Flame,
|
|
20
|
+
Gauge, Hammer, Layers, Lightbulb, Maximize2, Palette, Ruler, ScanSearch,
|
|
21
|
+
Scissors, Settings, Shield, Sparkles, Tag, Target, Wrench,
|
|
22
|
+
type LucideIcon,
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
|
|
25
|
+
export interface IconChoice {
|
|
26
|
+
key: string;
|
|
27
|
+
Icon: LucideIcon;
|
|
28
|
+
label: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Curated lucide subset (~29). Keys are lowercase-hyphenated so they
|
|
33
|
+
* survive JSON round-trips in `manifest.contributes.commands[].icon`.
|
|
34
|
+
*/
|
|
35
|
+
export const ICON_CHOICES: readonly IconChoice[] = [
|
|
36
|
+
{ key: 'sparkles', Icon: Sparkles, label: 'AI / magic' },
|
|
37
|
+
{ key: 'wrench', Icon: Wrench, label: 'Tool' },
|
|
38
|
+
{ key: 'hammer', Icon: Hammer, label: 'Build' },
|
|
39
|
+
{ key: 'palette', Icon: Palette, label: 'Color' },
|
|
40
|
+
{ key: 'eye', Icon: Eye, label: 'View' },
|
|
41
|
+
{ key: 'filter', Icon: Filter, label: 'Filter' },
|
|
42
|
+
{ key: 'shield', Icon: Shield, label: 'Compliance' },
|
|
43
|
+
{ key: 'flame', Icon: Flame, label: 'Fire rating' },
|
|
44
|
+
{ key: 'ruler', Icon: Ruler, label: 'Measure' },
|
|
45
|
+
{ key: 'calculator', Icon: Calculator, label: 'Quantity' },
|
|
46
|
+
{ key: 'box', Icon: Box, label: 'Element' },
|
|
47
|
+
{ key: 'layers', Icon: Layers, label: 'Storey' },
|
|
48
|
+
{ key: 'tag', Icon: Tag, label: 'Classification' },
|
|
49
|
+
{ key: 'target', Icon: Target, label: 'Isolate' },
|
|
50
|
+
{ key: 'scan-search', Icon: ScanSearch, label: 'Audit' },
|
|
51
|
+
{ key: 'clipboard-list', Icon: ClipboardList, label: 'Schedule' },
|
|
52
|
+
{ key: 'file-text', Icon: FileText, label: 'Report' },
|
|
53
|
+
{ key: 'file-bar-chart', Icon: FileBarChart, label: 'Chart' },
|
|
54
|
+
{ key: 'download', Icon: Download, label: 'Export' },
|
|
55
|
+
{ key: 'camera', Icon: Camera, label: 'Snapshot' },
|
|
56
|
+
{ key: 'scissors', Icon: Scissors, label: 'Section' },
|
|
57
|
+
{ key: 'maximize-2', Icon: Maximize2, label: 'Fly to' },
|
|
58
|
+
{ key: 'gauge', Icon: Gauge, label: 'Performance' },
|
|
59
|
+
{ key: 'lightbulb', Icon: Lightbulb, label: 'Idea' },
|
|
60
|
+
{ key: 'alert-triangle', Icon: AlertTriangle, label: 'Warning' },
|
|
61
|
+
{ key: 'beaker', Icon: Beaker, label: 'Test' },
|
|
62
|
+
{ key: 'brain', Icon: Brain, label: 'Memory' },
|
|
63
|
+
{ key: 'check-circle-2', Icon: CheckCircle2, label: 'Validate' },
|
|
64
|
+
{ key: 'settings', Icon: Settings, label: 'Setting' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const ICON_LOOKUP = new Map<string, LucideIcon>(
|
|
68
|
+
ICON_CHOICES.map((c) => [c.key, c.Icon]),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a manifest icon string to a lucide component. Falls back to
|
|
73
|
+
* `Sparkles` when the key is unknown or absent so the toolbar still
|
|
74
|
+
* renders something rather than a missing-icon hole. Also accepts a
|
|
75
|
+
* loose match on common variants (case, leading "icon-", underscores)
|
|
76
|
+
* so AI-authored manifests with slight key drift still resolve.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveExtensionIcon(key: string | undefined | null): LucideIcon {
|
|
79
|
+
if (!key) return Sparkles;
|
|
80
|
+
const direct = ICON_LOOKUP.get(key);
|
|
81
|
+
if (direct) return direct;
|
|
82
|
+
// Tolerate AI-authored variants the picker doesn't emit but the model
|
|
83
|
+
// might guess: `Wrench`, `alertTriangle`, `Alert_Triangle`,
|
|
84
|
+
// `icon-wrench`, etc. → all collapse to the kebab-case canonical key.
|
|
85
|
+
const normalised = key
|
|
86
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/^icon[-_]/, '')
|
|
89
|
+
.replace(/_/g, '-')
|
|
90
|
+
.trim();
|
|
91
|
+
return ICON_LOOKUP.get(normalised) ?? Sparkles;
|
|
92
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Centralised toast phrasing for the extensions / flavors surfaces.
|
|
7
|
+
* Keeps tone, capitalisation, and trailing-punctuation consistent
|
|
8
|
+
* across call sites.
|
|
9
|
+
*
|
|
10
|
+
* Callers pass the result to `toast.success` / `toast.info` /
|
|
11
|
+
* `toast.error` so the colour mapping stays at the call site.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function flavorSwitched(name: string): string {
|
|
15
|
+
return `Switched to ${name}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function flavorExported(filename: string): string {
|
|
19
|
+
return `Exported ${filename}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function flavorImported(name: string): string {
|
|
23
|
+
return `Imported ${name}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function flavorDeleted(name: string): string {
|
|
27
|
+
return `Deleted ${name}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function flavorReset(): string {
|
|
31
|
+
return 'Reset to baseline flavor';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function testsPassed(id: string, passed: number, total: number): string {
|
|
35
|
+
return `${id}: ${passed}/${total} tests passed`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function testsNotDeclared(id: string): string {
|
|
39
|
+
return `${id} declares no tests`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function testsFailed(id: string, failed: number, firstError: string): string {
|
|
43
|
+
return `${id}: ${failed} test${failed === 1 ? '' : 's'} failed — ${firstError}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function failed(operation: string, err: unknown): string {
|
|
47
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
48
|
+
return `${operation} failed — ${cause}`;
|
|
49
|
+
}
|