@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,228 @@
|
|
|
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
|
+
* IndexedDB-backed implementation of @ifc-lite/extensions' `ExtensionStorage`
|
|
7
|
+
* interface.
|
|
8
|
+
*
|
|
9
|
+
* Two object stores:
|
|
10
|
+
* - `extensions` keyed by extension id → InstalledExtensionRecord
|
|
11
|
+
* - `extension-bundles` keyed by `<id>@<version>` tuple → Uint8Array
|
|
12
|
+
*
|
|
13
|
+
* On startup, we open the database, verify both stores exist, and recreate
|
|
14
|
+
* the database from scratch if anything is missing (mirrors the recovery
|
|
15
|
+
* pattern used by `services/ifc-cache.ts`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ExtensionStorage,
|
|
20
|
+
InstalledExtensionRecord,
|
|
21
|
+
} from '@ifc-lite/extensions';
|
|
22
|
+
|
|
23
|
+
const DB_NAME = 'ifc-lite-extensions';
|
|
24
|
+
/**
|
|
25
|
+
* Bump this when adding/removing/renaming object stores or indexes.
|
|
26
|
+
* Every new version MUST extend the `onupgradeneeded` switch below
|
|
27
|
+
* with an idempotent migration step from `event.oldVersion`.
|
|
28
|
+
*/
|
|
29
|
+
const DB_VERSION = 1;
|
|
30
|
+
const STORE_EXT = 'extensions';
|
|
31
|
+
const STORE_BUNDLES = 'extension-bundles';
|
|
32
|
+
|
|
33
|
+
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Thrown from any IDB write path when the browser refuses the
|
|
37
|
+
* operation because the origin's storage quota is exhausted.
|
|
38
|
+
* Callers (host-installer, host) should catch this specifically and
|
|
39
|
+
* surface a "free up space / uninstall something" toast — bubbling
|
|
40
|
+
* the raw `QuotaExceededError` produces a generic console error and
|
|
41
|
+
* a silent UI.
|
|
42
|
+
*/
|
|
43
|
+
export class ExtensionStorageQuotaError extends Error {
|
|
44
|
+
readonly cause?: unknown;
|
|
45
|
+
constructor(operation: string, cause?: unknown) {
|
|
46
|
+
super(`Browser storage quota exceeded while ${operation}.`);
|
|
47
|
+
this.name = 'ExtensionStorageQuotaError';
|
|
48
|
+
this.cause = cause;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isQuotaError(err: unknown): boolean {
|
|
53
|
+
if (!err || typeof err !== 'object') return false;
|
|
54
|
+
const name = (err as { name?: unknown }).name;
|
|
55
|
+
return name === 'QuotaExceededError' || name === 'NS_ERROR_DOM_QUOTA_REACHED';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class IdbExtensionStorage implements ExtensionStorage {
|
|
59
|
+
async putExtension(record: InstalledExtensionRecord): Promise<void> {
|
|
60
|
+
const db = await openDatabase();
|
|
61
|
+
try {
|
|
62
|
+
await runStore(db, STORE_EXT, 'readwrite', (store) => store.put(record));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (isQuotaError(err)) {
|
|
65
|
+
throw new ExtensionStorageQuotaError(
|
|
66
|
+
`saving extension record "${record.id}"`,
|
|
67
|
+
err,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getExtension(id: string): Promise<InstalledExtensionRecord | undefined> {
|
|
75
|
+
const db = await openDatabase();
|
|
76
|
+
return runStore(db, STORE_EXT, 'readonly', (store) => store.get(id))
|
|
77
|
+
.then((v) => (v ? (v as InstalledExtensionRecord) : undefined));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async listExtensions(): Promise<InstalledExtensionRecord[]> {
|
|
81
|
+
const db = await openDatabase();
|
|
82
|
+
return runStore<InstalledExtensionRecord[]>(db, STORE_EXT, 'readonly', (store) => store.getAll());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async deleteExtension(id: string): Promise<void> {
|
|
86
|
+
const db = await openDatabase();
|
|
87
|
+
await runStore(db, STORE_EXT, 'readwrite', (store) => store.delete(id));
|
|
88
|
+
// Cascade: drop bundles for this extension. We can't use runStore
|
|
89
|
+
// here — it overwrites req.onsuccess to capture the result, which
|
|
90
|
+
// clobbers the cursor-iteration handler. Roll our own transaction.
|
|
91
|
+
await new Promise<void>((resolve, reject) => {
|
|
92
|
+
const tx = db.transaction(STORE_BUNDLES, 'readwrite');
|
|
93
|
+
const store = tx.objectStore(STORE_BUNDLES);
|
|
94
|
+
const req = store.openCursor();
|
|
95
|
+
req.onsuccess = () => {
|
|
96
|
+
const cursor = req.result;
|
|
97
|
+
if (!cursor) return;
|
|
98
|
+
const key = String(cursor.key);
|
|
99
|
+
if (key.startsWith(`${id}@`)) cursor.delete();
|
|
100
|
+
cursor.continue();
|
|
101
|
+
};
|
|
102
|
+
req.onerror = () => reject(req.error);
|
|
103
|
+
tx.oncomplete = () => resolve();
|
|
104
|
+
tx.onerror = () => reject(tx.error);
|
|
105
|
+
tx.onabort = () => reject(tx.error);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async putBundle(id: string, version: string, bytes: Uint8Array): Promise<void> {
|
|
110
|
+
const db = await openDatabase();
|
|
111
|
+
try {
|
|
112
|
+
await runStore(db, STORE_BUNDLES, 'readwrite', (store) =>
|
|
113
|
+
store.put(new Uint8Array(bytes), bundleKey(id, version)),
|
|
114
|
+
);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (isQuotaError(err)) {
|
|
117
|
+
throw new ExtensionStorageQuotaError(
|
|
118
|
+
`saving bundle bytes for "${id}@${version}" (${bytes.byteLength} bytes)`,
|
|
119
|
+
err,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getBundle(id: string, version: string): Promise<Uint8Array | undefined> {
|
|
127
|
+
const db = await openDatabase();
|
|
128
|
+
const value = await runStore<Uint8Array | undefined>(
|
|
129
|
+
db,
|
|
130
|
+
STORE_BUNDLES,
|
|
131
|
+
'readonly',
|
|
132
|
+
(store) => store.get(bundleKey(id, version)),
|
|
133
|
+
);
|
|
134
|
+
return value ? new Uint8Array(value) : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async deleteBundle(id: string, version: string): Promise<void> {
|
|
138
|
+
const db = await openDatabase();
|
|
139
|
+
await runStore(db, STORE_BUNDLES, 'readwrite', (store) =>
|
|
140
|
+
store.delete(bundleKey(id, version)),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async clear(): Promise<void> {
|
|
145
|
+
const db = await openDatabase();
|
|
146
|
+
await runStore(db, STORE_EXT, 'readwrite', (store) => store.clear());
|
|
147
|
+
await runStore(db, STORE_BUNDLES, 'readwrite', (store) => store.clear());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function bundleKey(id: string, version: string): string {
|
|
152
|
+
return `${id}@${version}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function openDatabase(): Promise<IDBDatabase> {
|
|
156
|
+
if (dbPromise) return dbPromise;
|
|
157
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
158
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
159
|
+
request.onerror = () => {
|
|
160
|
+
console.error('[extensions/idb] Failed to open database:', request.error);
|
|
161
|
+
dbPromise = null;
|
|
162
|
+
reject(request.error);
|
|
163
|
+
};
|
|
164
|
+
request.onupgradeneeded = (event) => {
|
|
165
|
+
const db = request.result;
|
|
166
|
+
// Migrations are append-only. When DB_VERSION bumps:
|
|
167
|
+
// case 1: ... // run v1→v2 migration here (e.g. add an index)
|
|
168
|
+
// case 2: ... // run v2→v3 here
|
|
169
|
+
// The createObjectStore calls below seed a fresh database (when
|
|
170
|
+
// event.oldVersion is 0) and remain idempotent for users who land
|
|
171
|
+
// here because recovery deleted the database below.
|
|
172
|
+
switch (event.oldVersion) {
|
|
173
|
+
case 0:
|
|
174
|
+
db.createObjectStore(STORE_EXT, { keyPath: 'id' });
|
|
175
|
+
db.createObjectStore(STORE_BUNDLES);
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
// No-op until a v2+ migration exists. Leaving the switch
|
|
179
|
+
// explicit keeps the contract visible: every future bump
|
|
180
|
+
// adds its own case.
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
request.onsuccess = () => {
|
|
185
|
+
const db = request.result;
|
|
186
|
+
if (!db.objectStoreNames.contains(STORE_EXT) || !db.objectStoreNames.contains(STORE_BUNDLES)) {
|
|
187
|
+
// Recovery: delete and recreate.
|
|
188
|
+
db.close();
|
|
189
|
+
dbPromise = null;
|
|
190
|
+
const del = indexedDB.deleteDatabase(DB_NAME);
|
|
191
|
+
del.onsuccess = () => openDatabase().then(resolve).catch(reject);
|
|
192
|
+
del.onerror = () => reject(new Error('Failed to recreate extensions database.'));
|
|
193
|
+
// Without onblocked, another tab holding a connection makes the
|
|
194
|
+
// delete request hang indefinitely and openDatabase() never
|
|
195
|
+
// resolves. Reject explicitly so the caller can surface the issue.
|
|
196
|
+
del.onblocked = () => reject(new Error(
|
|
197
|
+
'Extensions database recreation is blocked by another open tab. Close other tabs and reload.',
|
|
198
|
+
));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
resolve(db);
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
return dbPromise;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function runStore<T = unknown>(
|
|
208
|
+
db: IDBDatabase,
|
|
209
|
+
storeName: string,
|
|
210
|
+
mode: IDBTransactionMode,
|
|
211
|
+
fn: (store: IDBObjectStore) => IDBRequest | void,
|
|
212
|
+
): Promise<T> {
|
|
213
|
+
return new Promise<T>((resolve, reject) => {
|
|
214
|
+
const tx = db.transaction(storeName, mode);
|
|
215
|
+
const store = tx.objectStore(storeName);
|
|
216
|
+
let value: unknown;
|
|
217
|
+
const req = fn(store);
|
|
218
|
+
if (req instanceof IDBRequest) {
|
|
219
|
+
req.onsuccess = () => {
|
|
220
|
+
value = req.result;
|
|
221
|
+
};
|
|
222
|
+
req.onerror = () => reject(req.error);
|
|
223
|
+
}
|
|
224
|
+
tx.oncomplete = () => resolve(value as T);
|
|
225
|
+
tx.onerror = () => reject(tx.error);
|
|
226
|
+
tx.onabort = () => reject(tx.error);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* Helpers for turning runtime errors thrown by extension command
|
|
7
|
+
* execution into user-facing messages. The sandbox throws
|
|
8
|
+
* `CapabilityDeniedError` (and friends) by `.name`; we don't want
|
|
9
|
+
* every callsite re-implementing the discrimination, and we want a
|
|
10
|
+
* single place to refine the wording when we revisit copy.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export function describeRunCommandError(commandId: string, err: unknown): string {
|
|
14
|
+
if (err && typeof err === 'object' && 'name' in err) {
|
|
15
|
+
const name = (err as { name?: string }).name;
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
if (name === 'CapabilityDeniedError') {
|
|
18
|
+
return (
|
|
19
|
+
`"${commandId}" tried to use a capability that wasn't granted. ` +
|
|
20
|
+
`Re-install with the missing capability checked, or skip this action. (${message})`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25
|
+
return `Failed to run "${commandId}": ${message}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
* Adapter — `RuntimeSandboxFactory` backed by `@ifc-lite/sandbox`.
|
|
7
|
+
*
|
|
8
|
+
* The viewer's production sandbox is the QuickJS-WASM runtime that the
|
|
9
|
+
* existing `@ifc-lite/sandbox` package already wraps. This module
|
|
10
|
+
* adapts that surface into the runtime contract defined by
|
|
11
|
+
* `@ifc-lite/extensions`:
|
|
12
|
+
*
|
|
13
|
+
* - `RuntimeSandboxHandle.setGlobal(name, value)` is implemented by
|
|
14
|
+
* wrapping the value as a JSON literal and pre-defining it on the
|
|
15
|
+
* QuickJS realm before each `run`.
|
|
16
|
+
* - `RuntimeSandboxHandle.run(source)` evaluates the wrapped source
|
|
17
|
+
* via `Sandbox.eval`, maps the returned log entries into the
|
|
18
|
+
* runtime's `RuntimeLogEntry` shape.
|
|
19
|
+
* - `RuntimeSandboxHandle.dispose()` calls `Sandbox.dispose()`.
|
|
20
|
+
*
|
|
21
|
+
* The factory accepts a `BimContext` at construction; that SDK is
|
|
22
|
+
* passed to every Sandbox created. The capability layer (in
|
|
23
|
+
* `@ifc-lite/extensions/host`) enforces granular access; the sandbox's
|
|
24
|
+
* coarse permission flags act as the outer ring.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
assertMethodCall,
|
|
29
|
+
CapabilityDeniedError,
|
|
30
|
+
type Capability,
|
|
31
|
+
type RuntimeRunOptions,
|
|
32
|
+
type RuntimeRunResult,
|
|
33
|
+
type RuntimeSandboxCreateOptions,
|
|
34
|
+
type RuntimeSandboxFactory,
|
|
35
|
+
type RuntimeSandboxHandle,
|
|
36
|
+
} from '@ifc-lite/extensions';
|
|
37
|
+
import { createSandbox, type Sandbox } from '@ifc-lite/sandbox';
|
|
38
|
+
import type { BimContext } from '@ifc-lite/sdk';
|
|
39
|
+
|
|
40
|
+
export interface SandboxFactoryOptions {
|
|
41
|
+
sdk: BimContext;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createBimSandboxFactory(opts: SandboxFactoryOptions): RuntimeSandboxFactory {
|
|
45
|
+
return {
|
|
46
|
+
async create(createOpts: RuntimeSandboxCreateOptions): Promise<RuntimeSandboxHandle> {
|
|
47
|
+
// Wrap the SDK with a per-method capability gate using the
|
|
48
|
+
// create-time grants. The outer-ring permission flags already
|
|
49
|
+
// gate at namespace level (model/viewer/etc.); this Proxy is
|
|
50
|
+
// the inner ring that flags fine-grained denials like
|
|
51
|
+
// "granted viewer.colorize but called viewer.fly".
|
|
52
|
+
const gatedSdk = createOpts.grants
|
|
53
|
+
? wrapWithCapabilityGate(opts.sdk, createOpts.grants, createOpts.extensionId)
|
|
54
|
+
: opts.sdk;
|
|
55
|
+
const sandbox = await createSandbox(gatedSdk, {
|
|
56
|
+
permissions: createOpts.permissions,
|
|
57
|
+
limits: createOpts.limits,
|
|
58
|
+
});
|
|
59
|
+
return new BimSandboxHandle(sandbox);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wrap the BimContext so each object-namespace method runs
|
|
66
|
+
* `assertMethodCall(namespace, method, grants)` before forwarding.
|
|
67
|
+
*
|
|
68
|
+
* Implemented as a Proxy on the SDK ROOT — not by enumerating
|
|
69
|
+
* `Object.keys(sdk)`. The BimContext is a class instance: its
|
|
70
|
+
* namespaces and top-level methods (`query`, `entity`, `viewer`, …)
|
|
71
|
+
* live on the prototype, so `Object.keys` returns none of them and a
|
|
72
|
+
* key-enumerated copy comes out empty — every `bim.*` call then fails
|
|
73
|
+
* with "<x> is not a function". The root Proxy resolves members via
|
|
74
|
+
* the prototype chain, so nothing is dropped.
|
|
75
|
+
*
|
|
76
|
+
* Top-level functions (e.g. `sdk.query()`, `sdk.entity()`) pass
|
|
77
|
+
* through ungated — the coarse permission ring already gates whole
|
|
78
|
+
* namespaces; this inner ring only wraps object-namespace methods
|
|
79
|
+
* (`sdk.viewer.colorize`, etc.).
|
|
80
|
+
*/
|
|
81
|
+
function wrapWithCapabilityGate(
|
|
82
|
+
sdk: BimContext,
|
|
83
|
+
grants: readonly Capability[],
|
|
84
|
+
extensionId: string,
|
|
85
|
+
): BimContext {
|
|
86
|
+
const nsCache = new Map<string, unknown>();
|
|
87
|
+
return new Proxy(sdk as object, {
|
|
88
|
+
get(target, prop) {
|
|
89
|
+
const value = (target as Record<string | symbol, unknown>)[prop];
|
|
90
|
+
if (typeof prop !== 'string') return value;
|
|
91
|
+
// Functions / primitives pass through; only object namespaces
|
|
92
|
+
// get the per-method capability gate.
|
|
93
|
+
if (value === null || typeof value !== 'object') return value;
|
|
94
|
+
let wrapped = nsCache.get(prop);
|
|
95
|
+
if (!wrapped) {
|
|
96
|
+
wrapped = new Proxy(value as object, {
|
|
97
|
+
get(nsTarget, method) {
|
|
98
|
+
const m = (nsTarget as Record<string | symbol, unknown>)[method];
|
|
99
|
+
if (typeof m !== 'function' || typeof method !== 'string') return m;
|
|
100
|
+
return function gated(this: unknown, ...args: unknown[]) {
|
|
101
|
+
try {
|
|
102
|
+
assertMethodCall(prop, method, grants);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof CapabilityDeniedError) {
|
|
105
|
+
console.warn(`[ext:${extensionId}] denied ${prop}.${method}: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
return (m as (...a: unknown[]) => unknown).apply(nsTarget, args);
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
nsCache.set(prop, wrapped);
|
|
114
|
+
}
|
|
115
|
+
return wrapped;
|
|
116
|
+
},
|
|
117
|
+
}) as unknown as BimContext;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class BimSandboxHandle implements RuntimeSandboxHandle {
|
|
121
|
+
/**
|
|
122
|
+
* Globals pre-defined for the next `run`, keyed by name so re-setting
|
|
123
|
+
* a global REPLACES its assignment instead of appending a duplicate.
|
|
124
|
+
* (A plain accumulating string grew the wrapped source ~54 chars on
|
|
125
|
+
* every run as `__ifclite_ctx__` was re-set.)
|
|
126
|
+
*/
|
|
127
|
+
private globals = new Map<string, string>();
|
|
128
|
+
private disposed = false;
|
|
129
|
+
|
|
130
|
+
constructor(private sandbox: Sandbox) {}
|
|
131
|
+
|
|
132
|
+
setGlobal(name: string, value: unknown): void {
|
|
133
|
+
if (this.disposed) throw new Error('Sandbox disposed.');
|
|
134
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
|
|
135
|
+
throw new Error(`Invalid global name: ${name}`);
|
|
136
|
+
}
|
|
137
|
+
// Special-case `__ifclite_ctx__` — the runtime calls
|
|
138
|
+
// `setGlobal('__ifclite_ctx__', { bim: <host SDK> })` for every
|
|
139
|
+
// activate / command-run. The host SDK is the wrapped BimContext
|
|
140
|
+
// (cyclic Proxies for the inner-ring capability gate) so JSON
|
|
141
|
+
// serialisation crashes. The bridge has already installed `bim`
|
|
142
|
+
// inside the QuickJS realm — synthesize ctx from that instead.
|
|
143
|
+
if (name === '__ifclite_ctx__') {
|
|
144
|
+
this.globals.set(name, `globalThis.__ifclite_ctx__ = { bim: globalThis.bim };`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Other globals (test args, synthetic-spec data) are JSON-safe;
|
|
148
|
+
// serialise so the value crosses the realm boundary intact.
|
|
149
|
+
let serialised: string;
|
|
150
|
+
try {
|
|
151
|
+
serialised = JSON.stringify(value ?? null);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`setGlobal("${name}"): value is not JSON-serialisable (${err instanceof Error ? err.message : err}).`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
this.globals.set(name, `globalThis.${name} = ${serialised};`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async run(source: string, _opts?: RuntimeRunOptions): Promise<RuntimeRunResult> {
|
|
161
|
+
if (this.disposed) throw new Error('Sandbox disposed.');
|
|
162
|
+
const prelude = [...this.globals.values()].join('\n');
|
|
163
|
+
const wrapped = prelude ? `${prelude}\n${source}` : source;
|
|
164
|
+
let result;
|
|
165
|
+
try {
|
|
166
|
+
result = await this.sandbox.eval(wrapped, { typescript: false });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// QuickJS throws "Lifetime not alive" (QuickJSUseAfterFree) when
|
|
169
|
+
// a handle is touched after its underlying realm was disposed —
|
|
170
|
+
// typically because a prior run / flavor switch tore down this
|
|
171
|
+
// sandbox while the host still held the activation record. Mark
|
|
172
|
+
// ourselves disposed so the runtime knows to reactivate on the
|
|
173
|
+
// next call, and surface a clear retry-friendly message.
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
if (/Lifetime not alive|QuickJSUseAfterFree/i.test(msg)) {
|
|
176
|
+
this.disposed = true;
|
|
177
|
+
try { this.sandbox.dispose(); } catch { /* already torn down */ }
|
|
178
|
+
throw new Error(
|
|
179
|
+
'Sandbox was torn down between activate and run. Click Run again — the runtime will reactivate.',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
value: result.value,
|
|
186
|
+
logs: result.logs.map((log) => ({
|
|
187
|
+
level: log.level === 'log' ? 'log' : log.level,
|
|
188
|
+
message: log.args.map(stringifyArg).join(' '),
|
|
189
|
+
timestamp: log.timestamp,
|
|
190
|
+
})),
|
|
191
|
+
durationMs: result.durationMs,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** True iff the sandbox has been torn down (host-disposed or auto-disposed on a Lifetime crash). */
|
|
196
|
+
get isDisposed(): boolean {
|
|
197
|
+
return this.disposed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
dispose(): void {
|
|
201
|
+
if (this.disposed) return;
|
|
202
|
+
this.disposed = true;
|
|
203
|
+
this.sandbox.dispose();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stringifyArg(arg: unknown): string {
|
|
208
|
+
if (typeof arg === 'string') return arg;
|
|
209
|
+
try {
|
|
210
|
+
return JSON.stringify(arg);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
// JSON.stringify throws on cycles / BigInt — fall back to String()
|
|
213
|
+
// but log so we can spot pathological logging in dev.
|
|
214
|
+
console.warn('[sandbox-factory] non-stringifiable log arg:', err);
|
|
215
|
+
return String(arg);
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/store/constants.ts
CHANGED
|
@@ -147,13 +147,55 @@ export const UI_DEFAULTS = {
|
|
|
147
147
|
// Type Visibility Defaults
|
|
148
148
|
// ============================================================================
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* localStorage keys for the type-visibility toggles. Each maps to a
|
|
152
|
+
* single boolean preference; same persistence pattern as
|
|
153
|
+
* `MERGE_LAYERS_STORAGE_KEY` (`'true'` / `'false'` string, anything
|
|
154
|
+
* else falls back to the semantic default). One key per toggle so a
|
|
155
|
+
* user can clear an individual preference without nuking the rest.
|
|
156
|
+
*/
|
|
157
|
+
export const TYPE_VISIBILITY_STORAGE_KEYS = {
|
|
158
|
+
spaces: 'ifc-lite-ifc-spaces-visible',
|
|
159
|
+
openings: 'ifc-lite-ifc-openings-visible',
|
|
160
|
+
site: 'ifc-lite-ifc-site-visible',
|
|
161
|
+
ifcAnnotations: 'ifc-lite-ifc-annotations-visible',
|
|
162
|
+
} as const;
|
|
163
|
+
|
|
164
|
+
/** Legacy alias — kept until external callers migrate. */
|
|
165
|
+
export const IFC_ANNOTATIONS_STORAGE_KEY = TYPE_VISIBILITY_STORAGE_KEYS.ifcAnnotations;
|
|
166
|
+
|
|
167
|
+
function readPersistedBool(key: string, fallback: boolean): boolean {
|
|
168
|
+
if (typeof window === 'undefined') return fallback;
|
|
169
|
+
try {
|
|
170
|
+
const raw = localStorage.getItem(key);
|
|
171
|
+
if (raw === 'true') return true;
|
|
172
|
+
if (raw === 'false') return false;
|
|
173
|
+
return fallback;
|
|
174
|
+
} catch {
|
|
175
|
+
return fallback;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Semantic defaults applied when no localStorage preference is set.
|
|
180
|
+
// IfcSpace / IfcOpeningElement off — they cover walls and confuse novices
|
|
181
|
+
// on first load. IfcSite + IfcAnnotation/IfcGrid on — both convey
|
|
182
|
+
// design intent users expect to see by default.
|
|
183
|
+
const SEMANTIC_DEFAULTS = {
|
|
184
|
+
spaces: false,
|
|
185
|
+
openings: false,
|
|
186
|
+
site: true,
|
|
187
|
+
ifcAnnotations: true,
|
|
188
|
+
} as const;
|
|
189
|
+
|
|
150
190
|
export const TYPE_VISIBILITY_DEFAULTS = {
|
|
151
|
-
/** IfcSpace visibility
|
|
152
|
-
SPACES:
|
|
153
|
-
/** IfcOpeningElement visibility
|
|
154
|
-
OPENINGS:
|
|
155
|
-
/** IfcSite visibility
|
|
156
|
-
SITE:
|
|
191
|
+
/** IfcSpace visibility — persisted across reloads. */
|
|
192
|
+
SPACES: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.spaces, SEMANTIC_DEFAULTS.spaces),
|
|
193
|
+
/** IfcOpeningElement visibility — persisted across reloads. */
|
|
194
|
+
OPENINGS: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.openings, SEMANTIC_DEFAULTS.openings),
|
|
195
|
+
/** IfcSite visibility — persisted across reloads. */
|
|
196
|
+
SITE: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.site, SEMANTIC_DEFAULTS.site),
|
|
197
|
+
/** IfcAnnotation + IfcGrid visibility — persisted across reloads. */
|
|
198
|
+
IFC_ANNOTATIONS: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.ifcAnnotations, SEMANTIC_DEFAULTS.ifcAnnotations),
|
|
157
199
|
} as const;
|
|
158
200
|
|
|
159
201
|
// ============================================================================
|
package/src/store/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { createDrawing2DSlice, type Drawing2DSlice } from './slices/drawing2DSli
|
|
|
29
29
|
import { createSheetSlice, type SheetSlice } from './slices/sheetSlice.js';
|
|
30
30
|
import { createBcfSlice, type BCFSlice } from './slices/bcfSlice.js';
|
|
31
31
|
import { createIdsSlice, type IDSSlice } from './slices/idsSlice.js';
|
|
32
|
+
import { createExtensionsSlice, type ExtensionsSlice } from './slices/extensionsSlice.js';
|
|
32
33
|
import { createListSlice, type ListSlice } from './slices/listSlice.js';
|
|
33
34
|
import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
|
|
34
35
|
import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
|
|
@@ -140,7 +141,8 @@ export type ViewerState = LoadingSlice &
|
|
|
140
141
|
AddElementSlice &
|
|
141
142
|
SplitToolSlice &
|
|
142
143
|
LevelDisplaySlice &
|
|
143
|
-
PointCloudSlice &
|
|
144
|
+
PointCloudSlice &
|
|
145
|
+
ExtensionsSlice & {
|
|
144
146
|
resetViewerState: () => void;
|
|
145
147
|
};
|
|
146
148
|
|
|
@@ -180,6 +182,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
180
182
|
...createSplitToolSlice(...args),
|
|
181
183
|
...createLevelDisplaySlice(...args),
|
|
182
184
|
...createPointCloudSlice(...args),
|
|
185
|
+
...createExtensionsSlice(...args),
|
|
183
186
|
|
|
184
187
|
// Reset all viewer state when loading new file
|
|
185
188
|
// Note: Does NOT clear models - use clearAllModels() for that
|
|
@@ -204,6 +207,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
204
207
|
spaces: TYPE_VISIBILITY_DEFAULTS.SPACES,
|
|
205
208
|
openings: TYPE_VISIBILITY_DEFAULTS.OPENINGS,
|
|
206
209
|
site: TYPE_VISIBILITY_DEFAULTS.SITE,
|
|
210
|
+
ifcAnnotations: TYPE_VISIBILITY_DEFAULTS.IFC_ANNOTATIONS,
|
|
207
211
|
},
|
|
208
212
|
|
|
209
213
|
// Visibility (multi-model)
|
|
@@ -301,6 +305,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
301
305
|
show3DOverlay: true,
|
|
302
306
|
scale: 100,
|
|
303
307
|
useSymbolicRepresentations: false,
|
|
308
|
+
showIfcAnnotations: true,
|
|
304
309
|
},
|
|
305
310
|
// Graphic overrides (keep presets, reset active and custom)
|
|
306
311
|
activePresetId: 'preset-3d-colors',
|
|
@@ -91,6 +91,13 @@ export interface Drawing2DState {
|
|
|
91
91
|
scale: number;
|
|
92
92
|
/** Use authored symbolic representations (Plan/Annotation) when available instead of section cut */
|
|
93
93
|
useSymbolicRepresentations: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether to overlay IfcAnnotation curves, text, and fills on the 2D
|
|
96
|
+
* section view. Filtered to annotations whose world position falls
|
|
97
|
+
* inside the section's view-range on the cut axis (issue #812 follow-up
|
|
98
|
+
* to the IfcAnnotation text feature).
|
|
99
|
+
*/
|
|
100
|
+
showIfcAnnotations: boolean;
|
|
94
101
|
};
|
|
95
102
|
/** Available graphic override presets */
|
|
96
103
|
graphicOverridePresets: GraphicOverridePreset[];
|
|
@@ -236,6 +243,7 @@ const getDefaultDisplayOptions = (): Drawing2DState['drawing2DDisplayOptions'] =
|
|
|
236
243
|
show3DOverlay: true, // Show 3D overlay by default
|
|
237
244
|
scale: 100, // 1:100 default
|
|
238
245
|
useSymbolicRepresentations: false, // Default to section cut (Body geometry)
|
|
246
|
+
showIfcAnnotations: true, // Mirror the 3D Class Visibility default
|
|
239
247
|
});
|
|
240
248
|
|
|
241
249
|
const getDefaultState = (): Drawing2DState => ({
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
* Extensions panel visibility slice.
|
|
7
|
+
*
|
|
8
|
+
* The extension system's actual state lives in the host service
|
|
9
|
+
* (services/extensions/host.ts) — installed bundles, audit log, slot
|
|
10
|
+
* contributions. The store only owns the UI-toggle state so panel
|
|
11
|
+
* visibility behaves like every other dock panel (IDS, BCF, Lens,
|
|
12
|
+
* Lists, Script).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { StateCreator } from 'zustand';
|
|
16
|
+
|
|
17
|
+
export type ExtensionsTabView = 'installed' | 'ideas' | 'audit' | 'repair' | 'privacy';
|
|
18
|
+
|
|
19
|
+
export interface ExtensionsSlice {
|
|
20
|
+
extensionsPanelVisible: boolean;
|
|
21
|
+
setExtensionsPanelVisible: (visible: boolean) => void;
|
|
22
|
+
toggleExtensionsPanel: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Bytes of an authored bundle waiting for the user to review. The
|
|
25
|
+
* chat-side authoring loop sets this on success; the Extensions
|
|
26
|
+
* panel picks it up on mount, routes through CapabilityReview, and
|
|
27
|
+
* clears the slot after install or cancel.
|
|
28
|
+
*/
|
|
29
|
+
pendingAuthoredBundle: Uint8Array | null;
|
|
30
|
+
setPendingAuthoredBundle: (bytes: Uint8Array | null) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Which tab the Extensions panel should show. Set by deep-link
|
|
33
|
+
* entry points (Command Palette "Author an extension…", chat
|
|
34
|
+
* routing after a successful authoring loop) so the panel mounts
|
|
35
|
+
* straight into the right surface.
|
|
36
|
+
*/
|
|
37
|
+
extensionsRequestedView: ExtensionsTabView | null;
|
|
38
|
+
setExtensionsRequestedView: (view: ExtensionsTabView | null) => void;
|
|
39
|
+
/**
|
|
40
|
+
* When true, the IdeasPanel should open the Plan Card in
|
|
41
|
+
* empty-plan mode on mount. Consumed once then cleared.
|
|
42
|
+
*/
|
|
43
|
+
ideasOpenEmptyPlan: boolean;
|
|
44
|
+
setIdeasOpenEmptyPlan: (open: boolean) => void;
|
|
45
|
+
/**
|
|
46
|
+
* When true, the FlavorDialog should auto-open. Consumed once
|
|
47
|
+
* then cleared by the dialog wrapper.
|
|
48
|
+
*/
|
|
49
|
+
flavorDialogRequested: boolean;
|
|
50
|
+
setFlavorDialogRequested: (open: boolean) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Post-authoring "install" handoff. After the chat finishes an
|
|
53
|
+
* authoring turn, it sets this so the chat panel can render a
|
|
54
|
+
* prominent inline CTA — the user no longer has to hunt for a
|
|
55
|
+
* Promote button in another panel.
|
|
56
|
+
*
|
|
57
|
+
* kind 'bundle' — a full `.iflx` bundle was synthesised and is
|
|
58
|
+
* waiting in `pendingAuthoredBundle`; the CTA
|
|
59
|
+
* routes to the Extensions panel review.
|
|
60
|
+
* kind 'script' — the assistant wrote a one-shot script into the
|
|
61
|
+
* editor; the CTA opens PromoteToolDialog.
|
|
62
|
+
*
|
|
63
|
+
* Cleared when the user acts on it, dismisses it, or starts a new
|
|
64
|
+
* chat.
|
|
65
|
+
*/
|
|
66
|
+
chatToolReady: { kind: 'bundle' | 'script'; name: string } | null;
|
|
67
|
+
setChatToolReady: (v: { kind: 'bundle' | 'script'; name: string } | null) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const createExtensionsSlice: StateCreator<
|
|
71
|
+
ExtensionsSlice,
|
|
72
|
+
[],
|
|
73
|
+
[],
|
|
74
|
+
ExtensionsSlice
|
|
75
|
+
> = (set) => ({
|
|
76
|
+
extensionsPanelVisible: false,
|
|
77
|
+
setExtensionsPanelVisible: (extensionsPanelVisible) => set({ extensionsPanelVisible }),
|
|
78
|
+
toggleExtensionsPanel: () =>
|
|
79
|
+
set((state) => ({ extensionsPanelVisible: !state.extensionsPanelVisible })),
|
|
80
|
+
pendingAuthoredBundle: null,
|
|
81
|
+
setPendingAuthoredBundle: (pendingAuthoredBundle) => set({ pendingAuthoredBundle }),
|
|
82
|
+
extensionsRequestedView: null,
|
|
83
|
+
setExtensionsRequestedView: (extensionsRequestedView) => set({ extensionsRequestedView }),
|
|
84
|
+
ideasOpenEmptyPlan: false,
|
|
85
|
+
setIdeasOpenEmptyPlan: (ideasOpenEmptyPlan) => set({ ideasOpenEmptyPlan }),
|
|
86
|
+
flavorDialogRequested: false,
|
|
87
|
+
setFlavorDialogRequested: (flavorDialogRequested) => set({ flavorDialogRequested }),
|
|
88
|
+
chatToolReady: null,
|
|
89
|
+
setChatToolReady: (chatToolReady) => set({ chatToolReady }),
|
|
90
|
+
});
|