@iblai/iblai-js 2.3.2 → 2.3.4

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.
@@ -440,15 +440,15 @@ const TAURI_COMMANDS = {
440
440
  // Contract for the Cowork manager, which installs and sets up GhostOS
441
441
  // (https://github.com/ghostwright/ghost-os) on-device via the Tauri host. This
442
442
  // mirrors the Local Models (Ollama) contract above: the web layer ships the
443
- // presentational card + types, while the Tauri host (e.g. the ghost-os desktop
444
- // app) implements the native install/setup/status commands.
443
+ // presentational card + types, while the Tauri host implements the native
444
+ // install/status commands and owns the driver process.
445
445
  // ============================================================================
446
- /** GitHub repository the Cowork manager installs and sets up. */
447
- const GHOST_OS_REPO_URL = 'https://github.com/ghostwright/ghost-os';
446
+ /** Upstream project the Cowork driver comes from. */
447
+ const CUA_DRIVER_REPO_URL = 'https://github.com/trycua/cua';
448
448
  /**
449
449
  * Initial state for the Cowork install flow.
450
450
  */
451
- const initialGhostOsInstallState = {
451
+ const initialCuaDriverInstallState = {
452
452
  status: 'idle',
453
453
  progress: 0,
454
454
  message: '',
@@ -456,24 +456,27 @@ const initialGhostOsInstallState = {
456
456
  lastUpdated: new Date().toISOString(),
457
457
  };
458
458
  /**
459
- * Tauri event names emitted by the host during GhostOS install/setup.
459
+ * Tauri event names emitted by the host during Cua Driver install.
460
460
  */
461
- const GHOST_OS_TAURI_EVENTS = {
462
- INSTALL_PROGRESS: 'ghost-os:install-progress',
463
- INSTALLATION_LOG: 'ghost-os:installation-log',
464
- STATUS: 'ghost-os:status',
461
+ const CUA_DRIVER_TAURI_EVENTS = {
462
+ INSTALL_PROGRESS: 'cua-driver:install-progress',
463
+ INSTALLATION_LOG: 'cua-driver:installation-log',
464
+ STATUS: 'cua-driver:status',
465
465
  };
466
466
  /**
467
- * Tauri command names the host implements for GhostOS install/setup. The web
468
- * layer invokes these; the native side performs the clone + setup.
467
+ * Tauri command names the host implements for the Cua Driver. The web layer
468
+ * invokes these; the native side downloads the pinned per-platform binary and
469
+ * owns the MCP process.
469
470
  */
470
- const GHOST_OS_TAURI_COMMANDS = {
471
- /** Install and set up GhostOS (clone repo + run setup). */
472
- INSTALL_GHOST_OS: 'install_ghost_os',
473
- /** Stop the running GhostOS manager. */
474
- STOP_GHOST_OS: 'stop_ghost_os',
475
- /** Report current {@link GhostOsStatus}. */
476
- CHECK_GHOST_OS_STATUS: 'check_ghost_os_status',
471
+ const CUA_DRIVER_TAURI_COMMANDS = {
472
+ /** Download + install the pinned Cua Driver binary. */
473
+ INSTALL: 'install_cua_driver',
474
+ /** Report current {@link CuaDriverStatus}. */
475
+ CHECK_STATUS: 'check_cua_driver_status',
476
+ /** Whether Cowork can run on this desktop session, and why not if it can't. */
477
+ SUPPORT: 'cua_driver_support',
478
+ /** Stop the driver's MCP process. */
479
+ STOP: 'cua_driver_stop',
477
480
  };
478
481
  /**
479
482
  * Tauri command strings exposed by `tauri-plugin-macos-permissions`
@@ -560,16 +563,26 @@ function useTauri() {
560
563
  };
561
564
  }
562
565
 
563
- const IS_SERVER = typeof window === 'undefined';
566
+ // Evaluated per call, not once at module load. A module-level constant is
567
+ // captured before any test can influence it, which left every server-side guard
568
+ // below permanently unreachable — and therefore unverifiable — in a jsdom runner.
569
+ const isServer = () => typeof window === 'undefined';
564
570
  /**
565
571
  * A simplified useLocalStorage hook for persisting state to localStorage
566
572
  * with SSR safety.
567
573
  */
568
574
  function useLocalStorage(key, initialValue) {
575
+ // The initial value is captured, not tracked. Depending on its identity meant
576
+ // an inline literal — `useLocalStorage('k', { n: 0 })`, the ordinary React
577
+ // idiom — gave `getInitialValue` a new identity every render, which re-ran the
578
+ // key-change effect below, which called `setStoredValue` with a fresh object,
579
+ // which re-rendered… an infinite loop that ends in an out-of-memory crash.
580
+ // Existing callers only escaped it by happening to pass module constants.
581
+ const initialRef = useRef(initialValue);
569
582
  // Get the initial value
570
583
  const getInitialValue = useCallback(() => {
571
- const initial = initialValue instanceof Function ? initialValue() : initialValue;
572
- if (IS_SERVER) {
584
+ const initial = initialRef.current instanceof Function ? initialRef.current() : initialRef.current;
585
+ if (isServer()) {
573
586
  return initial;
574
587
  }
575
588
  try {
@@ -583,42 +596,59 @@ function useLocalStorage(key, initialValue) {
583
596
  console.warn(`Error reading localStorage key "${key}":`, error);
584
597
  return initial;
585
598
  }
586
- }, [key, initialValue]);
599
+ }, [key]);
587
600
  const [storedValue, setStoredValue] = useState(getInitialValue);
588
601
  // Update stored value when key changes
589
602
  useEffect(() => {
590
603
  setStoredValue(getInitialValue());
591
604
  }, [key, getInitialValue]);
592
- // Persist to localStorage
605
+ // Latest value, readable synchronously. Two writes in the same tick would
606
+ // otherwise both resolve their updater against the render's snapshot, so the
607
+ // second silently reverts the first (a status change followed by a log append
608
+ // losing the status, for instance).
609
+ const storedRef = useRef(storedValue);
610
+ storedRef.current = storedValue;
611
+ // Persist to localStorage.
612
+ //
613
+ // Depends on `key` ALONE, deliberately. This used to close over `storedValue`,
614
+ // which gave the setter a new identity on every value change — and anything
615
+ // built on it (a `useCallback` logger, say) churned too. An effect listing
616
+ // those in its dependencies then re-ran on every state change; where such an
617
+ // effect subscribes asynchronously, its cleanup runs before the subscription
618
+ // resolves and unregisters nothing, so each pass leaks another live listener
619
+ // and one event fans out into two, four, eight… until the webview locks up.
593
620
  const setValue = useCallback((value) => {
594
- if (IS_SERVER) {
621
+ if (isServer()) {
595
622
  console.warn(`Tried setting localStorage key "${key}" even though environment is not a client`);
596
623
  return;
597
624
  }
598
625
  try {
599
- const valueToStore = value instanceof Function ? value(storedValue) : value;
626
+ const valueToStore = value instanceof Function ? value(storedRef.current) : value;
627
+ storedRef.current = valueToStore;
600
628
  setStoredValue(valueToStore);
601
629
  window.localStorage.setItem(key, JSON.stringify(valueToStore));
602
630
  }
603
631
  catch (error) {
604
632
  console.warn(`Error setting localStorage key "${key}":`, error);
605
633
  }
606
- }, [key, storedValue]);
634
+ }, [key]);
607
635
  // Remove from localStorage
608
636
  const removeValue = useCallback(() => {
609
- if (IS_SERVER) {
637
+ if (isServer()) {
610
638
  console.warn(`Tried removing localStorage key "${key}" even though environment is not a client`);
611
639
  return;
612
640
  }
613
641
  try {
614
- const initial = initialValue instanceof Function ? initialValue() : initialValue;
642
+ const initial = initialRef.current instanceof Function ? initialRef.current() : initialRef.current;
615
643
  window.localStorage.removeItem(key);
616
644
  setStoredValue(initial);
617
645
  }
618
646
  catch (error) {
619
647
  console.warn(`Error removing localStorage key "${key}":`, error);
620
648
  }
621
- }, [key, initialValue]);
649
+ // `[key]` alone, for the same reason as getInitialValue above — otherwise an
650
+ // inline initial value churns this callback's identity on every render.
651
+ }, [key]);
622
652
  return [storedValue, setValue, removeValue];
623
653
  }
624
654
 
@@ -1070,79 +1100,148 @@ function useModelDownload() {
1070
1100
  };
1071
1101
  }
1072
1102
 
1073
- const LOCAL_STORAGE_KEY = 'ghost_os_install_state';
1103
+ const LOCAL_STORAGE_KEY = 'cua_driver_install_state';
1074
1104
  /**
1075
- * Manage installing & setting up GhostOS (https://github.com/ghostwright/ghost-os)
1076
- * via the Tauri host. This is the self-contained backing hook for the System
1077
- * Control card and deliberately mirrors `useModelDownload`:
1105
+ * Manage installing the Cua Driver (https://github.com/trycua/cua) via the Tauri
1106
+ * host. This is the self-contained backing hook for the Cowork card and
1107
+ * deliberately mirrors `useModelDownload`:
1078
1108
  *
1079
- * - checks GhostOS install/run status through the host
1080
- * - installs/sets up GhostOS with real-time progress + logs
1109
+ * - checks driver install status AND whether this desktop session is supported
1110
+ * - downloads/installs the pinned driver with real-time progress + logs
1081
1111
  * - persists state across app restarts
1082
1112
  *
1083
1113
  * Outside the desktop (Tauri) app every action is a no-op and `isAvailable` is
1084
- * false, so the card hides itself.
1114
+ * false, so the card hides itself. Inside it, `isSupported` is what gates the
1115
+ * Cowork toggle — the driver has no proven behaviour on every Linux session.
1116
+ *
1117
+ * **Consent is the host application's job, not this hook's.** Enabling Cowork
1118
+ * hands an LLM synthetic input, screen capture, and — through the driver's
1119
+ * `--grant existing-profile` — the user's logged-in browser sessions. The driver
1120
+ * renders no consent UI of its own, and neither does the `InsideButtons` toggle
1121
+ * in this package. A host must explain that and get consent before calling
1122
+ * `requestDriverPermissions()`.
1085
1123
  */
1086
- function useGhostOs() {
1124
+ function useCuaDriver() {
1087
1125
  const { isAvailable, invoke, listen } = useTauri();
1088
- const [state, setState] = useLocalStorage(LOCAL_STORAGE_KEY, initialGhostOsInstallState);
1126
+ const [state, setState] = useLocalStorage(LOCAL_STORAGE_KEY, initialCuaDriverInstallState);
1089
1127
  const [status, setStatus] = useState(null);
1090
- // macOS Accessibility permission, which GhostOS needs to control the device.
1091
- // null = unknown/not-yet-checked or unsupported (non-macOS / plugin absent).
1128
+ // The two macOS grants the driver needs: Accessibility to post synthetic input,
1129
+ // Screen Recording because get_window_state always captures alongside the tree.
1130
+ //
1131
+ // `null` is a third state and a meaningful one: the commands exist only on
1132
+ // macOS, so elsewhere the invoke throws and the answer is "not applicable"
1133
+ // rather than "denied". Callers must branch on `=== false`, never on falsy —
1134
+ // treating null as denied would make Cowork unusable on Linux and Windows.
1092
1135
  const [accessibilityPermission, setAccessibilityPermission] = useState(null);
1093
- const unlistenRefs = useRef([]);
1136
+ const [screenRecordingPermission, setScreenRecordingPermission] = useState(null);
1094
1137
  const hasCheckedStatus = useRef(false);
1095
1138
  /**
1096
- * Refresh the macOS Accessibility permission via tauri-plugin-macos-permissions.
1097
- * Independent of the GhostOS status check so a missing host command for one
1098
- * doesn't suppress the other. Stays null where unsupported.
1139
+ * Read one permission. Checking never prompts (`AXIsProcessTrusted` /
1140
+ * `CGPreflightScreenCaptureAccess`), which is what makes it safe to do on mount.
1099
1141
  */
1100
- const refreshAccessibilityPermission = useCallback(async () => {
1101
- if (!isAvailable)
1102
- return;
1142
+ const checkPermission = useCallback(async (command) => {
1103
1143
  try {
1104
- const granted = await invoke(MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY);
1105
- setAccessibilityPermission(granted);
1144
+ return await invoke(command);
1106
1145
  }
1107
1146
  catch (error) {
1108
- // Plugin not registered or non-macOS host — leave permission unknown.
1109
- console.warn('[useGhostOs] Accessibility permission check unavailable:', error);
1110
- setAccessibilityPermission(null);
1147
+ // Plugin not registered or non-macOS host — not applicable, not denied.
1148
+ console.warn(`[useCuaDriver] ${command} unavailable:`, error);
1149
+ return null;
1111
1150
  }
1112
- }, [isAvailable, invoke]);
1151
+ }, [invoke]);
1113
1152
  /**
1114
- * Open the Accessibility pane / prompt so the user can grant permission, then
1115
- * re-check (the grant happens in System Settings, so we refresh after).
1153
+ * Refresh both permissions. Independent of the driver status check so a missing
1154
+ * host command for one doesn't suppress the other.
1116
1155
  */
1117
- const requestAccessibilityPermission = useCallback(async () => {
1156
+ const refreshPermissions = useCallback(async () => {
1118
1157
  if (!isAvailable)
1119
1158
  return;
1120
- try {
1121
- await invoke(MACOS_PERMISSIONS_COMMANDS.REQUEST_ACCESSIBILITY);
1122
- }
1123
- catch (error) {
1124
- console.error('[useGhostOs] Failed to request accessibility permission:', error);
1159
+ const [accessibility, screenRecording] = await Promise.all([
1160
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY),
1161
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_SCREEN_RECORDING),
1162
+ ]);
1163
+ setAccessibilityPermission(accessibility);
1164
+ setScreenRecordingPermission(screenRecording);
1165
+ }, [isAvailable, checkPermission]);
1166
+ /**
1167
+ * Ask for whichever grants are missing, then report what is actually held.
1168
+ *
1169
+ * **Prompts the user, so never call this from an effect** — it belongs to an
1170
+ * explicit action (switching Cowork on), not to mounting a component. The OS
1171
+ * prompt is not a consent step: it asks for a macOS grant, not for permission
1172
+ * to read the screen and act as the user. Explain that first (see the hook
1173
+ * docblock) — this package does not.
1174
+ *
1175
+ * Returns the post-request values rather than leaving the caller to read state:
1176
+ * a `setState` in this tick is not visible to the caller that awaited us, and
1177
+ * that caller has to decide right now whether to proceed.
1178
+ *
1179
+ * Note the values can still be `false` immediately after a successful grant —
1180
+ * `CGRequestScreenCaptureAccess` returns before the grant takes effect, which
1181
+ * on macOS usually needs an app restart. Copy shown to the user must say so.
1182
+ */
1183
+ const requestDriverPermissions = useCallback(async () => {
1184
+ if (!isAvailable)
1185
+ return { accessibility: null, screenRecording: null };
1186
+ const pairs = [
1187
+ [
1188
+ MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY,
1189
+ MACOS_PERMISSIONS_COMMANDS.REQUEST_ACCESSIBILITY,
1190
+ ],
1191
+ [
1192
+ MACOS_PERMISSIONS_COMMANDS.CHECK_SCREEN_RECORDING,
1193
+ MACOS_PERMISSIONS_COMMANDS.REQUEST_SCREEN_RECORDING,
1194
+ ],
1195
+ ];
1196
+ for (const [check, request] of pairs) {
1197
+ // Only prompt for what is actually missing — re-requesting a held grant
1198
+ // just reopens System Settings for no reason.
1199
+ if ((await checkPermission(check)) !== false)
1200
+ continue;
1201
+ try {
1202
+ await invoke(request);
1203
+ }
1204
+ catch (error) {
1205
+ console.error(`[useCuaDriver] ${request} failed:`, error);
1206
+ }
1125
1207
  }
1126
- await refreshAccessibilityPermission();
1127
- }, [isAvailable, invoke, refreshAccessibilityPermission]);
1128
- const addLog = useCallback((log) => {
1208
+ const [accessibility, screenRecording] = await Promise.all([
1209
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY),
1210
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_SCREEN_RECORDING),
1211
+ ]);
1212
+ setAccessibilityPermission(accessibility);
1213
+ setScreenRecordingPermission(screenRecording);
1214
+ return { accessibility, screenRecording };
1215
+ }, [isAvailable, invoke, checkPermission]);
1216
+ /**
1217
+ * Append a log line, optionally in the SAME update as other state changes.
1218
+ *
1219
+ * The `patch` argument is not a convenience: `setState` here is backed by
1220
+ * `useLocalStorage`, whose write + re-read means a log appended straight after
1221
+ * a status change can be computed from the pre-change snapshot and silently
1222
+ * revert it. That is how a failed install used to end up displayed as
1223
+ * "completed" with the error only visible down in the log pane. Anything that
1224
+ * sets status AND logs must do both in one update.
1225
+ */
1226
+ const addLog = useCallback((log, patch) => {
1129
1227
  setState((prev) => ({
1130
1228
  ...prev,
1229
+ ...patch,
1131
1230
  logs: [...prev.logs.slice(-99), log],
1132
1231
  lastUpdated: new Date().toISOString(),
1133
1232
  }));
1134
1233
  }, [setState]);
1135
1234
  /**
1136
- * Query GhostOS status from the host.
1235
+ * Query driver install status + session support from the host.
1137
1236
  */
1138
1237
  const checkStatus = useCallback(async () => {
1139
1238
  if (!isAvailable)
1140
1239
  return;
1141
- // Refresh the accessibility permission alongside install status (independent).
1142
- refreshAccessibilityPermission();
1240
+ // Refresh both permissions alongside install status (independent, no prompt).
1241
+ refreshPermissions();
1143
1242
  try {
1144
1243
  setState((prev) => ({ ...prev, status: 'checking' }));
1145
- const result = await invoke(GHOST_OS_TAURI_COMMANDS.CHECK_GHOST_OS_STATUS);
1244
+ const result = await invoke(CUA_DRIVER_TAURI_COMMANDS.CHECK_STATUS);
1146
1245
  setStatus(result);
1147
1246
  setState((prev) => ({
1148
1247
  ...prev,
@@ -1152,13 +1251,13 @@ function useGhostOs() {
1152
1251
  }));
1153
1252
  }
1154
1253
  catch (error) {
1155
- console.error('[useGhostOs] Failed to check status:', error);
1254
+ console.error('[useCuaDriver] Failed to check status:', error);
1156
1255
  setState((prev) => ({ ...prev, status: 'idle' }));
1157
1256
  }
1158
- }, [isAvailable, invoke, setState, refreshAccessibilityPermission]);
1257
+ }, [isAvailable, invoke, setState, refreshPermissions]);
1159
1258
  /**
1160
- * Install and set up GhostOS. The host clones the repo + runs setup and emits
1161
- * progress/log/status events along the way.
1259
+ * Download + install the pinned driver. The host emits progress/log events
1260
+ * along the way, and refuses up front on an unsupported desktop session.
1162
1261
  */
1163
1262
  const install = useCallback(async () => {
1164
1263
  if (!isAvailable)
@@ -1177,64 +1276,73 @@ function useGhostOs() {
1177
1276
  level: 'info',
1178
1277
  message: 'Starting setup…',
1179
1278
  });
1180
- const result = await invoke(GHOST_OS_TAURI_COMMANDS.INSTALL_GHOST_OS);
1279
+ const result = await invoke(CUA_DRIVER_TAURI_COMMANDS.INSTALL);
1181
1280
  addLog({
1182
1281
  timestamp: new Date().toISOString(),
1183
1282
  level: 'info',
1184
1283
  message: typeof result === 'string' && result ? result : 'Setup complete',
1185
- });
1186
- setState((prev) => ({
1187
- ...prev,
1284
+ }, {
1188
1285
  status: 'completed',
1189
1286
  progress: 100,
1190
1287
  message: 'Your assistant is ready',
1191
- lastUpdated: new Date().toISOString(),
1192
- }));
1288
+ });
1193
1289
  await checkStatus();
1194
1290
  }
