@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.
@@ -98253,7 +98253,11 @@ var chatInputFormInsideButtons$3 = {
98253
98253
  cowork: "Cowork",
98254
98254
  coworkNeedsBackend: "Enable a large local model or sign in to a workspace to use Cowork.",
98255
98255
  coworkModelTooSmall: "Your local model is too small for Cowork. Pick a model of at least 14GB in Local Models.",
98256
- moreOptions: "More options"
98256
+ moreOptions: "More options",
98257
+ coworkUnsupportedKde: "Cowork isn’t supported on KDE Plasma yet.",
98258
+ coworkUnsupportedGnomeHelper: "Cowork on GNOME needs the WinRects helper installed, then a session restart.",
98259
+ coworkUnsupportedOs: "Cowork isn’t supported on this operating system.",
98260
+ coworkUnsupportedSession: "Cowork isn’t supported on this desktop session."
98257
98261
  };
98258
98262
  var chatInputFormMemoryMenu$3 = {
98259
98263
  title: "Your Memory",
@@ -104581,7 +104585,11 @@ var chatInputFormInsideButtons$2 = {
104581
104585
  cowork: "Cowork",
104582
104586
  coworkNeedsBackend: "Activez un grand modèle local ou connectez-vous à un espace de travail pour utiliser Cowork.",
104583
104587
  coworkModelTooSmall: "Votre modèle local est trop petit pour Cowork. Choisissez un modèle d’au moins 14 Go dans Modèles Locaux.",
104584
- moreOptions: "Plus d'options"
104588
+ moreOptions: "Plus d'options",
104589
+ coworkUnsupportedKde: "Cowork n’est pas encore pris en charge sur KDE Plasma.",
104590
+ coworkUnsupportedGnomeHelper: "Cowork sur GNOME nécessite l’extension WinRects, puis un redémarrage de la session.",
104591
+ coworkUnsupportedOs: "Cowork n’est pas pris en charge sur ce système d’exploitation.",
104592
+ coworkUnsupportedSession: "Cowork n’est pas pris en charge sur cette session de bureau."
104585
104593
  };
104586
104594
  var chatInputFormMemoryMenu$2 = {
104587
104595
  title: "Votre mémoire",
@@ -110797,7 +110805,11 @@ var chatInputFormInsideButtons$1 = {
110797
110805
  cowork: "Cowork",
110798
110806
  coworkNeedsBackend: "Activa un modelo local grande o inicia sesión en un espacio de trabajo para usar Cowork.",
110799
110807
  coworkModelTooSmall: "Tu modelo local es demasiado pequeño para Cowork. Elige un modelo de al menos 14 GB en Modelos Locales.",
110800
- moreOptions: "Más opciones"
110808
+ moreOptions: "Más opciones",
110809
+ coworkUnsupportedKde: "Cowork aún no es compatible con KDE Plasma.",
110810
+ coworkUnsupportedGnomeHelper: "Cowork en GNOME necesita la extensión WinRects instalada y reiniciar la sesión.",
110811
+ coworkUnsupportedOs: "Cowork no es compatible con este sistema operativo.",
110812
+ coworkUnsupportedSession: "Cowork no es compatible con esta sesión de escritorio."
110801
110813
  };
110802
110814
  var chatInputFormMemoryMenu$1 = {
110803
110815
  title: "Tu memoria",
@@ -117013,7 +117025,11 @@ var chatInputFormInsideButtons = {
117013
117025
  cowork: "Cowork",
117014
117026
  coworkNeedsBackend: "启用大型本地模型或登录工作区即可使用 Cowork。",
117015
117027
  coworkModelTooSmall: "您的本地模型太小,无法用于 Cowork。请在“本地模型”中选择至少 14GB 的模型。",
117016
- moreOptions: "更多选项"
117028
+ moreOptions: "更多选项",
117029
+ coworkUnsupportedKde: "Cowork 尚不支持 KDE Plasma。",
117030
+ coworkUnsupportedGnomeHelper: "在 GNOME 上使用 Cowork 需要安装 WinRects 扩展,然后重启会话。",
117031
+ coworkUnsupportedOs: "Cowork 不支持此操作系统。",
117032
+ coworkUnsupportedSession: "Cowork 不支持此桌面会话。"
117017
117033
  };
117018
117034
  var chatInputFormMemoryMenu = {
117019
117035
  title: "您的记忆",
@@ -204436,7 +204452,7 @@ var joinTextblockForward = () => ({ state, dispatch }) => {
204436
204452
  };
204437
204453
 
204438
204454
  // src/utilities/isMacOS.ts
204439
- function isMacOS$1() {
204455
+ function isMacOS() {
204440
204456
  return typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
204441
204457
  }
204442
204458
 
@@ -204462,7 +204478,7 @@ function normalizeKeyName(name) {
204462
204478
  } else if (/^s(hift)?$/i.test(mod)) {
204463
204479
  shift = true;
204464
204480
  } else if (/^mod$/i.test(mod)) {
204465
- if (isiOS() || isMacOS$1()) {
204481
+ if (isiOS() || isMacOS()) {
204466
204482
  meta = true;
204467
204483
  } else {
204468
204484
  ctrl = true;
@@ -207439,7 +207455,7 @@ var Keymap = Extension.create({
207439
207455
  "Ctrl-a": () => this.editor.commands.selectTextblockStart(),
207440
207456
  "Ctrl-e": () => this.editor.commands.selectTextblockEnd()
207441
207457
  };
207442
- if (isiOS() || isMacOS$1()) {
207458
+ if (isiOS() || isMacOS()) {
207443
207459
  return macKeymap;
207444
207460
  }
207445
207461
  return pcKeymap;
@@ -254795,7 +254811,7 @@ const TAURI_COMMANDS = {
254795
254811
  /**
254796
254812
  * Initial state for the Cowork install flow.
254797
254813
  */
254798
- const initialGhostOsInstallState = {
254814
+ const initialCuaDriverInstallState = {
254799
254815
  status: 'idle',
254800
254816
  progress: 0,
254801
254817
  message: '',
@@ -254803,24 +254819,27 @@ const initialGhostOsInstallState = {
254803
254819
  lastUpdated: new Date().toISOString(),
254804
254820
  };
254805
254821
  /**
254806
- * Tauri event names emitted by the host during GhostOS install/setup.
254822
+ * Tauri event names emitted by the host during Cua Driver install.
254807
254823
  */
254808
- const GHOST_OS_TAURI_EVENTS = {
254809
- INSTALL_PROGRESS: 'ghost-os:install-progress',
254810
- INSTALLATION_LOG: 'ghost-os:installation-log',
254811
- STATUS: 'ghost-os:status',
254824
+ const CUA_DRIVER_TAURI_EVENTS = {
254825
+ INSTALL_PROGRESS: 'cua-driver:install-progress',
254826
+ INSTALLATION_LOG: 'cua-driver:installation-log',
254827
+ STATUS: 'cua-driver:status',
254812
254828
  };
254813
254829
  /**
254814
- * Tauri command names the host implements for GhostOS install/setup. The web
254815
- * layer invokes these; the native side performs the clone + setup.
254830
+ * Tauri command names the host implements for the Cua Driver. The web layer
254831
+ * invokes these; the native side downloads the pinned per-platform binary and
254832
+ * owns the MCP process.
254816
254833
  */
254817
- const GHOST_OS_TAURI_COMMANDS = {
254818
- /** Install and set up GhostOS (clone repo + run setup). */
254819
- INSTALL_GHOST_OS: 'install_ghost_os',
254820
- /** Stop the running GhostOS manager. */
254821
- STOP_GHOST_OS: 'stop_ghost_os',
254822
- /** Report current {@link GhostOsStatus}. */
254823
- CHECK_GHOST_OS_STATUS: 'check_ghost_os_status',
254834
+ const CUA_DRIVER_TAURI_COMMANDS = {
254835
+ /** Download + install the pinned Cua Driver binary. */
254836
+ INSTALL: 'install_cua_driver',
254837
+ /** Report current {@link CuaDriverStatus}. */
254838
+ CHECK_STATUS: 'check_cua_driver_status',
254839
+ /** Whether Cowork can run on this desktop session, and why not if it can't. */
254840
+ SUPPORT: 'cua_driver_support',
254841
+ /** Stop the driver's MCP process. */
254842
+ STOP: 'cua_driver_stop',
254824
254843
  };
254825
254844
  /**
254826
254845
  * Tauri command strings exposed by `tauri-plugin-macos-permissions`
@@ -254835,12 +254854,7 @@ const MACOS_PERMISSIONS_COMMANDS = {
254835
254854
  CHECK_ACCESSIBILITY: 'plugin:macos-permissions|check_accessibility_permission',
254836
254855
  REQUEST_ACCESSIBILITY: 'plugin:macos-permissions|request_accessibility_permission',
254837
254856
  CHECK_SCREEN_RECORDING: 'plugin:macos-permissions|check_screen_recording_permission',
254838
- REQUEST_SCREEN_RECORDING: 'plugin:macos-permissions|request_screen_recording_permission',
254839
- CHECK_INPUT_MONITORING: 'plugin:macos-permissions|check_input_monitoring_permission',
254840
- REQUEST_INPUT_MONITORING: 'plugin:macos-permissions|request_input_monitoring_permission',
254841
- CHECK_FULL_DISK_ACCESS: 'plugin:macos-permissions|check_full_disk_access_permission',
254842
- REQUEST_FULL_DISK_ACCESS: 'plugin:macos-permissions|request_full_disk_access_permission',
254843
- };
254857
+ REQUEST_SCREEN_RECORDING: 'plugin:macos-permissions|request_screen_recording_permission'};
254844
254858
 
254845
254859
  /**
254846
254860
  * Dynamically import Tauri APIs to avoid SSR issues
@@ -254907,16 +254921,26 @@ function useTauri() {
254907
254921
  };
254908
254922
  }
254909
254923
 
254910
- const IS_SERVER = typeof window === 'undefined';
254924
+ // Evaluated per call, not once at module load. A module-level constant is
254925
+ // captured before any test can influence it, which left every server-side guard
254926
+ // below permanently unreachable — and therefore unverifiable — in a jsdom runner.
254927
+ const isServer = () => typeof window === 'undefined';
254911
254928
  /**
254912
254929
  * A simplified useLocalStorage hook for persisting state to localStorage
254913
254930
  * with SSR safety.
254914
254931
  */
254915
254932
  function useLocalStorage(key, initialValue) {
254933
+ // The initial value is captured, not tracked. Depending on its identity meant
254934
+ // an inline literal — `useLocalStorage('k', { n: 0 })`, the ordinary React
254935
+ // idiom — gave `getInitialValue` a new identity every render, which re-ran the
254936
+ // key-change effect below, which called `setStoredValue` with a fresh object,
254937
+ // which re-rendered… an infinite loop that ends in an out-of-memory crash.
254938
+ // Existing callers only escaped it by happening to pass module constants.
254939
+ const initialRef = useRef(initialValue);
254916
254940
  // Get the initial value
254917
254941
  const getInitialValue = useCallback(() => {
254918
- const initial = initialValue instanceof Function ? initialValue() : initialValue;
254919
- if (IS_SERVER) {
254942
+ const initial = initialRef.current instanceof Function ? initialRef.current() : initialRef.current;
254943
+ if (isServer()) {
254920
254944
  return initial;
254921
254945
  }
254922
254946
  try {
@@ -254930,118 +254954,204 @@ function useLocalStorage(key, initialValue) {
254930
254954
  console.warn(`Error reading localStorage key "${key}":`, error);
254931
254955
  return initial;
254932
254956
  }
254933
- }, [key, initialValue]);
254957
+ }, [key]);
254934
254958
  const [storedValue, setStoredValue] = useState(getInitialValue);
254935
254959
  // Update stored value when key changes
254936
254960
  useEffect(() => {
254937
254961
  setStoredValue(getInitialValue());
254938
254962
  }, [key, getInitialValue]);
254939
- // Persist to localStorage
254963
+ // Latest value, readable synchronously. Two writes in the same tick would
254964
+ // otherwise both resolve their updater against the render's snapshot, so the
254965
+ // second silently reverts the first (a status change followed by a log append
254966
+ // losing the status, for instance).
254967
+ const storedRef = useRef(storedValue);
254968
+ storedRef.current = storedValue;
254969
+ // Persist to localStorage.
254970
+ //
254971
+ // Depends on `key` ALONE, deliberately. This used to close over `storedValue`,
254972
+ // which gave the setter a new identity on every value change — and anything
254973
+ // built on it (a `useCallback` logger, say) churned too. An effect listing
254974
+ // those in its dependencies then re-ran on every state change; where such an
254975
+ // effect subscribes asynchronously, its cleanup runs before the subscription
254976
+ // resolves and unregisters nothing, so each pass leaks another live listener
254977
+ // and one event fans out into two, four, eight… until the webview locks up.
254940
254978
  const setValue = useCallback((value) => {
254941
- if (IS_SERVER) {
254979
+ if (isServer()) {
254942
254980
  console.warn(`Tried setting localStorage key "${key}" even though environment is not a client`);
254943
254981
  return;
254944
254982
  }
254945
254983
  try {
254946
- const valueToStore = value instanceof Function ? value(storedValue) : value;
254984
+ const valueToStore = value instanceof Function ? value(storedRef.current) : value;
254985
+ storedRef.current = valueToStore;
254947
254986
  setStoredValue(valueToStore);
254948
254987
  window.localStorage.setItem(key, JSON.stringify(valueToStore));
254949
254988
  }
254950
254989
  catch (error) {
254951
254990
  console.warn(`Error setting localStorage key "${key}":`, error);
254952
254991
  }
254953
- }, [key, storedValue]);
254992
+ }, [key]);
254954
254993
  // Remove from localStorage
254955
254994
  const removeValue = useCallback(() => {
254956
- if (IS_SERVER) {
254995
+ if (isServer()) {
254957
254996
  console.warn(`Tried removing localStorage key "${key}" even though environment is not a client`);
254958
254997
  return;
254959
254998
  }
254960
254999
  try {
254961
- const initial = initialValue instanceof Function ? initialValue() : initialValue;
255000
+ const initial = initialRef.current instanceof Function ? initialRef.current() : initialRef.current;
254962
255001
  window.localStorage.removeItem(key);
254963
255002
  setStoredValue(initial);
254964
255003
  }
254965
255004
  catch (error) {
254966
255005
  console.warn(`Error removing localStorage key "${key}":`, error);
254967
255006
  }
254968
- }, [key, initialValue]);
255007
+ // `[key]` alone, for the same reason as getInitialValue above — otherwise an
255008
+ // inline initial value churns this callback's identity on every render.
255009
+ }, [key]);
254969
255010
  return [storedValue, setValue, removeValue];
254970
255011
  }
254971
255012
 
254972
- const LOCAL_STORAGE_KEY$1 = 'ghost_os_install_state';
255013
+ const LOCAL_STORAGE_KEY$1 = 'cua_driver_install_state';
254973
255014
  /**
254974
- * Manage installing & setting up GhostOS (https://github.com/ghostwright/ghost-os)
254975
- * via the Tauri host. This is the self-contained backing hook for the System
254976
- * Control card and deliberately mirrors `useModelDownload`:
255015
+ * Manage installing the Cua Driver (https://github.com/trycua/cua) via the Tauri
255016
+ * host. This is the self-contained backing hook for the Cowork card and
255017
+ * deliberately mirrors `useModelDownload`:
254977
255018
  *
254978
- * - checks GhostOS install/run status through the host
254979
- * - installs/sets up GhostOS with real-time progress + logs
255019
+ * - checks driver install status AND whether this desktop session is supported
255020
+ * - downloads/installs the pinned driver with real-time progress + logs
254980
255021
  * - persists state across app restarts
254981
255022
  *
254982
255023
  * Outside the desktop (Tauri) app every action is a no-op and `isAvailable` is
254983
- * false, so the card hides itself.
255024
+ * false, so the card hides itself. Inside it, `isSupported` is what gates the
255025
+ * Cowork toggle — the driver has no proven behaviour on every Linux session.
255026
+ *
255027
+ * **Consent is the host application's job, not this hook's.** Enabling Cowork
255028
+ * hands an LLM synthetic input, screen capture, and — through the driver's
255029
+ * `--grant existing-profile` — the user's logged-in browser sessions. The driver
255030
+ * renders no consent UI of its own, and neither does the `InsideButtons` toggle
255031
+ * in this package. A host must explain that and get consent before calling
255032
+ * `requestDriverPermissions()`.
254984
255033
  */
254985
- function useGhostOs() {
255034
+ function useCuaDriver() {
254986
255035
  const { isAvailable, invoke, listen } = useTauri();
254987
- const [state, setState] = useLocalStorage(LOCAL_STORAGE_KEY$1, initialGhostOsInstallState);
255036
+ const [state, setState] = useLocalStorage(LOCAL_STORAGE_KEY$1, initialCuaDriverInstallState);
254988
255037
  const [status, setStatus] = useState(null);
254989
- // macOS Accessibility permission, which GhostOS needs to control the device.
254990
- // null = unknown/not-yet-checked or unsupported (non-macOS / plugin absent).
255038
+ // The two macOS grants the driver needs: Accessibility to post synthetic input,
255039
+ // Screen Recording because get_window_state always captures alongside the tree.
255040
+ //
255041
+ // `null` is a third state and a meaningful one: the commands exist only on
255042
+ // macOS, so elsewhere the invoke throws and the answer is "not applicable"
255043
+ // rather than "denied". Callers must branch on `=== false`, never on falsy —
255044
+ // treating null as denied would make Cowork unusable on Linux and Windows.
254991
255045
  const [accessibilityPermission, setAccessibilityPermission] = useState(null);
254992
- const unlistenRefs = useRef([]);
255046
+ const [screenRecordingPermission, setScreenRecordingPermission] = useState(null);
254993
255047
  const hasCheckedStatus = useRef(false);
254994
255048
  /**
254995
- * Refresh the macOS Accessibility permission via tauri-plugin-macos-permissions.
254996
- * Independent of the GhostOS status check so a missing host command for one
254997
- * doesn't suppress the other. Stays null where unsupported.
255049
+ * Read one permission. Checking never prompts (`AXIsProcessTrusted` /
255050
+ * `CGPreflightScreenCaptureAccess`), which is what makes it safe to do on mount.
254998
255051
  */
254999
- const refreshAccessibilityPermission = useCallback(async () => {
255000
- if (!isAvailable)
255001
- return;
255052
+ const checkPermission = useCallback(async (command) => {
255002
255053
  try {
255003
- const granted = await invoke(MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY);
255004
- setAccessibilityPermission(granted);
255054
+ return await invoke(command);
255005
255055
  }
255006
255056
  catch (error) {
255007
- // Plugin not registered or non-macOS host — leave permission unknown.
255008
- console.warn('[useGhostOs] Accessibility permission check unavailable:', error);
255009
- setAccessibilityPermission(null);
255057
+ // Plugin not registered or non-macOS host — not applicable, not denied.
255058
+ console.warn(`[useCuaDriver] ${command} unavailable:`, error);
255059
+ return null;
255010
255060
  }
255011
- }, [isAvailable, invoke]);
255061
+ }, [invoke]);
255012
255062
  /**
255013
- * Open the Accessibility pane / prompt so the user can grant permission, then
255014
- * re-check (the grant happens in System Settings, so we refresh after).
255063
+ * Refresh both permissions. Independent of the driver status check so a missing
255064
+ * host command for one doesn't suppress the other.
255015
255065
  */
255016
- const requestAccessibilityPermission = useCallback(async () => {
255066
+ const refreshPermissions = useCallback(async () => {
255017
255067
  if (!isAvailable)
255018
255068
  return;
255019
- try {
255020
- await invoke(MACOS_PERMISSIONS_COMMANDS.REQUEST_ACCESSIBILITY);
255021
- }
255022
- catch (error) {
255023
- console.error('[useGhostOs] Failed to request accessibility permission:', error);
255069
+ const [accessibility, screenRecording] = await Promise.all([
255070
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY),
255071
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_SCREEN_RECORDING),
255072
+ ]);
255073
+ setAccessibilityPermission(accessibility);
255074
+ setScreenRecordingPermission(screenRecording);
255075
+ }, [isAvailable, checkPermission]);
255076
+ /**
255077
+ * Ask for whichever grants are missing, then report what is actually held.
255078
+ *
255079
+ * **Prompts the user, so never call this from an effect** — it belongs to an
255080
+ * explicit action (switching Cowork on), not to mounting a component. The OS
255081
+ * prompt is not a consent step: it asks for a macOS grant, not for permission
255082
+ * to read the screen and act as the user. Explain that first (see the hook
255083
+ * docblock) — this package does not.
255084
+ *
255085
+ * Returns the post-request values rather than leaving the caller to read state:
255086
+ * a `setState` in this tick is not visible to the caller that awaited us, and
255087
+ * that caller has to decide right now whether to proceed.
255088
+ *
255089
+ * Note the values can still be `false` immediately after a successful grant —
255090
+ * `CGRequestScreenCaptureAccess` returns before the grant takes effect, which
255091
+ * on macOS usually needs an app restart. Copy shown to the user must say so.
255092
+ */
255093
+ const requestDriverPermissions = useCallback(async () => {
255094
+ if (!isAvailable)
255095
+ return { accessibility: null, screenRecording: null };
255096
+ const pairs = [
255097
+ [
255098
+ MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY,
255099
+ MACOS_PERMISSIONS_COMMANDS.REQUEST_ACCESSIBILITY,
255100
+ ],
255101
+ [
255102
+ MACOS_PERMISSIONS_COMMANDS.CHECK_SCREEN_RECORDING,
255103
+ MACOS_PERMISSIONS_COMMANDS.REQUEST_SCREEN_RECORDING,
255104
+ ],
255105
+ ];
255106
+ for (const [check, request] of pairs) {
255107
+ // Only prompt for what is actually missing — re-requesting a held grant
255108
+ // just reopens System Settings for no reason.
255109
+ if ((await checkPermission(check)) !== false)
255110
+ continue;
255111
+ try {
255112
+ await invoke(request);
255113
+ }
255114
+ catch (error) {
255115
+ console.error(`[useCuaDriver] ${request} failed:`, error);
255116
+ }
255024
255117
  }
255025
- await refreshAccessibilityPermission();
255026
- }, [isAvailable, invoke, refreshAccessibilityPermission]);
255027
- const addLog = useCallback((log) => {
255118
+ const [accessibility, screenRecording] = await Promise.all([
255119
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_ACCESSIBILITY),
255120
+ checkPermission(MACOS_PERMISSIONS_COMMANDS.CHECK_SCREEN_RECORDING),
255121
+ ]);
255122
+ setAccessibilityPermission(accessibility);
255123
+ setScreenRecordingPermission(screenRecording);
255124
+ return { accessibility, screenRecording };
255125
+ }, [isAvailable, invoke, checkPermission]);
255126
+ /**
255127
+ * Append a log line, optionally in the SAME update as other state changes.
255128
+ *
255129
+ * The `patch` argument is not a convenience: `setState` here is backed by
255130
+ * `useLocalStorage`, whose write + re-read means a log appended straight after
255131
+ * a status change can be computed from the pre-change snapshot and silently
255132
+ * revert it. That is how a failed install used to end up displayed as
255133
+ * "completed" with the error only visible down in the log pane. Anything that
255134
+ * sets status AND logs must do both in one update.
255135
+ */
255136
+ const addLog = useCallback((log, patch) => {
255028
255137
  setState((prev) => ({
255029
255138
  ...prev,
255139
+ ...patch,
255030
255140
  logs: [...prev.logs.slice(-99), log],
255031
255141
  lastUpdated: new Date().toISOString(),
255032
255142
  }));
255033
255143
  }, [setState]);
255034
255144
  /**
255035
- * Query GhostOS status from the host.
255145
+ * Query driver install status + session support from the host.
255036
255146
  */
255037
255147
  const checkStatus = useCallback(async () => {
255038
255148
  if (!isAvailable)
255039
255149
  return;
255040
- // Refresh the accessibility permission alongside install status (independent).
255041
- refreshAccessibilityPermission();
255150
+ // Refresh both permissions alongside install status (independent, no prompt).
255151
+ refreshPermissions();
255042
255152
  try {
255043
255153
  setState((prev) => ({ ...prev, status: 'checking' }));
255044
- const result = await invoke(GHOST_OS_TAURI_COMMANDS.CHECK_GHOST_OS_STATUS);
255154
+ const result = await invoke(CUA_DRIVER_TAURI_COMMANDS.CHECK_STATUS);
255045
255155
  setStatus(result);
255046
255156
  setState((prev) => ({
255047
255157
  ...prev,
@@ -255051,13 +255161,13 @@ function useGhostOs() {
255051
255161
  }));
255052
255162
  }
255053
255163
  catch (error) {
255054
- console.error('[useGhostOs] Failed to check status:', error);
255164
+ console.error('[useCuaDriver] Failed to check status:', error);
255055
255165
  setState((prev) => ({ ...prev, status: 'idle' }));
255056
255166
  }
255057
- }, [isAvailable, invoke, setState, refreshAccessibilityPermission]);
255167
+ }, [isAvailable, invoke, setState, refreshPermissions]);
255058
255168
  /**
255059
- * Install and set up GhostOS. The host clones the repo + runs setup and emits
255060
- * progress/log/status events along the way.
255169
+ * Download + install the pinned driver. The host emits progress/log events
255170
+ * along the way, and refuses up front on an unsupported desktop session.
255061
255171
  */
255062
255172
  const install = useCallback(async () => {
255063
255173
  if (!isAvailable)
@@ -255076,64 +255186,73 @@ function useGhostOs() {
255076
255186
  level: 'info',
255077
255187
  message: 'Starting setup…',
255078
255188
  });
255079
- const result = await invoke(GHOST_OS_TAURI_COMMANDS.INSTALL_GHOST_OS);
255189
+ const result = await invoke(CUA_DRIVER_TAURI_COMMANDS.INSTALL);
255080
255190
  addLog({
255081
255191
  timestamp: new Date().toISOString(),
255082
255192
  level: 'info',
255083
255193
  message: typeof result === 'string' && result ? result : 'Setup complete',
255084
- });
255085
- setState((prev) => ({
255086
- ...prev,
255194
+ }, {
255087
255195
  status: 'completed',
255088
255196
  progress: 100,
255089
255197
  message: 'Your assistant is ready',
255090
- lastUpdated: new Date().toISOString(),
255091
- }));
255198
+ });
255092
255199
  await checkStatus();
255093
255200
  }
255094
255201
  catch (error) {
255095
255202
  const message = error instanceof Error ? error.message : String(error);
255096
- setState((prev) => ({
255097
- ...prev,
255098
- status: 'error',
255099
- error: message,
255100
- message: 'Setup didn’t finish',
255101
- lastUpdated: new Date().toISOString(),
255102
- }));
255103
255203
  addLog({
255104
255204
  timestamp: new Date().toISOString(),
255105
255205
  level: 'error',
255106
255206
  message: `Install failed: ${message}`,
255107
- });
255207
+ }, { status: 'error', error: message, message: 'Setup didn’t finish' });
255108
255208
  }
255109
255209
  }, [isAvailable, invoke, addLog, checkStatus, setState]);
255110
255210
  /**
255111
- * Stop the running GhostOS manager.
255211
+ * Stop the driver's MCP process. The host announces the exit, which makes the
255212
+ * chat layer drop its cached MCP session and reconnect on the next turn.
255112
255213
  */
255113
255214
  const stop = useCallback(async () => {
255114
255215
  if (!isAvailable)
255115
255216
  return;
255116
255217
  try {
255117
- await invoke(GHOST_OS_TAURI_COMMANDS.STOP_GHOST_OS);
255218
+ await invoke(CUA_DRIVER_TAURI_COMMANDS.STOP);
255118
255219
  await checkStatus();
255119
255220
  }
255120
255221
  catch (error) {
255121
- console.error('[useGhostOs] Failed to stop GhostOS:', error);
255222
+ console.error('[useCuaDriver] Failed to stop the driver:', error);
255122
255223
  }
255123
255224
  }, [isAvailable, invoke, checkStatus]);
255124
255225
  /**
255125
255226
  * Reset the install state (for retrying after an error).
255126
255227
  */
255127
255228
  const resetState = useCallback(() => {
255128
- setState(initialGhostOsInstallState);
255229
+ setState(initialCuaDriverInstallState);
255129
255230
  }, [setState]);
255231
+ // Latest handlers, read at event time. Keeping them OUT of the effect's
255232
+ // dependencies is the point: anything derived from `setState` can change
255233
+ // identity as state moves, and re-subscribing per state change is what turned
255234
+ // a handful of install events into a runaway cascade.
255235
+ const handlersRef = useRef({ addLog, setState });
255236
+ handlersRef.current = { addLog, setState };
255130
255237
  // Wire up host event listeners (progress / logs / status).
255131
255238
  useEffect(() => {
255132
255239
  if (!isAvailable)
255133
255240
  return;
255241
+ // `listen()` is async, so a teardown can land before the subscription
255242
+ // resolves. Registering the handle unconditionally would leave a live
255243
+ // listener behind with nothing holding its unsubscribe — the leak that made
255244
+ // one install log event fan out and freeze the webview for good.
255245
+ let cancelled = false;
255246
+ const handles = [];
255247
+ const track = (unlisten) => {
255248
+ if (cancelled)
255249
+ unlisten();
255250
+ else
255251
+ handles.push(unlisten);
255252
+ };
255134
255253
  const setupListeners = async () => {
255135
255254
  try {
255136
- const unlistenProgress = await listen(GHOST_OS_TAURI_EVENTS.INSTALL_PROGRESS, (payload) => {
255255
+ track(await listen(CUA_DRIVER_TAURI_EVENTS.INSTALL_PROGRESS, (payload) => {
255137
255256
  const nextStatus = payload.status === 'completed'
255138
255257
  ? 'completed'
255139
255258
  : payload.status === 'cancelled'
@@ -255141,30 +255260,28 @@ function useGhostOs() {
255141
255260
  : payload.status === 'error'
255142
255261
  ? 'error'
255143
255262
  : 'installing';
255144
- setState((prev) => ({
255263
+ handlersRef.current.setState((prev) => ({
255145
255264
  ...prev,
255146
255265
  status: nextStatus,
255147
255266
  progress: payload.percentage,
255148
255267
  message: payload.message,
255149
255268
  lastUpdated: new Date().toISOString(),
255150
255269
  }));
255151
- });
255152
- unlistenRefs.current.push(unlistenProgress);
255153
- const unlistenLogs = await listen(GHOST_OS_TAURI_EVENTS.INSTALLATION_LOG, addLog);
255154
- unlistenRefs.current.push(unlistenLogs);
255155
- const unlistenStatus = await listen(GHOST_OS_TAURI_EVENTS.STATUS, setStatus);
255156
- unlistenRefs.current.push(unlistenStatus);
255270
+ }));
255271
+ track(await listen(CUA_DRIVER_TAURI_EVENTS.INSTALLATION_LOG, (log) => handlersRef.current.addLog(log)));
255272
+ track(await listen(CUA_DRIVER_TAURI_EVENTS.STATUS, setStatus));
255157
255273
  }
255158
255274
  catch (error) {
255159
- console.error('[useGhostOs] Failed to setup event listeners:', error);
255275
+ console.error('[useCuaDriver] Failed to setup event listeners:', error);
255160
255276
  }
255161
255277
  };
255162
255278
  setupListeners();
255163
255279
  return () => {
255164
- unlistenRefs.current.forEach((unlisten) => unlisten());
255165
- unlistenRefs.current = [];
255280
+ cancelled = true;
255281
+ handles.splice(0).forEach((unlisten) => unlisten());
255166
255282
  };
255167
- }, [isAvailable, listen, setState, addLog]);
255283
+ // Only re-subscribe if the host itself changes — never on state.
255284
+ }, [isAvailable, listen]);
255168
255285
  // Check status once on mount (in the desktop app).
255169
255286
  useEffect(() => {
255170
255287
  if (!isAvailable || hasCheckedStatus.current)
@@ -255174,40 +255291,65 @@ function useGhostOs() {
255174
255291
  }, [isAvailable, checkStatus]);
255175
255292
  // Only expose the feature as available inside the desktop (Tauri) app.
255176
255293
  const finalIsAvailable = isAvailable && isTauriApp$1();
255294
+ // Deliberately `=== true`, not `?? true`: until the first status check lands
255295
+ // we do not know whether this desktop session is one the driver can drive, and
255296
+ // offering Cowork optimistically would show the toggle to (say) a KDE user for
255297
+ // a beat before snatching it away. One IPC round-trip on mount is cheaper than
255298
+ // that flicker, and errs towards not promising what we cannot deliver.
255299
+ const isSupported = (status === null || status === void 0 ? void 0 : status.supported) === true;
255300
+ const unsupportedReason = (status === null || status === void 0 ? void 0 : status.supported) === false ? status.reason : undefined;
255177
255301
  return {
255178
255302
  isAvailable: finalIsAvailable,
255303
+ /** Session support — gate the Cowork toggle on this, not on `isAvailable`. */
255304
+ isSupported,
255305
+ /** Machine-readable code the UI maps to a translated explanation. */
255306
+ unsupportedReason,
255179
255307
  state,
255180
255308
  status,
255181
255309
  accessibilityPermission,
255310
+ screenRecordingPermission,
255182
255311
  install,
255183
255312
  stop,
255184
255313
  checkStatus,
255185
255314
  resetState,
255186
- requestAccessibilityPermission,
255315
+ requestDriverPermissions,
255187
255316
  };
255188
255317
  }
255189
255318
 
255190
- // Cowork is macOS-only in prod; the env flag bypasses the OS check so the
255191
- // toggle can be exercised on Linux/Windows desktop builds during testing.
255192
- const isMacOS = () => {
255193
- if (typeof navigator === 'undefined')
255194
- return false;
255195
- return /mac/i.test(navigator.userAgent || '');
255196
- };
255197
- const allowNonMacOSCowork = () => process.env.NEXT_PUBLIC_ALLOW_NON_MACOS_COMPUTER_USE_TOGGLE === 'true';
255198
255319
  // 14GB floor per spec; modelSupportsCowork gates size > gb, so a model of
255199
255320
  // exactly 14GB is also off (no 14.0GB model in the catalog). Only the local-model
255200
255321
  // backend needs this; the remote AI has no size requirement.
255201
255322
  const COWORK_MIN_MODEL_GB = 14;
255202
255323
  const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode, artifactsEnabled, containerWidth, disabled = false, onOpenPromptGallery, embedMode = false, promptsIsEnabled = false, memoryEnabled = false, mentorId, tenantKey, username, }) => {
255203
255324
  const t = useT();
255204
- // Cowork = the Tauri GhostOS assistant. GhostOS runs the tools on-device;
255325
+ // Cowork = the Tauri cua-driver assistant. The driver runs the tools on-device;
255205
255326
  // the "brain" is EITHER a large local model (Local Models / Ollama) OR the
255206
- // remote AI (DM OpenAI-compatible endpoint), whichever is set up. useGhostOs
255327
+ // remote AI (DM OpenAI-compatible endpoint), whichever is set up. useCuaDriver
255207
255328
  // handles install/stop; the localStorage pref (isCoworkEnabled) is read
255208
255329
  // by the chat hook to route the turn. Reads the pref on mount.
255209
255330
  // Local state is `coworkOn` so it doesn't shadow the imported setCoworkEnabled.
255210
- const ghostOs = useGhostOs();
255331
+ //
255332
+ // NOTE — this toggle is deliberately UNGATED: it explains nothing and requests
255333
+ // no OS permission. Enabling Cowork gives an LLM synthetic input, screen
255334
+ // capture, and the user's logged-in browser sessions (the driver is spawned
255335
+ // with `--grant existing-profile`). A host that wants a consent step and the
255336
+ // macOS grants must build them and call `useCuaDriver().requestDriverPermissions()`
255337
+ // itself — the OS app's fork of this component does exactly that. See the
255338
+ // useCuaDriver docblock.
255339
+ // The host reports a machine-readable code; never render it raw.
255340
+ const unsupportedCoworkReason = (reason) => {
255341
+ switch (reason) {
255342
+ case 'kde_unproven':
255343
+ return t('chatInputFormInsideButtons.coworkUnsupportedKde');
255344
+ case 'gnome_helper_missing':
255345
+ return t('chatInputFormInsideButtons.coworkUnsupportedGnomeHelper');
255346
+ case 'unsupported_os':
255347
+ return t('chatInputFormInsideButtons.coworkUnsupportedOs');
255348
+ default:
255349
+ return t('chatInputFormInsideButtons.coworkUnsupportedSession');
255350
+ }
255351
+ };
255352
+ const ghostOs = useCuaDriver();
255211
255353
  const [coworkOn, setCoworkOn] = useState(isCoworkEnabled);
255212
255354
  const toggleCowork = () => {
255213
255355
  const next = !coworkOn;
@@ -255218,8 +255360,7 @@ const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode,
255218
255360
  // `disabled` when the user lacks the mentor `#chat` permission — see
255219
255361
  // chat-input-form's isChatDisabledByRbac.)
255220
255362
  if (next) {
255221
- const localReady = isLocalLLMEnabled() &&
255222
- modelSupportsCowork(getLocalLLMModel(), COWORK_MIN_MODEL_GB);
255363
+ const localReady = isLocalLLMEnabled() && modelSupportsCowork(getLocalLLMModel(), COWORK_MIN_MODEL_GB);
255223
255364
  if (!localReady && !hasRemoteAiConfig()) {
255224
255365
  toast.warning(isLocalLLMEnabled()
255225
255366
  ? t('chatInputFormInsideButtons.coworkModelTooSmall')
@@ -255283,7 +255424,15 @@ const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode,
255283
255424
  icon: jsx(Monitor, { className: "h-4 w-4" }),
255284
255425
  isActive: coworkOn,
255285
255426
  action: toggleCowork,
255286
- isEnabled: ghostOs.isAvailable && (isMacOS() || allowNonMacOSCowork()),
255427
+ // Cowork used to be macOS-only because GhostOS was. The Cua Driver runs
255428
+ // on Windows, macOS and Linux — but not on every Linux session, so an
255429
+ // unsupported one renders the pill DISABLED with the reason rather than
255430
+ // hiding it. The chatbox is Cowork's only surface: hide it and a KDE user
255431
+ // is left with no way to find out why the feature is missing.
255432
+ isEnabled: ghostOs.isAvailable,
255433
+ disabledReason: ghostOs.isSupported
255434
+ ? undefined
255435
+ : unsupportedCoworkReason(ghostOs.unsupportedReason),
255287
255436
  },
255288
255437
  ].filter((item) => item.isEnabled);
255289
255438
  // Get visible inside buttons based on screen size
@@ -255321,7 +255470,7 @@ const InsideButtons = ({ activeOptions, onOptionClick, deepResearch, studyMode,
255321
255470
  if (button.name === 'Memory') {
255322
255471
  return (jsx(MemoryButton, { mentorId: mentorId, tenantKey: tenantKey, username: username }, button.name));
255323
255472
  }
255324
- 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
255473
+ 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
255325
255474
  ? 'border border-[#D0E0FF] bg-[#F5F8FF] text-[#38A1E5]'
255326
255475
  : 'text-gray-600 hover:border hover:border-[#D0E0FF] hover:bg-[#F5F8FF]'}`, onClick: (e) => {
255327
255476
  e.preventDefault();