1195
1291
  catch (error) {
1196
1292
  const message = error instanceof Error ? error.message : String(error);
1197
- setState((prev) => ({
1198
- ...prev,
1199
- status: 'error',
1200
- error: message,
1201
- message: 'Setup didn’t finish',
1202
- lastUpdated: new Date().toISOString(),
1203
- }));
1204
1293
  addLog({
1205
1294
  timestamp: new Date().toISOString(),
1206
1295
  level: 'error',
1207
1296
  message: `Install failed: ${message}`,
1208
- });
1297
+ }, { status: 'error', error: message, message: 'Setup didn’t finish' });
1209
1298
  }
1210
1299
  }, [isAvailable, invoke, addLog, checkStatus, setState]);
1211
1300
  /**
1212
- * Stop the running GhostOS manager.
1301
+ * Stop the driver's MCP process. The host announces the exit, which makes the
1302
+ * chat layer drop its cached MCP session and reconnect on the next turn.
1213
1303
  */
1214
1304
  const stop = useCallback(async () => {
1215
1305
  if (!isAvailable)
1216
1306
  return;
1217
1307
  try {
1218
- await invoke(GHOST_OS_TAURI_COMMANDS.STOP_GHOST_OS);
1308
+ await invoke(CUA_DRIVER_TAURI_COMMANDS.STOP);
1219
1309
  await checkStatus();
1220
1310
  }
1221
1311
  catch (error) {
1222
- console.error('[useGhostOs] Failed to stop GhostOS:', error);
1312
+ console.error('[useCuaDriver] Failed to stop the driver:', error);
1223
1313
  }
1224
1314
  }, [isAvailable, invoke, checkStatus]);
1225
1315
  /**
1226
1316
  * Reset the install state (for retrying after an error).
1227
1317
  */
1228
1318
  const resetState = useCallback(() => {
1229
- setState(initialGhostOsInstallState);
1319
+ setState(initialCuaDriverInstallState);
1230
1320
  }, [setState]);
1321
+ // Latest handlers, read at event time. Keeping them OUT of the effect's
1322
+ // dependencies is the point: anything derived from `setState` can change
1323
+ // identity as state moves, and re-subscribing per state change is what turned
1324
+ // a handful of install events into a runaway cascade.
1325
+ const handlersRef = useRef({ addLog, setState });
1326
+ handlersRef.current = { addLog, setState };
1231
1327
  // Wire up host event listeners (progress / logs / status).
1232
1328
  useEffect(() => {
1233
1329
  if (!isAvailable)
1234
1330
  return;
1331
+ // `listen()` is async, so a teardown can land before the subscription
1332
+ // resolves. Registering the handle unconditionally would leave a live
1333
+ // listener behind with nothing holding its unsubscribe — the leak that made
1334
+ // one install log event fan out and freeze the webview for good.
1335
+ let cancelled = false;
1336
+ const handles = [];
1337
+ const track = (unlisten) => {
1338
+ if (cancelled)
1339
+ unlisten();
1340
+ else
1341
+ handles.push(unlisten);
1342
+ };
1235
1343
  const setupListeners = async () => {
1236
1344
  try {
1237
- const unlistenProgress = await listen(GHOST_OS_TAURI_EVENTS.INSTALL_PROGRESS, (payload) => {
1345
+ track(await listen(CUA_DRIVER_TAURI_EVENTS.INSTALL_PROGRESS, (payload) => {
1238
1346
  const nextStatus = payload.status === 'completed'
1239
1347
  ? 'completed'
1240
1348
  : payload.status === 'cancelled'
@@ -1242,30 +1350,28 @@ function useGhostOs() {
1242
1350
  : payload.status === 'error'
1243
1351
  ? 'error'
1244
1352
  : 'installing';
1245
- setState((prev) => ({
1353
+ handlersRef.current.setState((prev) => ({
1246
1354
  ...prev,
1247
1355
  status: nextStatus,
1248
1356
  progress: payload.percentage,
1249
1357
  message: payload.message,
1250
1358
  lastUpdated: new Date().toISOString(),
1251
1359
  }));
1252
- });
1253
- unlistenRefs.current.push(unlistenProgress);
1254
- const unlistenLogs = await listen(GHOST_OS_TAURI_EVENTS.INSTALLATION_LOG, addLog);
1255
- unlistenRefs.current.push(unlistenLogs);
1256
- const unlistenStatus = await listen(GHOST_OS_TAURI_EVENTS.STATUS, setStatus);
1257
- unlistenRefs.current.push(unlistenStatus);
1360
+ }));
1361
+ track(await listen(CUA_DRIVER_TAURI_EVENTS.INSTALLATION_LOG, (log) => handlersRef.current.addLog(log)));
1362
+ track(await listen(CUA_DRIVER_TAURI_EVENTS.STATUS, setStatus));
1258
1363
  }
1259
1364
  catch (error) {
1260
- console.error('[useGhostOs] Failed to setup event listeners:', error);
1365
+ console.error('[useCuaDriver] Failed to setup event listeners:', error);
1261
1366
  }
1262
1367
  };
1263
1368
  setupListeners();
1264
1369
  return () => {
1265
- unlistenRefs.current.forEach((unlisten) => unlisten());
1266
- unlistenRefs.current = [];
1370
+ cancelled = true;
1371
+ handles.splice(0).forEach((unlisten) => unlisten());
1267
1372
  };
1268
- }, [isAvailable, listen, setState, addLog]);
1373
+ // Only re-subscribe if the host itself changes — never on state.
1374
+ }, [isAvailable, listen]);
1269
1375
  // Check status once on mount (in the desktop app).
1270
1376
  useEffect(() => {
1271
1377
  if (!isAvailable || hasCheckedStatus.current)
@@ -1275,16 +1381,28 @@ function useGhostOs() {
1275
1381
  }, [isAvailable, checkStatus]);
1276
1382
  // Only expose the feature as available inside the desktop (Tauri) app.
1277
1383
  const finalIsAvailable = isAvailable && isTauriApp();
1384
+ // Deliberately `=== true`, not `?? true`: until the first status check lands
1385
+ // we do not know whether this desktop session is one the driver can drive, and
1386
+ // offering Cowork optimistically would show the toggle to (say) a KDE user for
1387
+ // a beat before snatching it away. One IPC round-trip on mount is cheaper than
1388
+ // that flicker, and errs towards not promising what we cannot deliver.
1389
+ const isSupported = (status === null || status === void 0 ? void 0 : status.supported) === true;
1390
+ const unsupportedReason = (status === null || status === void 0 ? void 0 : status.supported) === false ? status.reason : undefined;
1278
1391
  return {
1279
1392
  isAvailable: finalIsAvailable,
1393
+ /** Session support — gate the Cowork toggle on this, not on `isAvailable`. */
1394
+ isSupported,
1395
+ /** Machine-readable code the UI maps to a translated explanation. */
1396
+ unsupportedReason,
1280
1397
  state,
1281
1398
  status,
1282
1399
  accessibilityPermission,
1400
+ screenRecordingPermission,
1283
1401
  install,
1284
1402
  stop,
1285
1403
  checkStatus,
1286
1404
  resetState,
1287
- requestAccessibilityPermission,
1405
+ requestDriverPermissions,
1288
1406
  };
1289
1407
  }
1290
1408
 
@@ -61217,7 +61335,11 @@ var chatInputFormInsideButtons$3 = {
61217
61335
  cowork: "Cowork",
61218
61336
  coworkNeedsBackend: "Enable a large local model or sign in to a workspace to use Cowork.",
61219
61337
  coworkModelTooSmall: "Your local model is too small for Cowork. Pick a model of at least 14GB in Local Models.",
61220
- moreOptions: "More options"
61338
+ moreOptions: "More options",
61339
+ coworkUnsupportedKde: "Cowork isn’t supported on KDE Plasma yet.",
61340
+ coworkUnsupportedGnomeHelper: "Cowork on GNOME needs the WinRects helper installed, then a session restart.",
61341
+ coworkUnsupportedOs: "Cowork isn’t supported on this operating system.",
61342
+ coworkUnsupportedSession: "Cowork isn’t supported on this desktop session."
61221
61343
  };
61222
61344
  var chatInputFormMemoryMenu$3 = {
61223
61345
  title: "Your Memory",
@@ -67545,7 +67667,11 @@ var chatInputFormInsideButtons$2 = {
67545
67667
  cowork: "Cowork",
67546
67668
  coworkNeedsBackend: "Activez un grand modèle local ou connectez-vous à un espace de travail pour utiliser Cowork.",
67547
67669
  coworkModelTooSmall: "Votre modèle local est trop petit pour Cowork. Choisissez un modèle d’au moins 14 Go dans Modèles Locaux.",
67548
- moreOptions: "Plus d'options"
67670
+ moreOptions: "Plus d'options",
67671
+ coworkUnsupportedKde: "Cowork n’est pas encore pris en charge sur KDE Plasma.",
67672
+ coworkUnsupportedGnomeHelper: "Cowork sur GNOME nécessite l’extension WinRects, puis un redémarrage de la session.",
67673
+ coworkUnsupportedOs: "Cowork n’est pas pris en charge sur ce système d’exploitation.",
67674
+ coworkUnsupportedSession: "Cowork n’est pas pris en charge sur cette session de bureau."
67549
67675
  };
67550
67676
  var chatInputFormMemoryMenu$2 = {
67551
67677
  title: "Votre mémoire",
@@ -73761,7 +73887,11 @@ var chatInputFormInsideButtons$1 = {
73761
73887
  cowork: "Cowork",
73762
73888
  coworkNeedsBackend: "Activa un modelo local grande o inicia sesión en un espacio de trabajo para usar Cowork.",
73763
73889
  coworkModelTooSmall: "Tu modelo local es demasiado pequeño para Cowork. Elige un modelo de al menos 14 GB en Modelos Locales.",
73764
- moreOptions: "Más opciones"
73890
+ moreOptions: "Más opciones",
73891
+ coworkUnsupportedKde: "Cowork aún no es compatible con KDE Plasma.",
73892
+ coworkUnsupportedGnomeHelper: "Cowork en GNOME necesita la extensión WinRects instalada y reiniciar la sesión.",
73893
+ coworkUnsupportedOs: "Cowork no es compatible con este sistema operativo.",
73894
+ coworkUnsupportedSession: "Cowork no es compatible con esta sesión de escritorio."
73765
73895
  };
73766
73896
  var chatInputFormMemoryMenu$1 = {
73767
73897
  title: "Tu memoria",
@@ -79977,7 +80107,11 @@ var chatInputFormInsideButtons = {
79977
80107
  cowork: "Cowork",
79978
80108
  coworkNeedsBackend: "启用大型本地模型或登录工作区即可使用 Cowork。",
79979
80109
  coworkModelTooSmall: "您的本地模型太小,无法用于 Cowork。请在“本地模型”中选择至少 14GB 的模型。",
79980
- moreOptions: "更多选项"
80110
+ moreOptions: "更多选项",
80111
+ coworkUnsupportedKde: "Cowork 尚不支持 KDE Plasma。",
80112
+ coworkUnsupportedGnomeHelper: "在 GNOME 上使用 Cowork 需要安装 WinRects 扩展,然后重启会话。",
80113
+ coworkUnsupportedOs: "Cowork 不支持此操作系统。",
80114
+ coworkUnsupportedSession: "Cowork 不支持此桌面会话。"
79981
80115
  };
79982
80116
  var chatInputFormMemoryMenu = {
79983
80117
  title: "您的记忆",
@@ -170006,7 +170140,7 @@ var joinTextblockForward = () => ({ state, dispatch }) => {
170006
170140
  };
170007
170141
 
170008
170142
  // src/utilities/isMacOS.ts
170009
- function isMacOS$1() {
170143
+ function isMacOS() {
170010
170144
  return typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
170011
170145
  }
170012
170146
 
@@ -170032,7 +170166,7 @@ function normalizeKeyName(name) {
170032
170166
  } else if (/^s(hift)?$/i.test(mod)) {
170033
170167
  shift = true;
170034
170168
  } else if (/^mod$/i.test(mod)) {
170035
- if (isiOS() || isMacOS$1()) {
170169
+ if (isiOS() || isMacOS()) {
170036
170170
  meta = true;
170037
170171
  } else {
170038
170172
  ctrl = true;
@@ -173009,7 +173143,7 @@ var Keymap = Extension.create({
173009
173143
  "Ctrl-a": () => this.editor.commands.selectTextblockStart(),
173010
173144
  "Ctrl-e": () => this.editor.commands.selectTextblockEnd()
173011
173145
  };
173012
- if (isiOS() || isMacOS$1()) {
173146
+ if (isiOS() || isMacOS()) {
173013
173147
  return macKeymap;
173014
173148
  }
173015
173149
  return pcKeymap;
@@ -230077,27 +230211,40 @@ function MemoryButton({ mentorId, tenantKey, username }) {
230077
230211
  } }))] }) }), jsx(PopoverContent, { className: "w-96 rounded-lg border border-gray-200 bg-white p-0 shadow-xl", children: jsx(MemoryMenu, { onClose: () => setIsOpen(false), mentorId: mentorId, tenantKey: tenantKey, username: username }) })] }));
230078
230212
  }
230079
230213
 
230080
- // Cowork is macOS-only in prod; the env flag bypasses the OS check so the
230081
- // toggle can be exercised on Linux/Windows desktop builds during testing.
230082
- const isMacOS = () => {
230083
- if (typeof navigator === 'undefined')
230084
- return false;
230085
- return /mac/i.test(navigator.userAgent || '');
230086
- };
230087
- const allowNonMacOSCowork = () => process.env.NEXT_PUBLIC_ALLOW_NON_MACOS_COMPUTER_USE_TOGGLE === 'true';
230088
230214
  // 14GB floor per spec; modelSupportsCowork gates size > gb, so a model of
230089
230215
  // exactly 14GB is also off (no 14.0GB model in the catalog). Only the local-model
230090
230216
  // backend needs this; the remote AI has no size requirement.
230091
230217
  const COWORK_MIN_MODEL_GB = 14;
230092
230218
  const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode, artifactsEnabled, containerWidth, disabled = false, onOpenPromptGallery, embedMode = false, promptsIsEnabled = false, memoryEnabled = false, mentorId, tenantKey, username, }) => {
230093
230219
  const t = useT();
230094
- // Cowork = the Tauri GhostOS assistant. GhostOS runs the tools on-device;
230220
+ // Cowork = the Tauri cua-driver assistant. The driver runs the tools on-device;
230095
230221
  // the "brain" is EITHER a large local model (Local Models / Ollama) OR the
230096
- // remote AI (DM OpenAI-compatible endpoint), whichever is set up. useGhostOs
230222
+ // remote AI (DM OpenAI-compatible endpoint), whichever is set up. useCuaDriver
230097
230223
  // handles install/stop; the localStorage pref (isCoworkEnabled) is read
230098
230224
  // by the chat hook to route the turn. Reads the pref on mount.
230099
230225
  // Local state is `coworkOn` so it doesn't shadow the imported setCoworkEnabled.
230100
- const ghostOs = useGhostOs();
230226
+ //
230227
+ // NOTE — this toggle is deliberately UNGATED: it explains nothing and requests
230228
+ // no OS permission. Enabling Cowork gives an LLM synthetic input, screen
230229
+ // capture, and the user's logged-in browser sessions (the driver is spawned
230230
+ // with `--grant existing-profile`). A host that wants a consent step and the
230231
+ // macOS grants must build them and call `useCuaDriver().requestDriverPermissions()`
230232
+ // itself — the OS app's fork of this component does exactly that. See the
230233
+ // useCuaDriver docblock.
230234
+ // The host reports a machine-readable code; never render it raw.
230235
+ const unsupportedCoworkReason = (reason) => {
230236
+ switch (reason) {
230237
+ case 'kde_unproven':
230238
+ return t('chatInputFormInsideButtons.coworkUnsupportedKde');
230239
+ case 'gnome_helper_missing':
230240
+ return t('chatInputFormInsideButtons.coworkUnsupportedGnomeHelper');
230241
+ case 'unsupported_os':
230242
+ return t('chatInputFormInsideButtons.coworkUnsupportedOs');
230243
+ default:
230244
+ return t('chatInputFormInsideButtons.coworkUnsupportedSession');
230245
+ }
230246
+ };
230247
+ const ghostOs = useCuaDriver();
230101
230248
  const [coworkOn, setCoworkOn] = useState(isCoworkEnabled);
230102
230249
  const toggleCowork = () => {
230103
230250
  const next = !coworkOn;
@@ -230108,8 +230255,7 @@ const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode,
230108
230255
  // `disabled` when the user lacks the mentor `#chat` permission — see
230109
230256
  // chat-input-form's isChatDisabledByRbac.)
230110
230257
  if (next) {
230111
- const localReady = isLocalLLMEnabled() &&
230112
- modelSupportsCowork(getLocalLLMModel(), COWORK_MIN_MODEL_GB);
230258
+ const localReady = isLocalLLMEnabled() && modelSupportsCowork(getLocalLLMModel(), COWORK_MIN_MODEL_GB);
230113
230259
  if (!localReady && !hasRemoteAiConfig()) {
230114
230260
  toast.warning(isLocalLLMEnabled()
230115
230261
  ? t('chatInputFormInsideButtons.coworkModelTooSmall')
@@ -230173,7 +230319,15 @@ const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode,
230173
230319
  icon: jsx(Monitor, { className: "h-4 w-4" }),
230174
230320
  isActive: coworkOn,
230175
230321
  action: toggleCowork,
230176
- isEnabled: ghostOs.isAvailable && (isMacOS() || allowNonMacOSCowork()),
230322
+ // Cowork used to be macOS-only because GhostOS was. The Cua Driver runs
230323
+ // on Windows, macOS and Linux — but not on every Linux session, so an
230324
+ // unsupported one renders the pill DISABLED with the reason rather than
230325
+ // hiding it. The chatbox is Cowork's only surface: hide it and a KDE user
230326
+ // is left with no way to find out why the feature is missing.
230327
+ isEnabled: ghostOs.isAvailable,
230328
+ disabledReason: ghostOs.isSupported
230329
+ ? undefined
230330
+ : unsupportedCoworkReason(ghostOs.unsupportedReason),
230177
230331
  },
230178
230332
  ].filter((item) => item.isEnabled);
230179
230333
  // Get visible inside buttons based on screen size
@@ -230211,7 +230365,7 @@ const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode,
230211
230365
  if (button.name === 'Memory') {
230212
230366
  return (jsx(MemoryButton, { mentorId: mentorId, tenantKey: tenantKey, username: username }, button.name));
230213
230367
  }
230214
- return (jsx("div", { className: "relative", children: jsxs(Button$1, { variant: "ghost", size: "sm", type: "button", disabled: disabled, className: `flex h-8 items-center gap-1.5 rounded-lg px-2 text-sm transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50 ${button.isActive
230368
+ return (jsx("div", { className: "relative", children: jsxs(Button$1, { variant: "ghost", size: "sm", type: "button", disabled: disabled || !!button.disabledReason, title: button.disabledReason, className: `flex h-8 items-center gap-1.5 rounded-lg px-2 text-sm transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50 ${button.isActive
230215
230369
  ? 'border border-[#D0E0FF] bg-[#F5F8FF] text-[#38A1E5]'
230216
230370
  : 'text-gray-600 hover:border hover:border-[#D0E0FF] hover:bg-[#F5F8FF]'}`, onClick: (e) => {
230217
230371
  e.preventDefault();
@@ -234967,5 +235121,5 @@ var trainOrDeleteModal = /*#__PURE__*/Object.freeze({
234967
235121
  TrainOrDeleteModal: TrainOrDeleteModal
234968
235122
  });
234969
235123
 
234970
- export { ACCESS_COURSE_LABEL, AccessTimeHeatmap, AccessiblePaginate, AddMentorToProjectModal, AddSkillDialog, Admin, AdvancedTab, AgentConfigPrompts, AgentSkills, AlertsTab, AnalyticsAuditLogStats, AnalyticsCourseDetail, AnalyticsCourses, AnalyticsFinancialStats, AnalyticsLayout, AnalyticsMonetizationStats, AnalyticsOverview, AnalyticsProgramDetail, AnalyticsPrograms, AnalyticsReportDownload, AnalyticsReports, AnalyticsSettingsProvider, AnalyticsTopicsStats, AnalyticsTranscriptsStats, AnalyticsUsersStats, AppleRestrictionModal, ApplyWizard, BUY_NOW_LABEL, BillingTab, CATALOGS, CREATE_AGENT_LABELS, CategorizedDropdownMenu, ChartCardWrapper, ChartFiltersProvider, ChartLoading, ChatContext, ChatPrivacyToggle, ChatProvider, CompanyDialog, ConnectorManagementDialog, CopyButtonIcon, CourseAccessGuard, CourseCardSkeleton, CourseContentLoading, CourseOutline, CourseOutlineContext, CourseOutlineDrawer, CreateAgentForm, CreateAgentModal, CreateProjectModal, CreateWorkflowModal, CredentialBox, CredentialMiniBoxSkeleton, CredentialsList, CreditBalance, CustomDateRangePicker, CustomTooltip, DEFAULT_COWORK_REQUIRED_SIZE_GB, DEFAULT_LOCALE, DefaultEmptyBox, DeleteProjectModal, DeleteWorkflowModal, DiscoverContentCard, DiscoverFacetsFilter, DiscoverFilterDrawer, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, ENROLL_NOW_COURSE_STARTING_SOON_LABEL, ENROLL_NOW_LABEL, EditAlertDialog, EducationBox, EducationDialog, EducationTab, EdxIframeContext, EmptyStats, ExperienceBox, ExperienceDialog, ExperienceTab, FacetFilterContext, Footer, GHOST_OS_REPO_URL, GHOST_OS_TAURI_COMMANDS, GHOST_OS_TAURI_EVENTS, GradebookTab, GroupsFilterDropdown, INVITATION_ONLY_LABEL, InstitutionDialog, IntegrationsTab, InviteUserContent, InviteUserDialog, InvitedUsersDialog, LOCAL_MODELS, Loader, LocalLLMTab, LocalModelsContent, LoginButton, MACOS_PERMISSIONS_COMMANDS, Markdown, MinimumMentorAlert, MonetizationTab, NotificationDisplay, NotificationDropdown, ONBOARDING_SECTORS, OnboardingShell, OnboardingWizard, PaywallModal, PlacementExam, PlacementExamsDropdown, PlatformNavbar, PlatformNavbarSearch, Profile, ProfileTimeChart, ProgressDots, ProjectActionButtons, ProjectFilesModal, ProjectInstructionsModal, ProjectLandingPage, ProjectMentorsList, ProjectsPage, REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL, REQUEST_ACCESS_LABEL, RenameProjectModal, ResumeBox, ResumeTab, RichTextEditor, SUPPORTED_LOCALES, SandboxConfig, SearchableMultiSelect, SendNotificationDialog, SignupButton, SkeletonActivityStatBox, SkeletonAddSkillsLoading, SkeletonCreatePathwaySearchList, SkeletonDiscoverFilterBox, SkeletonEducationBox, SkeletonMultiplier, SkeletonPathwayBox, SkeletonProfileInfoCard, SkeletonSkillBox, SkillBox, SkillDetailModal, SkillLeaderboardChart, SkillsBox, SkillsList, SlashSkillPicker, Spinner, StatCard, StepHeader, TAURI_COMMANDS, TAURI_EVENTS, TenantSwitcher, TimeFilter, TimeTrackingProvider, TimedExam, ToolDialogs, TopBanner, UpgradePackageModal, UserAvatar, Version, WebContainersI18nProvider, WorkflowSidebar, addBookmarksTab, availablePlacementExams, findLastResumeBlock, findSequentialParent, flattenVerticalBlocks, getCatalog, getFirstAvailableUnit, getLocalLLMModel, getLocalLLMToolSupport, getNextUnitIframe, getOrg, getParentBlockById, getParentsInfosFromSublessonId, getPreviousUnitIframe, getRandomCourseImage, getSectorById, getTenant, getUnitToIframe, getUserId, getUserName, inBrowserPrint, inIframe, initialGhostOsInstallState, initialModelDownloadState, isCoworkEnabled, isLocalLLMEnabled, isPlatformNavbarCreditBalanceVisible, isSlashCommandToken, isTauriApp, isValidLocale, components as markdownComponents, modelSupportsCowork, onboardingPrimaryButtonClass, onboardingSecondaryButtonClass, resolveCreateAgentLabels, sanitizeCss, setCoworkEnabled, setLocalLLMEnabled, setLocalLLMModel, setLocalLLMToolSupport, smallestCoworkModel, useAnalyticsSettings, useAuditLog, useCatalogSearch, useChartFilters, useChatPrivacy, useChatState, useCourseDetail, useCourseMetadata, useCourses, useDiscover, useEdxIframe, useFinancial, useGhostOs, useIframeMessageHandler, useLocalStorage, useModelDownload, useMonetization, useOverview, useProfileActivityStats, useProfileCredentials, useProfilePathways, useProfilePrograms, useProfileSkills, useProfileTimeSpent, usePrograms, useReports, useSlashSkillPicker, useT, useTauri, useTimeTracking, useTopics, useTranscripts, useUserCourses, useUserMetadata, useUsers, useWebContainersI18n, useWebContainersLocale };
235124
+ export { ACCESS_COURSE_LABEL, AccessTimeHeatmap, AccessiblePaginate, AddMentorToProjectModal, AddSkillDialog, Admin, AdvancedTab, AgentConfigPrompts, AgentSkills, AlertsTab, AnalyticsAuditLogStats, AnalyticsCourseDetail, AnalyticsCourses, AnalyticsFinancialStats, AnalyticsLayout, AnalyticsMonetizationStats, AnalyticsOverview, AnalyticsProgramDetail, AnalyticsPrograms, AnalyticsReportDownload, AnalyticsReports, AnalyticsSettingsProvider, AnalyticsTopicsStats, AnalyticsTranscriptsStats, AnalyticsUsersStats, AppleRestrictionModal, ApplyWizard, BUY_NOW_LABEL, BillingTab, CATALOGS, CREATE_AGENT_LABELS, CUA_DRIVER_REPO_URL, CUA_DRIVER_TAURI_COMMANDS, CUA_DRIVER_TAURI_EVENTS, CategorizedDropdownMenu, ChartCardWrapper, ChartFiltersProvider, ChartLoading, ChatContext, ChatPrivacyToggle, ChatProvider, CompanyDialog, ConnectorManagementDialog, CopyButtonIcon, CourseAccessGuard, CourseCardSkeleton, CourseContentLoading, CourseOutline, CourseOutlineContext, CourseOutlineDrawer, CreateAgentForm, CreateAgentModal, CreateProjectModal, CreateWorkflowModal, CredentialBox, CredentialMiniBoxSkeleton, CredentialsList, CreditBalance, CustomDateRangePicker, CustomTooltip, DEFAULT_COWORK_REQUIRED_SIZE_GB, DEFAULT_LOCALE, DefaultEmptyBox, DeleteProjectModal, DeleteWorkflowModal, DiscoverContentCard, DiscoverFacetsFilter, DiscoverFilterDrawer, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, ENROLL_NOW_COURSE_STARTING_SOON_LABEL, ENROLL_NOW_LABEL, EditAlertDialog, EducationBox, EducationDialog, EducationTab, EdxIframeContext, EmptyStats, ExperienceBox, ExperienceDialog, ExperienceTab, FacetFilterContext, Footer, GradebookTab, GroupsFilterDropdown, INVITATION_ONLY_LABEL, InstitutionDialog, IntegrationsTab, InviteUserContent, InviteUserDialog, InvitedUsersDialog, LOCAL_MODELS, Loader, LocalLLMTab, LocalModelsContent, LoginButton, MACOS_PERMISSIONS_COMMANDS, Markdown, MinimumMentorAlert, MonetizationTab, NotificationDisplay, NotificationDropdown, ONBOARDING_SECTORS, OnboardingShell, OnboardingWizard, PaywallModal, PlacementExam, PlacementExamsDropdown, PlatformNavbar, PlatformNavbarSearch, Profile, ProfileTimeChart, ProgressDots, ProjectActionButtons, ProjectFilesModal, ProjectInstructionsModal, ProjectLandingPage, ProjectMentorsList, ProjectsPage, REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL, REQUEST_ACCESS_LABEL, RenameProjectModal, ResumeBox, ResumeTab, RichTextEditor, SUPPORTED_LOCALES, SandboxConfig, SearchableMultiSelect, SendNotificationDialog, SignupButton, SkeletonActivityStatBox, SkeletonAddSkillsLoading, SkeletonCreatePathwaySearchList, SkeletonDiscoverFilterBox, SkeletonEducationBox, SkeletonMultiplier, SkeletonPathwayBox, SkeletonProfileInfoCard, SkeletonSkillBox, SkillBox, SkillDetailModal, SkillLeaderboardChart, SkillsBox, SkillsList, SlashSkillPicker, Spinner, StatCard, StepHeader, TAURI_COMMANDS, TAURI_EVENTS, TenantSwitcher, TimeFilter, TimeTrackingProvider, TimedExam, ToolDialogs, TopBanner, UpgradePackageModal, UserAvatar, Version, WebContainersI18nProvider, WorkflowSidebar, addBookmarksTab, availablePlacementExams, findLastResumeBlock, findSequentialParent, flattenVerticalBlocks, getCatalog, getFirstAvailableUnit, getLocalLLMModel, getLocalLLMToolSupport, getNextUnitIframe, getOrg, getParentBlockById, getParentsInfosFromSublessonId, getPreviousUnitIframe, getRandomCourseImage, getSectorById, getTenant, getUnitToIframe, getUserId, getUserName, inBrowserPrint, inIframe, initialCuaDriverInstallState, initialModelDownloadState, isCoworkEnabled, isLocalLLMEnabled, isPlatformNavbarCreditBalanceVisible, isSlashCommandToken, isTauriApp, isValidLocale, components as markdownComponents, modelSupportsCowork, onboardingPrimaryButtonClass, onboardingSecondaryButtonClass, resolveCreateAgentLabels, sanitizeCss, setCoworkEnabled, setLocalLLMEnabled, setLocalLLMModel, setLocalLLMToolSupport, smallestCoworkModel, useAnalyticsSettings, useAuditLog, useCatalogSearch, useChartFilters, useChatPrivacy, useChatState, useCourseDetail, useCourseMetadata, useCourses, useCuaDriver, useDiscover, useEdxIframe, useFinancial, useIframeMessageHandler, useLocalStorage, useModelDownload, useMonetization, useOverview, useProfileActivityStats, useProfileCredentials, useProfilePathways, useProfilePrograms, useProfileSkills, useProfileTimeSpent, usePrograms, useReports, useSlashSkillPicker, useT, useTauri, useTimeTracking, useTopics, useTranscripts, useUserCourses, useUserMetadata, useUsers, useWebContainersI18n, useWebContainersLocale };
234971
235125
  //# sourceMappingURL=index.esm.js.map