@ottocode/web-sdk 0.1.288 → 0.1.290

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.
Files changed (80) hide show
  1. package/dist/assets/provider-logos.d.ts +1 -1
  2. package/dist/assets/provider-logos.d.ts.map +1 -1
  3. package/dist/components/browser/BrowserViewerPanel.d.ts +25 -0
  4. package/dist/components/browser/BrowserViewerPanel.d.ts.map +1 -1
  5. package/dist/components/chat/ChatInput.d.ts.map +1 -1
  6. package/dist/components/chat/ChatInputKeyHandler.d.ts +6 -0
  7. package/dist/components/chat/ChatInputKeyHandler.d.ts.map +1 -1
  8. package/dist/components/chat/ConfigModal.d.ts.map +1 -1
  9. package/dist/components/chat/DictationInstallPrompt.d.ts +8 -0
  10. package/dist/components/chat/DictationInstallPrompt.d.ts.map +1 -0
  11. package/dist/components/chat/LiveWaveform.d.ts +32 -0
  12. package/dist/components/chat/LiveWaveform.d.ts.map +1 -0
  13. package/dist/components/chat/ReasoningTabs.d.ts +13 -0
  14. package/dist/components/chat/ReasoningTabs.d.ts.map +1 -0
  15. package/dist/components/chat/ShortcutsModal.d.ts.map +1 -1
  16. package/dist/components/chat/SkillMentionPopup.d.ts +12 -0
  17. package/dist/components/chat/SkillMentionPopup.d.ts.map +1 -0
  18. package/dist/components/git/GitFileTree.d.ts.map +1 -1
  19. package/dist/components/index.d.ts +3 -0
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/components/index.js +9619 -7188
  22. package/dist/components/index.js.map +53 -43
  23. package/dist/components/messages/UserMessageGroup.d.ts.map +1 -1
  24. package/dist/components/onboarding/OnboardingModal.d.ts.map +1 -1
  25. package/dist/components/onboarding/steps/ProviderSetupStep.d.ts +10 -0
  26. package/dist/components/onboarding/steps/ProviderSetupStep.d.ts.map +1 -1
  27. package/dist/components/settings/DictationSettings.d.ts +6 -0
  28. package/dist/components/settings/DictationSettings.d.ts.map +1 -0
  29. package/dist/components/settings/SettingsSidebar.d.ts.map +1 -1
  30. package/dist/components/ui/Modal.d.ts +1 -1
  31. package/dist/components/ui/Modal.d.ts.map +1 -1
  32. package/dist/components/workspace/ViewerTabs.d.ts.map +1 -1
  33. package/dist/hooks/index.d.ts +3 -1
  34. package/dist/hooks/index.d.ts.map +1 -1
  35. package/dist/hooks/index.js +713 -210
  36. package/dist/hooks/index.js.map +20 -17
  37. package/dist/hooks/useAuthStatus.d.ts +5 -0
  38. package/dist/hooks/useAuthStatus.d.ts.map +1 -1
  39. package/dist/hooks/useConfig.d.ts +24 -0
  40. package/dist/hooks/useConfig.d.ts.map +1 -1
  41. package/dist/hooks/useDictationModels.d.ts +23 -0
  42. package/dist/hooks/useDictationModels.d.ts.map +1 -0
  43. package/dist/hooks/useOttoRouterPayments.d.ts +2 -0
  44. package/dist/hooks/useOttoRouterPayments.d.ts.map +1 -0
  45. package/dist/hooks/usePreferences.d.ts +2 -3
  46. package/dist/hooks/usePreferences.d.ts.map +1 -1
  47. package/dist/hooks/useSkillMention.d.ts +16 -0
  48. package/dist/hooks/useSkillMention.d.ts.map +1 -0
  49. package/dist/hooks/useTheme.d.ts +1 -1
  50. package/dist/hooks/useTheme.d.ts.map +1 -1
  51. package/dist/hooks/useVoiceInput.d.ts +24 -0
  52. package/dist/hooks/useVoiceInput.d.ts.map +1 -0
  53. package/dist/index.js +9668 -7250
  54. package/dist/index.js.map +55 -45
  55. package/dist/lib/api-client/auth.d.ts +10 -0
  56. package/dist/lib/api-client/auth.d.ts.map +1 -1
  57. package/dist/lib/api-client/config.d.ts +18 -0
  58. package/dist/lib/api-client/config.d.ts.map +1 -1
  59. package/dist/lib/api-client/dictation.d.ts +74 -0
  60. package/dist/lib/api-client/dictation.d.ts.map +1 -0
  61. package/dist/lib/api-client/index.d.ts +35 -0
  62. package/dist/lib/api-client/index.d.ts.map +1 -1
  63. package/dist/lib/api-client/utils.d.ts +1 -0
  64. package/dist/lib/api-client/utils.d.ts.map +1 -1
  65. package/dist/lib/config.d.ts +6 -0
  66. package/dist/lib/config.d.ts.map +1 -1
  67. package/dist/lib/index.d.ts +1 -1
  68. package/dist/lib/index.d.ts.map +1 -1
  69. package/dist/lib/index.js +186 -29
  70. package/dist/lib/index.js.map +10 -9
  71. package/dist/lib/skillMentions.d.ts +8 -0
  72. package/dist/lib/skillMentions.d.ts.map +1 -0
  73. package/dist/stores/index.d.ts +1 -0
  74. package/dist/stores/index.d.ts.map +1 -1
  75. package/dist/stores/index.js +42 -2
  76. package/dist/stores/index.js.map +6 -5
  77. package/dist/stores/skillsStore.d.ts.map +1 -1
  78. package/package.json +3 -3
  79. package/dist/hooks/useSetuPayments.d.ts +0 -2
  80. package/dist/hooks/useSetuPayments.d.ts.map +0 -1
@@ -21,20 +21,83 @@ import {
21
21
  import { client } from "@ottocode/api";
22
22
 
23
23
  // src/lib/config.ts
24
- function computeApiBaseUrl() {
25
- const envUrl = import.meta.env?.VITE_API_BASE_URL;
26
- if (envUrl) {
27
- return envUrl;
24
+ var RUNTIME_API_BASE_URL_STORAGE_KEY = "otto-api-base-url";
25
+ function normalizeApiBaseUrl(value) {
26
+ const trimmed = value.trim();
27
+ if (!trimmed) {
28
+ throw new Error("Enter an otto tunnel URL.");
29
+ }
30
+ let url;
31
+ try {
32
+ url = new URL(trimmed);
33
+ } catch {
34
+ throw new Error("Enter a valid URL, including http:// or https://.");
35
+ }
36
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
37
+ throw new Error("Tunnel URL must start with http:// or https://.");
38
+ }
39
+ url.hash = "";
40
+ url.pathname = url.pathname.replace(/\/+$/, "") || "";
41
+ return url.toString().replace(/\/+$/, "");
42
+ }
43
+ function getStoredApiBaseUrl() {
44
+ if (typeof window === "undefined")
45
+ return;
46
+ try {
47
+ return window.localStorage.getItem(RUNTIME_API_BASE_URL_STORAGE_KEY) ?? undefined;
48
+ } catch {
49
+ return;
28
50
  }
51
+ }
52
+ function getConfiguredRuntimeApiBaseUrl() {
53
+ if (typeof window === "undefined")
54
+ return;
55
+ const params = new URLSearchParams(window.location.search);
56
+ const urlParam = params.get("url");
57
+ if (urlParam)
58
+ return normalizeApiBaseUrl(urlParam);
59
+ const win = window;
60
+ if (win.OTTO_SERVER_URL)
61
+ return normalizeApiBaseUrl(win.OTTO_SERVER_URL);
62
+ if (win.__OTTO_API_URL__)
63
+ return normalizeApiBaseUrl(win.__OTTO_API_URL__);
64
+ const storedUrl = getStoredApiBaseUrl();
65
+ if (storedUrl)
66
+ return normalizeApiBaseUrl(storedUrl);
67
+ return;
68
+ }
69
+ function hasConfiguredRuntimeApiBaseUrl() {
70
+ return Boolean(getConfiguredRuntimeApiBaseUrl());
71
+ }
72
+ function setRuntimeApiBaseUrl(value) {
73
+ const baseUrl = normalizeApiBaseUrl(value);
29
74
  if (typeof window !== "undefined") {
30
75
  const win = window;
31
- if (win.OTTO_SERVER_URL) {
32
- return win.OTTO_SERVER_URL;
33
- }
34
- if (win.__OTTO_API_URL__) {
35
- return win.__OTTO_API_URL__;
36
- }
76
+ win.OTTO_SERVER_URL = baseUrl;
77
+ try {
78
+ window.localStorage.setItem(RUNTIME_API_BASE_URL_STORAGE_KEY, baseUrl);
79
+ } catch {}
37
80
  }
81
+ return baseUrl;
82
+ }
83
+ function clearRuntimeApiBaseUrl() {
84
+ if (typeof window === "undefined")
85
+ return;
86
+ const win = window;
87
+ delete win.OTTO_SERVER_URL;
88
+ delete win.__OTTO_API_URL__;
89
+ try {
90
+ window.localStorage.removeItem(RUNTIME_API_BASE_URL_STORAGE_KEY);
91
+ } catch {}
92
+ }
93
+ function computeApiBaseUrl() {
94
+ const runtimeUrl = getConfiguredRuntimeApiBaseUrl();
95
+ if (runtimeUrl) {
96
+ return runtimeUrl;
97
+ }
98
+ const envUrl = import.meta.env?.VITE_API_BASE_URL;
99
+ if (envUrl)
100
+ return normalizeApiBaseUrl(envUrl);
38
101
  return "http://localhost:9100";
39
102
  }
40
103
  function getRuntimeApiBaseUrl() {
@@ -77,8 +140,7 @@ function extractErrorMessage(error) {
77
140
  return "Unknown error";
78
141
  }
79
142
  function configureApiClient() {
80
- const win = window;
81
- const baseURL = win.OTTO_SERVER_URL || API_BASE_URL;
143
+ const baseURL = getRuntimeApiBaseUrl();
82
144
  client.setConfig({
83
145
  baseURL,
84
146
  adapter: getClientAdapter()
@@ -86,10 +148,7 @@ function configureApiClient() {
86
148
  }
87
149
  configureApiClient();
88
150
  function getBaseUrl() {
89
- const win = window;
90
- if (win.OTTO_SERVER_URL)
91
- return win.OTTO_SERVER_URL;
92
- return API_BASE_URL;
151
+ return getRuntimeApiBaseUrl();
93
152
  }
94
153
  function convertSession(apiSession) {
95
154
  return {
@@ -611,9 +670,9 @@ var approvalMixin = {
611
670
 
612
671
  // src/lib/api-client/ottorouter.ts
613
672
  import {
614
- getOttoRouterBalance as apiGetSetuBalance,
615
- getOttoRouterWallet as apiGetSetuWallet,
616
- getOttoRouterUsdcBalance as apiGetSetuUsdcBalance,
673
+ getOttoRouterBalance as apiGetOttoRouterBalance,
674
+ getOttoRouterWallet as apiGetOttoRouterWallet,
675
+ getOttoRouterUsdcBalance as apiGetOttoRouterUsdcBalance,
617
676
  createPolarCheckout as apiCreatePolarCheckout,
618
677
  getPolarTopupEstimate as apiGetPolarTopupEstimate,
619
678
  getPolarTopupStatus as apiGetPolarTopupStatus,
@@ -627,7 +686,7 @@ import {
627
686
  var ottorouterMixin = {
628
687
  async getOttoRouterBalance() {
629
688
  try {
630
- const response = await apiGetSetuBalance();
689
+ const response = await apiGetOttoRouterBalance();
631
690
  if (response.error)
632
691
  return null;
633
692
  return response.data;
@@ -637,7 +696,7 @@ var ottorouterMixin = {
637
696
  },
638
697
  async getOttoRouterWallet() {
639
698
  try {
640
- const response = await apiGetSetuWallet();
699
+ const response = await apiGetOttoRouterWallet();
641
700
  if (response.error)
642
701
  return { configured: false };
643
702
  return response.data;
@@ -647,7 +706,7 @@ var ottorouterMixin = {
647
706
  },
648
707
  async getOttoRouterUsdcBalance(network = "mainnet") {
649
708
  try {
650
- const response = await apiGetSetuUsdcBalance({
709
+ const response = await apiGetOttoRouterUsdcBalance({
651
710
  query: { network }
652
711
  });
653
712
  if (response.error)
@@ -750,14 +809,16 @@ var ottorouterMixin = {
750
809
  // src/lib/api-client/auth.ts
751
810
  import {
752
811
  getAuthStatus as apiGetAuthStatus,
753
- setupOttoRouterWallet as apiSetupSetuWallet,
754
- importOttoRouterWallet as apiImportSetuWallet,
755
- exportOttoRouterWallet as apiExportSetuWallet,
812
+ setupOttoRouterWallet as apiSetupOttoRouterWallet,
813
+ importOttoRouterWallet as apiImportOttoRouterWallet,
814
+ exportOttoRouterWallet as apiExportOttoRouterWallet,
756
815
  addProviderApiKey as apiAddProviderApiKey,
757
816
  removeProvider as apiRemoveProvider,
758
817
  completeOnboarding as apiCompleteOnboarding,
759
818
  getOAuthUrl as apiGetOAuthUrl,
760
819
  exchangeOAuthCode as apiExchangeOAuthCode,
820
+ startOpenAiDeviceFlow as apiStartOpenAiDeviceFlow,
821
+ pollOpenAiDeviceFlow as apiPollOpenAiDeviceFlow,
761
822
  startCopilotDeviceFlow as apiStartCopilotDeviceFlow,
762
823
  pollCopilotDeviceFlow as apiPollCopilotDeviceFlow,
763
824
  getCopilotAuthMethods as apiGetCopilotAuthMethods,
@@ -774,13 +835,13 @@ var authMixin = {
774
835
  return response.data;
775
836
  },
776
837
  async setupOttoRouterWallet() {
777
- const response = await apiSetupSetuWallet();
838
+ const response = await apiSetupOttoRouterWallet();
778
839
  if (response.error)
779
840
  throw new Error(extractErrorMessage(response.error));
780
841
  return response.data;
781
842
  },
782
843
  async importOttoRouterWallet(privateKey) {
783
- const response = await apiImportSetuWallet({
844
+ const response = await apiImportOttoRouterWallet({
784
845
  body: { privateKey }
785
846
  });
786
847
  if (response.error)
@@ -788,7 +849,7 @@ var authMixin = {
788
849
  return response.data;
789
850
  },
790
851
  async exportOttoRouterWallet() {
791
- const response = await apiExportSetuWallet();
852
+ const response = await apiExportOttoRouterWallet();
792
853
  if (response.error)
793
854
  throw new Error(extractErrorMessage(response.error));
794
855
  return response.data;
@@ -838,6 +899,20 @@ var authMixin = {
838
899
  throw new Error(extractErrorMessage(response.error));
839
900
  return response.data;
840
901
  },
902
+ async startOpenAIDeviceFlow() {
903
+ const response = await apiStartOpenAiDeviceFlow();
904
+ if (response.error)
905
+ throw new Error(extractErrorMessage(response.error));
906
+ return response.data;
907
+ },
908
+ async pollOpenAIDeviceFlow(sessionId) {
909
+ const response = await apiPollOpenAiDeviceFlow({
910
+ body: { sessionId }
911
+ });
912
+ if (response.error)
913
+ throw new Error(extractErrorMessage(response.error));
914
+ return response.data;
915
+ },
841
916
  async startCopilotDeviceFlow() {
842
917
  const response = await apiStartCopilotDeviceFlow();
843
918
  if (response.error)
@@ -960,6 +1035,74 @@ var usageMixin = {
960
1035
  }
961
1036
  };
962
1037
 
1038
+ // src/lib/api-client/dictation.ts
1039
+ import {
1040
+ getDictationStatus as apiGetDictationStatus,
1041
+ installDictationModel as apiInstallDictationModel,
1042
+ listDictationModels as apiListDictationModels,
1043
+ removeDictationModel as apiRemoveDictationModel,
1044
+ createDictationSession as apiCreateDictationSession
1045
+ } from "@ottocode/api";
1046
+ function coerceModelState(value) {
1047
+ return value;
1048
+ }
1049
+ function coerceModels(value) {
1050
+ return Array.isArray(value) ? value.map(coerceModelState) : [];
1051
+ }
1052
+ function buildInstallEventsUrl(model) {
1053
+ const baseUrl = getBaseUrl().replace(/\/+$/, "");
1054
+ return `${baseUrl}/v1/dictation/models/${encodeURIComponent(model)}/install/events`;
1055
+ }
1056
+ var dictationMixin = {
1057
+ async getDictationStatus() {
1058
+ const response = await apiGetDictationStatus();
1059
+ if (response.error)
1060
+ throw new Error(extractErrorMessage(response.error));
1061
+ const data = response.data;
1062
+ return {
1063
+ ...data,
1064
+ models: coerceModels(data.models)
1065
+ };
1066
+ },
1067
+ async listDictationModels() {
1068
+ const response = await apiListDictationModels();
1069
+ if (response.error)
1070
+ throw new Error(extractErrorMessage(response.error));
1071
+ const data = response.data;
1072
+ return { models: coerceModels(data.models) };
1073
+ },
1074
+ async installDictationModel({
1075
+ model,
1076
+ force
1077
+ }) {
1078
+ const response = await apiInstallDictationModel({
1079
+ path: { model },
1080
+ body: { force }
1081
+ });
1082
+ if (response.error)
1083
+ throw new Error(extractErrorMessage(response.error));
1084
+ const data = response.data;
1085
+ return { model: coerceModelState(data.model) };
1086
+ },
1087
+ async removeDictationModel(model) {
1088
+ const response = await apiRemoveDictationModel({ path: { model } });
1089
+ if (response.error)
1090
+ throw new Error(extractErrorMessage(response.error));
1091
+ const data = response.data;
1092
+ return {
1093
+ removed: data.removed,
1094
+ model: coerceModelState(data.model)
1095
+ };
1096
+ },
1097
+ async createDictationSession(input = {}) {
1098
+ const response = await apiCreateDictationSession({ body: input });
1099
+ if (response.error)
1100
+ throw new Error(extractErrorMessage(response.error));
1101
+ return response.data;
1102
+ },
1103
+ getDictationModelInstallEventsUrl: buildInstallEventsUrl
1104
+ };
1105
+
963
1106
  // src/lib/api-client/index.ts
964
1107
  class ApiClient {
965
1108
  getSessions = sessionsMixin.getSessions;
@@ -1040,6 +1183,8 @@ class ApiClient {
1040
1183
  getOAuthStartUrl = authMixin.getOAuthStartUrl;
1041
1184
  getOAuthUrl = authMixin.getOAuthUrl;
1042
1185
  exchangeOAuthCode = authMixin.exchangeOAuthCode;
1186
+ startOpenAIDeviceFlow = authMixin.startOpenAIDeviceFlow;
1187
+ pollOpenAIDeviceFlow = authMixin.pollOpenAIDeviceFlow;
1043
1188
  startCopilotDeviceFlow = authMixin.startCopilotDeviceFlow;
1044
1189
  pollCopilotDeviceFlow = authMixin.pollCopilotDeviceFlow;
1045
1190
  getCopilotAuthMethods = authMixin.getCopilotAuthMethods;
@@ -1055,6 +1200,12 @@ class ApiClient {
1055
1200
  updateSkillsConfig = skillsMixin.updateSkillsConfig;
1056
1201
  getUsageStats = usageMixin.getUsageStats;
1057
1202
  getGlobalUsageStats = usageMixin.getGlobalUsageStats;
1203
+ getDictationStatus = dictationMixin.getDictationStatus;
1204
+ listDictationModels = dictationMixin.listDictationModels;
1205
+ installDictationModel = dictationMixin.installDictationModel;
1206
+ removeDictationModel = dictationMixin.removeDictationModel;
1207
+ createDictationSession = dictationMixin.createDictationSession;
1208
+ getDictationModelInstallEventsUrl = dictationMixin.getDictationModelInstallEventsUrl;
1058
1209
  }
1059
1210
  var apiClient = new ApiClient;
1060
1211
 
@@ -1109,7 +1260,7 @@ function useUpdateDefaults() {
1109
1260
  });
1110
1261
  }
1111
1262
  // src/hooks/usePreferences.ts
1112
- import { useCallback, useMemo, useSyncExternalStore } from "react";
1263
+ import { useCallback, useEffect, useMemo } from "react";
1113
1264
 
1114
1265
  // src/lib/platform.ts
1115
1266
  function getPlatformWindow() {
@@ -1174,13 +1325,14 @@ function isPlatformDesktop() {
1174
1325
  }
1175
1326
 
1176
1327
  // src/hooks/usePreferences.ts
1177
- var STORAGE_KEY = "otto-preferences";
1178
1328
  var DEFAULT_FONT_FAMILY = "IBM Plex Mono";
1179
- var DEFAULT_STORED_PREFERENCES = {
1329
+ var DEFAULT_PREFERENCES = {
1180
1330
  vimMode: false,
1181
1331
  compactThread: true,
1182
1332
  fontFamily: DEFAULT_FONT_FAMILY,
1183
- smartEdges: true
1333
+ smartEdges: true,
1334
+ releaseToSend: false,
1335
+ fullWidthContent: false
1184
1336
  };
1185
1337
  function cssFontFamily(fontFamily) {
1186
1338
  const trimmed = fontFamily.trim();
@@ -1202,90 +1354,52 @@ function applyFontFamily(fontFamily) {
1202
1354
  window.parent.postMessage({ type: "otto-font-family-changed", fontFamily }, "*");
1203
1355
  }
1204
1356
  }
1205
- function resolveInitialPreferences() {
1206
- if (typeof window === "undefined") {
1207
- return DEFAULT_STORED_PREFERENCES;
1208
- }
1209
- try {
1210
- const stored = window.localStorage.getItem(STORAGE_KEY);
1211
- if (stored) {
1212
- const parsed = JSON.parse(stored);
1213
- return {
1214
- vimMode: typeof parsed.vimMode === "boolean" ? parsed.vimMode : DEFAULT_STORED_PREFERENCES.vimMode,
1215
- compactThread: typeof parsed.compactThread === "boolean" ? parsed.compactThread : DEFAULT_STORED_PREFERENCES.compactThread,
1216
- fontFamily: typeof parsed.fontFamily === "string" && parsed.fontFamily.trim() ? parsed.fontFamily.trim() : DEFAULT_STORED_PREFERENCES.fontFamily,
1217
- smartEdges: typeof parsed.smartEdges === "boolean" ? parsed.smartEdges : DEFAULT_STORED_PREFERENCES.smartEdges
1218
- };
1219
- }
1220
- } catch (error) {
1221
- console.warn("Failed to load preferences", error);
1222
- }
1223
- return DEFAULT_STORED_PREFERENCES;
1224
- }
1225
- var preferences = resolveInitialPreferences();
1226
- applyFontFamily(preferences.fontFamily);
1227
- var listeners = new Set;
1228
- function getSnapshot() {
1229
- return preferences;
1230
- }
1231
- function getServerSnapshot() {
1232
- return DEFAULT_STORED_PREFERENCES;
1233
- }
1234
- function subscribe(listener) {
1235
- listeners.add(listener);
1236
- return () => listeners.delete(listener);
1237
- }
1238
- function notifyListeners() {
1239
- for (const listener of listeners) {
1240
- listener();
1241
- }
1242
- }
1243
- function updateStore(updates) {
1244
- preferences = { ...preferences, ...updates };
1245
- if (updates.fontFamily !== undefined) {
1246
- applyFontFamily(preferences.fontFamily);
1247
- }
1248
- if (typeof window !== "undefined") {
1249
- try {
1250
- window.localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
1251
- } catch (error) {
1252
- console.warn("Failed to persist preferences", error);
1253
- }
1254
- }
1255
- notifyListeners();
1256
- }
1257
1357
  function usePreferences() {
1258
- const currentPreferences = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
1259
1358
  const { data: config2 } = useConfig();
1260
1359
  const updateDefaults = useUpdateDefaults();
1360
+ const resolvedPreferences = useMemo(() => ({
1361
+ vimMode: config2?.defaults?.vimMode ?? DEFAULT_PREFERENCES.vimMode,
1362
+ compactThread: config2?.defaults?.compactThread ?? DEFAULT_PREFERENCES.compactThread,
1363
+ fontFamily: config2?.defaults?.fontFamily?.trim() || DEFAULT_PREFERENCES.fontFamily,
1364
+ smartEdges: config2?.defaults?.smartEdges ?? DEFAULT_PREFERENCES.smartEdges,
1365
+ releaseToSend: config2?.defaults?.releaseToSend ?? DEFAULT_PREFERENCES.releaseToSend,
1366
+ fullWidthContent: config2?.defaults?.fullWidthContent ?? DEFAULT_PREFERENCES.fullWidthContent
1367
+ }), [
1368
+ config2?.defaults?.vimMode,
1369
+ config2?.defaults?.compactThread,
1370
+ config2?.defaults?.fontFamily,
1371
+ config2?.defaults?.smartEdges,
1372
+ config2?.defaults?.releaseToSend,
1373
+ config2?.defaults?.fullWidthContent
1374
+ ]);
1375
+ useEffect(() => {
1376
+ applyFontFamily(resolvedPreferences.fontFamily);
1377
+ }, [resolvedPreferences.fontFamily]);
1261
1378
  const updatePreferences = useCallback((updates) => {
1262
- const localUpdates = {};
1379
+ const nextUpdates = {};
1263
1380
  if (updates.vimMode !== undefined) {
1264
- localUpdates.vimMode = updates.vimMode;
1381
+ nextUpdates.vimMode = updates.vimMode;
1265
1382
  }
1266
1383
  if (updates.compactThread !== undefined) {
1267
- localUpdates.compactThread = updates.compactThread;
1384
+ nextUpdates.compactThread = updates.compactThread;
1268
1385
  }
1269
1386
  if (updates.fontFamily !== undefined) {
1270
- localUpdates.fontFamily = updates.fontFamily.trim() || DEFAULT_FONT_FAMILY;
1387
+ nextUpdates.fontFamily = updates.fontFamily.trim() || DEFAULT_FONT_FAMILY;
1271
1388
  }
1272
1389
  if (updates.smartEdges !== undefined) {
1273
- localUpdates.smartEdges = updates.smartEdges;
1390
+ nextUpdates.smartEdges = updates.smartEdges;
1274
1391
  }
1275
- if (Object.keys(localUpdates).length > 0) {
1276
- updateStore(localUpdates);
1392
+ if (updates.releaseToSend !== undefined) {
1393
+ nextUpdates.releaseToSend = updates.releaseToSend;
1277
1394
  }
1278
- if (updates.fullWidthContent !== undefined && updates.fullWidthContent !== config2?.defaults?.fullWidthContent) {
1279
- updateDefaults.mutate({
1280
- fullWidthContent: updates.fullWidthContent,
1281
- scope: "global"
1282
- });
1395
+ if (updates.fullWidthContent !== undefined) {
1396
+ nextUpdates.fullWidthContent = updates.fullWidthContent;
1283
1397
  }
1284
- }, [config2?.defaults?.fullWidthContent, updateDefaults]);
1285
- const resolvedPreferences = useMemo(() => ({
1286
- ...currentPreferences,
1287
- fullWidthContent: config2?.defaults?.fullWidthContent ?? false
1288
- }), [currentPreferences, config2?.defaults?.fullWidthContent]);
1398
+ if (Object.keys(nextUpdates).length === 0) {
1399
+ return;
1400
+ }
1401
+ updateDefaults.mutate({ ...nextUpdates, scope: "global" });
1402
+ }, [updateDefaults]);
1289
1403
  return useMemo(() => ({ preferences: resolvedPreferences, updatePreferences }), [resolvedPreferences, updatePreferences]);
1290
1404
  }
1291
1405
  // src/hooks/useFiles.ts
@@ -1770,7 +1884,14 @@ var useSkillsStore = create2((set, get) => ({
1770
1884
  collapseSidebar: () => set({ isExpanded: false }),
1771
1885
  setSkills: (skills) => set({ skills }),
1772
1886
  setSkillsConfig: ({ skills, globalEnabled, totalCount, enabledCount }) => set({ skills, globalEnabled, totalCount, enabledCount }),
1773
- selectSkill: (name) => set({ selectedSkill: name, isViewerOpen: false, viewingFile: null }),
1887
+ selectSkill: (name) => {
1888
+ if (name) {
1889
+ useViewerTabsStore.getState().openSkillFileTab(name, null);
1890
+ set({ selectedSkill: name, isViewerOpen: true, viewingFile: null });
1891
+ return;
1892
+ }
1893
+ set({ selectedSkill: null, isViewerOpen: false, viewingFile: null });
1894
+ },
1774
1895
  openViewer: (file) => {
1775
1896
  const selectedSkill = get().selectedSkill;
1776
1897
  if (selectedSkill) {
@@ -2562,7 +2683,7 @@ function useSendMessage(sessionId) {
2562
2683
  });
2563
2684
  }
2564
2685
  // src/hooks/useSessionStream.ts
2565
- import { useEffect, useRef } from "react";
2686
+ import { useEffect as useEffect2, useRef } from "react";
2566
2687
  import { useQueryClient as useQueryClient5 } from "@tanstack/react-query";
2567
2688
 
2568
2689
  // src/lib/sse-client.ts
@@ -2717,7 +2838,7 @@ function useSessionStream(sessionId, enabled = true) {
2717
2838
  updatePendingApproval,
2718
2839
  setPendingApprovals
2719
2840
  } = useToolApprovalStore();
2720
- useEffect(() => {
2841
+ useEffect2(() => {
2721
2842
  if (!sessionId || !enabled) {
2722
2843
  return;
2723
2844
  }
@@ -4048,7 +4169,7 @@ ${bestEffortUnescapeJsonString(rawTail)}`;
4048
4169
  ]);
4049
4170
  }
4050
4171
  // src/hooks/useClientEvents.ts
4051
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
4172
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
4052
4173
  import { useQueryClient as useQueryClient6 } from "@tanstack/react-query";
4053
4174
  import {
4054
4175
  buildClientEventsStreamUrl,
@@ -4298,10 +4419,10 @@ async function maybeShowLocalAccessToast(baseUrl) {
4298
4419
  function useClientEvents(activeSessionId) {
4299
4420
  const queryClient = useQueryClient6();
4300
4421
  const activeSessionIdRef = useRef2(activeSessionId);
4301
- useEffect2(() => {
4422
+ useEffect3(() => {
4302
4423
  activeSessionIdRef.current = activeSessionId;
4303
4424
  }, [activeSessionId]);
4304
- useEffect2(() => {
4425
+ useEffect3(() => {
4305
4426
  if (typeof window === "undefined" || window.parent !== window)
4306
4427
  return;
4307
4428
  if (!("Notification" in window))
@@ -4330,7 +4451,7 @@ function useClientEvents(activeSessionId) {
4330
4451
  }
4331
4452
  });
4332
4453
  }, []);
4333
- useEffect2(() => {
4454
+ useEffect3(() => {
4334
4455
  const controller = new AbortController;
4335
4456
  const baseUrl = getBaseUrl();
4336
4457
  createClientEventsStream({
@@ -4380,24 +4501,15 @@ function useClientEvents(activeSessionId) {
4380
4501
  return buildClientEventsStreamUrl({ baseUrl: getBaseUrl() });
4381
4502
  }
4382
4503
  // src/hooks/useTheme.ts
4383
- import { useEffect as useEffect3, useState, useCallback as useCallback2, useMemo as useMemo3 } from "react";
4384
- var STORAGE_KEY2 = "otto-theme";
4385
- function resolveInitialTheme() {
4386
- if (typeof window === "undefined") {
4387
- return "dark";
4388
- }
4389
- const stored = window.localStorage.getItem(STORAGE_KEY2);
4390
- if (stored === "light" || stored === "dark") {
4391
- return stored;
4392
- }
4393
- if (window.matchMedia?.("(prefers-color-scheme: light)").matches) {
4394
- return "light";
4395
- }
4396
- return "dark";
4504
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3 } from "react";
4505
+ function normalizeTheme(theme) {
4506
+ return theme === "light" ? "light" : "dark";
4397
4507
  }
4398
4508
  function useTheme() {
4399
- const [theme, setTheme] = useState(() => resolveInitialTheme());
4400
- useEffect3(() => {
4509
+ const { data: config2 } = useConfig();
4510
+ const updateDefaults = useUpdateDefaults();
4511
+ const theme = normalizeTheme(config2?.defaults?.theme);
4512
+ useEffect4(() => {
4401
4513
  if (typeof document === "undefined")
4402
4514
  return;
4403
4515
  const root = document.documentElement;
@@ -4406,37 +4518,24 @@ function useTheme() {
4406
4518
  } else {
4407
4519
  root.classList.remove("dark");
4408
4520
  }
4409
- try {
4410
- window.localStorage.setItem(STORAGE_KEY2, theme);
4411
- } catch (error) {
4412
- console.warn("Failed to persist theme preference", error);
4413
- }
4414
4521
  if (window.parent && window.parent !== window) {
4415
4522
  window.parent.postMessage({ type: "otto-set-theme", theme }, "*");
4416
4523
  }
4417
4524
  }, [theme]);
4418
- useEffect3(() => {
4419
- if (typeof window === "undefined")
4420
- return;
4421
- const handler = (e) => {
4422
- if (e.data?.type === "otto-set-theme" && (e.data.theme === "light" || e.data.theme === "dark")) {
4423
- setTheme(e.data.theme);
4424
- }
4425
- };
4426
- window.addEventListener("message", handler);
4427
- return () => window.removeEventListener("message", handler);
4428
- }, []);
4525
+ const setTheme = useCallback2((nextTheme) => {
4526
+ updateDefaults.mutate({ theme: nextTheme, scope: "global" });
4527
+ }, [updateDefaults]);
4429
4528
  const toggleTheme = useCallback2(() => {
4430
- setTheme((prev) => prev === "dark" ? "light" : "dark");
4431
- }, []);
4432
- return useMemo3(() => ({ theme, setTheme, toggleTheme }), [theme, toggleTheme]);
4529
+ setTheme(theme === "dark" ? "light" : "dark");
4530
+ }, [setTheme, theme]);
4531
+ return useMemo3(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme]);
4433
4532
  }
4434
4533
  // src/hooks/useWorkingDirectory.ts
4435
- import { useEffect as useEffect4, useState as useState2 } from "react";
4534
+ import { useEffect as useEffect5, useState } from "react";
4436
4535
  import { getCwd } from "@ottocode/api";
4437
4536
  function useWorkingDirectory() {
4438
- const [dirName, setDirName] = useState2(null);
4439
- useEffect4(() => {
4537
+ const [dirName, setDirName] = useState(null);
4538
+ useEffect5(() => {
4440
4539
  const fetchWorkingDirectory = async () => {
4441
4540
  try {
4442
4541
  const response = await getCwd({ baseURL: getBaseUrl() });
@@ -4460,7 +4559,7 @@ function useWorkingDirectory() {
4460
4559
  return dirName;
4461
4560
  }
4462
4561
  // src/hooks/useKeyboardShortcuts.ts
4463
- import { useEffect as useEffect5, useCallback as useCallback3 } from "react";
4562
+ import { useEffect as useEffect6, useCallback as useCallback3 } from "react";
4464
4563
 
4465
4564
  // src/stores/focusStore.ts
4466
4565
  import { create as create12 } from "zustand";
@@ -4821,7 +4920,7 @@ function useKeyboardShortcuts({
4821
4920
  onReturnToInput,
4822
4921
  closeDiff
4823
4922
  ]);
4824
- useEffect5(() => {
4923
+ useEffect6(() => {
4825
4924
  window.addEventListener("keydown", handleKeyDown, true);
4826
4925
  return () => window.removeEventListener("keydown", handleKeyDown, true);
4827
4926
  }, [handleKeyDown]);
@@ -4833,9 +4932,9 @@ function useKeyboardShortcuts({
4833
4932
  }
4834
4933
  // src/hooks/useImageUpload.ts
4835
4934
  import {
4836
- useState as useState3,
4935
+ useState as useState2,
4837
4936
  useCallback as useCallback4,
4838
- useEffect as useEffect6
4937
+ useEffect as useEffect7
4839
4938
  } from "react";
4840
4939
  var SUPPORTED_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
4841
4940
  function generateId() {
@@ -4863,9 +4962,9 @@ async function fileToPreview(file) {
4863
4962
  }
4864
4963
  function useImageUpload(options = {}) {
4865
4964
  const { maxImages = 5, maxSizeMB = 5, pageWide = true } = options;
4866
- const [images, setImages] = useState3([]);
4867
- const [isDragging, setIsDragging] = useState3(false);
4868
- const [error, setError] = useState3(null);
4965
+ const [images, setImages] = useState2([]);
4966
+ const [isDragging, setIsDragging] = useState2(false);
4967
+ const [error, setError] = useState2(null);
4869
4968
  const maxSizeBytes = maxSizeMB * 1024 * 1024;
4870
4969
  const validateFile = useCallback4((file) => {
4871
4970
  if (!SUPPORTED_TYPES.includes(file.type)) {
@@ -4971,7 +5070,7 @@ function useImageUpload(options = {}) {
4971
5070
  addImages(imageFiles);
4972
5071
  }
4973
5072
  }, [addImages]);
4974
- useEffect6(() => {
5073
+ useEffect7(() => {
4975
5074
  if (!pageWide)
4976
5075
  return;
4977
5076
  let dragCounter = 0;
@@ -5033,9 +5132,9 @@ function useImageUpload(options = {}) {
5033
5132
  }
5034
5133
  // src/hooks/useFileUpload.ts
5035
5134
  import {
5036
- useState as useState4,
5135
+ useState as useState3,
5037
5136
  useCallback as useCallback5,
5038
- useEffect as useEffect7
5137
+ useEffect as useEffect8
5039
5138
  } from "react";
5040
5139
  var IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
5041
5140
  var PDF_TYPES = ["application/pdf"];
@@ -5165,9 +5264,9 @@ function useFileUpload(options = {}) {
5165
5264
  sessionId,
5166
5265
  onError
5167
5266
  } = options;
5168
- const [files, setFiles] = useState4([]);
5169
- const [isDragging, setIsDragging] = useState4(false);
5170
- const [error, setError] = useState4(null);
5267
+ const [files, setFiles] = useState3([]);
5268
+ const [isDragging, setIsDragging] = useState3(false);
5269
+ const [error, setError] = useState3(null);
5171
5270
  const maxSizeBytes = maxSizeMB * 1024 * 1024;
5172
5271
  const validateFile = useCallback5((file) => {
5173
5272
  if (file.size > maxSizeBytes) {
@@ -5326,7 +5425,7 @@ function useFileUpload(options = {}) {
5326
5425
  addFiles(pastedFiles);
5327
5426
  }
5328
5427
  }, [addFiles]);
5329
- useEffect7(() => {
5428
+ useEffect8(() => {
5330
5429
  if (!pageWide)
5331
5430
  return;
5332
5431
  let dragCounter = 0;
@@ -5599,8 +5698,8 @@ function useExportToSession() {
5599
5698
  }
5600
5699
  });
5601
5700
  }
5602
- // src/hooks/useSetuPayments.ts
5603
- import { useEffect as useEffect8, useRef as useRef3 } from "react";
5701
+ // src/hooks/useOttoRouterPayments.ts
5702
+ import { useEffect as useEffect9, useRef as useRef3 } from "react";
5604
5703
 
5605
5704
  // src/stores/ottorouterStore.ts
5606
5705
  import { create as create17 } from "zustand";
@@ -5644,8 +5743,8 @@ var useTopupApprovalStore = create18((set) => ({
5644
5743
  clearPendingTopup: () => set({ pendingTopup: null, isProcessing: false, selectedMethod: null })
5645
5744
  }));
5646
5745
 
5647
- // src/hooks/useSetuPayments.ts
5648
- function useSetuPayments(sessionId) {
5746
+ // src/hooks/useOttoRouterPayments.ts
5747
+ function useOttoRouterPayments(sessionId) {
5649
5748
  const clientRef = useRef3(null);
5650
5749
  const loadingToastIdRef = useRef3(null);
5651
5750
  const setBalance = useOttoRouterStore((s) => s.setBalance);
@@ -5654,7 +5753,7 @@ function useSetuPayments(sessionId) {
5654
5753
  const updateToast = useToastStore((s) => s.updateToast);
5655
5754
  const setPendingTopup = useTopupApprovalStore((s) => s.setPendingTopup);
5656
5755
  const clearPendingTopup = useTopupApprovalStore((s) => s.clearPendingTopup);
5657
- useEffect8(() => {
5756
+ useEffect9(() => {
5658
5757
  if (!sessionId)
5659
5758
  return;
5660
5759
  const client2 = new SSEClient;
@@ -5784,7 +5883,7 @@ function useSetuPayments(sessionId) {
5784
5883
  ]);
5785
5884
  }
5786
5885
  // src/hooks/useOttoRouterBalance.ts
5787
- import { useEffect as useEffect9, useCallback as useCallback6 } from "react";
5886
+ import { useEffect as useEffect10, useCallback as useCallback6 } from "react";
5788
5887
 
5789
5888
  // src/stores/usageStore.ts
5790
5889
  import { create as create19 } from "zustand";
@@ -5822,19 +5921,19 @@ function useOttoRouterBalance(providerName) {
5822
5921
  }
5823
5922
  setLoading(true);
5824
5923
  try {
5825
- const [setuData, usdcData, walletData] = await Promise.all([
5924
+ const [ottorouterData, usdcData, walletData] = await Promise.all([
5826
5925
  apiClient.getOttoRouterBalance(),
5827
5926
  apiClient.getOttoRouterUsdcBalance(network),
5828
5927
  apiClient.getOttoRouterWallet()
5829
5928
  ]);
5830
- if (setuData) {
5831
- setBalance(setuData.balance);
5832
- setWalletAddress(setuData.walletAddress);
5833
- setScope(setuData.scope ?? null);
5834
- setPayg(setuData.payg ?? null);
5835
- setSubscription(setuData.subscription ?? null);
5836
- setLimits(setuData.limits ?? null);
5837
- const sub = setuData.subscription;
5929
+ if (ottorouterData) {
5930
+ setBalance(ottorouterData.balance);
5931
+ setWalletAddress(ottorouterData.walletAddress);
5932
+ setScope(ottorouterData.scope ?? null);
5933
+ setPayg(ottorouterData.payg ?? null);
5934
+ setSubscription(ottorouterData.subscription ?? null);
5935
+ setLimits(ottorouterData.limits ?? null);
5936
+ const sub = ottorouterData.subscription;
5838
5937
  if (sub?.active && sub.usageWindows) {
5839
5938
  setUsage("ottorouter", {
5840
5939
  provider: "ottorouter",
@@ -5857,7 +5956,7 @@ function useOttoRouterBalance(providerName) {
5857
5956
  }
5858
5957
  if (usdcData) {
5859
5958
  setUsdcBalance(usdcData.usdcBalance);
5860
- if (!setuData && usdcData.walletAddress) {
5959
+ if (!ottorouterData && usdcData.walletAddress) {
5861
5960
  setWalletAddress(usdcData.walletAddress);
5862
5961
  }
5863
5962
  }
@@ -5878,7 +5977,7 @@ function useOttoRouterBalance(providerName) {
5878
5977
  setUsage
5879
5978
  ]);
5880
5979
  const needsUsageWindows = subscription?.active && !subscription.usageWindows;
5881
- useEffect9(() => {
5980
+ useEffect10(() => {
5882
5981
  if (providerName === "ottorouter" && (balance === null || usdcBalance === null || needsUsageWindows)) {
5883
5982
  fetchBalance();
5884
5983
  }
@@ -5899,7 +5998,7 @@ function useShareStatus(sessionId) {
5899
5998
  return { data, isLoading, error };
5900
5999
  }
5901
6000
  // src/hooks/useToolApprovalShortcuts.ts
5902
- import { useEffect as useEffect10, useCallback as useCallback7 } from "react";
6001
+ import { useEffect as useEffect11, useCallback as useCallback7 } from "react";
5903
6002
  function useToolApprovalShortcuts(sessionId) {
5904
6003
  const { pendingApprovals, removePendingApproval } = useToolApprovalStore();
5905
6004
  const sessionPendingApprovals = pendingApprovals;
@@ -5935,7 +6034,7 @@ function useToolApprovalShortcuts(sessionId) {
5935
6034
  console.error("Failed to approve all tool calls:", error);
5936
6035
  }
5937
6036
  }, [sessionId, sessionPendingApprovals, removePendingApproval]);
5938
- useEffect10(() => {
6037
+ useEffect11(() => {
5939
6038
  if (!sessionId || sessionPendingApprovals.length === 0)
5940
6039
  return;
5941
6040
  const handleKeyDown = (e) => {
@@ -5965,19 +6064,19 @@ function useToolApprovalShortcuts(sessionId) {
5965
6064
  ]);
5966
6065
  }
5967
6066
  // src/hooks/useTopupCallback.ts
5968
- import { useEffect as useEffect11, useRef as useRef4 } from "react";
5969
- var STORAGE_KEY3 = "pendingPolarCheckout";
6067
+ import { useEffect as useEffect12, useRef as useRef4 } from "react";
6068
+ var STORAGE_KEY = "pendingPolarCheckout";
5970
6069
  function useTopupCallback() {
5971
6070
  const hasHandled = useRef4(false);
5972
6071
  const loadingToastId = useRef4(null);
5973
6072
  const setBalance = useOttoRouterStore((s) => s.setBalance);
5974
6073
  const removeToast = useToastStore((s) => s.removeToast);
5975
- useEffect11(() => {
6074
+ useEffect12(() => {
5976
6075
  if (hasHandled.current)
5977
6076
  return;
5978
6077
  const params = new URLSearchParams(window.location.search);
5979
6078
  const topupStatus = params.get("topup");
5980
- const pendingCheckoutId = localStorage.getItem(STORAGE_KEY3);
6079
+ const pendingCheckoutId = localStorage.getItem(STORAGE_KEY);
5981
6080
  if (topupStatus === "pending" || pendingCheckoutId) {
5982
6081
  hasHandled.current = true;
5983
6082
  const url = new URL(window.location.href);
@@ -6002,7 +6101,7 @@ function useTopupCallback() {
6002
6101
  try {
6003
6102
  const status = await apiClient.getPolarTopupStatus(pendingCheckoutId);
6004
6103
  if (status?.confirmed) {
6005
- localStorage.removeItem(STORAGE_KEY3);
6104
+ localStorage.removeItem(STORAGE_KEY);
6006
6105
  dismissLoading();
6007
6106
  const balanceData = await apiClient.getOttoRouterBalance();
6008
6107
  if (balanceData?.balance !== undefined) {
@@ -6014,7 +6113,7 @@ function useTopupCallback() {
6014
6113
  if (attempts < maxAttempts) {
6015
6114
  setTimeout(checkStatus, delayMs);
6016
6115
  } else {
6017
- localStorage.removeItem(STORAGE_KEY3);
6116
+ localStorage.removeItem(STORAGE_KEY);
6018
6117
  dismissLoading();
6019
6118
  toast.info("Top-up is still processing. Balance will update automatically.");
6020
6119
  }
@@ -6022,7 +6121,7 @@ function useTopupCallback() {
6022
6121
  if (attempts < maxAttempts) {
6023
6122
  setTimeout(checkStatus, delayMs);
6024
6123
  } else {
6025
- localStorage.removeItem(STORAGE_KEY3);
6124
+ localStorage.removeItem(STORAGE_KEY);
6026
6125
  dismissLoading();
6027
6126
  toast.error("Could not verify top-up. Please check your balance.");
6028
6127
  }
@@ -6033,7 +6132,7 @@ function useTopupCallback() {
6033
6132
  }, [setBalance, removeToast]);
6034
6133
  }
6035
6134
  // src/hooks/useAuthStatus.ts
6036
- import { useEffect as useEffect12, useCallback as useCallback8, useState as useState5, useRef as useRef5 } from "react";
6135
+ import { useEffect as useEffect13, useCallback as useCallback8, useState as useState4, useRef as useRef5 } from "react";
6037
6136
  import { useQueryClient as useQueryClient9 } from "@tanstack/react-query";
6038
6137
 
6039
6138
  // src/stores/onboardingStore.ts
@@ -6085,8 +6184,8 @@ function useAuthStatus() {
6085
6184
  const authStatus = useOnboardingStore((s) => s.authStatus);
6086
6185
  const isOpen = useOnboardingStore((s) => s.isOpen);
6087
6186
  const queryClient = useQueryClient9();
6088
- const [initialized, setInitialized] = useState5(false);
6089
- const [oauthPolling, setOauthPolling] = useState5(false);
6187
+ const [initialized, setInitialized] = useState4(false);
6188
+ const [oauthPolling, setOauthPolling] = useState4(false);
6090
6189
  const oauthPollingRef = useRef5(null);
6091
6190
  const preOauthProvidersRef = useRef5(new Set);
6092
6191
  const fetchAuthStatus = useCallback8(async () => {
@@ -6278,7 +6377,7 @@ function useAuthStatus() {
6278
6377
  setLoading(false);
6279
6378
  }
6280
6379
  }, [fetchAuthStatus, setLoading, setError]);
6281
- useEffect12(() => {
6380
+ useEffect13(() => {
6282
6381
  if (!oauthPolling || !isInIframe)
6283
6382
  return;
6284
6383
  oauthPollingRef.current = setInterval(() => {
@@ -6292,7 +6391,7 @@ function useAuthStatus() {
6292
6391
  clearTimeout(timeout);
6293
6392
  };
6294
6393
  }, [oauthPolling, fetchAuthStatus]);
6295
- useEffect12(() => {
6394
+ useEffect13(() => {
6296
6395
  if (!oauthPolling || !authStatus)
6297
6396
  return;
6298
6397
  const currentConfigured = Object.entries(authStatus.providers).filter(([, p]) => p.configured);
@@ -6301,7 +6400,7 @@ function useAuthStatus() {
6301
6400
  setOauthPolling(false);
6302
6401
  }
6303
6402
  }, [authStatus, oauthPolling]);
6304
- useEffect12(() => {
6403
+ useEffect13(() => {
6305
6404
  const handleOAuthMessage = (event) => {
6306
6405
  if (event.data?.type === "oauth-success") {
6307
6406
  fetchAuthStatus();
@@ -6317,6 +6416,13 @@ function useAuthStatus() {
6317
6416
  }
6318
6417
  return result;
6319
6418
  }, [fetchAuthStatus]);
6419
+ const pollOpenAIDeviceFlow = useCallback8(async (sessionId) => {
6420
+ const result = await apiClient.pollOpenAIDeviceFlow(sessionId);
6421
+ if (result.status === "complete") {
6422
+ await fetchAuthStatus();
6423
+ }
6424
+ return result;
6425
+ }, [fetchAuthStatus]);
6320
6426
  const saveCopilotToken = useCallback8(async (token) => {
6321
6427
  setLoading(true);
6322
6428
  setError(null);
@@ -6362,6 +6468,8 @@ function useAuthStatus() {
6362
6468
  startOAuth,
6363
6469
  startOAuthManual,
6364
6470
  exchangeOAuthCode,
6471
+ startOpenAIDeviceFlow: apiClient.startOpenAIDeviceFlow.bind(apiClient),
6472
+ pollOpenAIDeviceFlow,
6365
6473
  startCopilotDeviceFlow: apiClient.startCopilotDeviceFlow.bind(apiClient),
6366
6474
  pollCopilotDeviceFlow,
6367
6475
  getCopilotAuthMethods: apiClient.getCopilotAuthMethods.bind(apiClient),
@@ -6372,7 +6480,7 @@ function useAuthStatus() {
6372
6480
  }
6373
6481
  // src/hooks/useTunnel.ts
6374
6482
  import { useQuery as useQuery10, useMutation as useMutation7, useQueryClient as useQueryClient10 } from "@tanstack/react-query";
6375
- import { useEffect as useEffect13, useCallback as useCallback9, useRef as useRef6 } from "react";
6483
+ import { useEffect as useEffect14, useCallback as useCallback9, useRef as useRef6 } from "react";
6376
6484
  import {
6377
6485
  client as client2,
6378
6486
  getTunnelQr,
@@ -6415,7 +6523,7 @@ function useTunnelStatus() {
6415
6523
  queryFn: fetchTunnelStatus,
6416
6524
  refetchInterval: 3000
6417
6525
  });
6418
- useEffect13(() => {
6526
+ useEffect14(() => {
6419
6527
  if (query.data) {
6420
6528
  setStatus(query.data.status);
6421
6529
  setUrl(query.data.url);
@@ -6475,7 +6583,7 @@ function useTunnelQr() {
6475
6583
  queryFn: fetchTunnelQr,
6476
6584
  enabled: !!url
6477
6585
  });
6478
- useEffect13(() => {
6586
+ useEffect14(() => {
6479
6587
  if (query.data?.ok && query.data.qrCode) {
6480
6588
  setQrCode(query.data.qrCode);
6481
6589
  }
@@ -6515,7 +6623,7 @@ function useTunnelStream() {
6515
6623
  eventSourceRef.current = null;
6516
6624
  };
6517
6625
  }, [setStatus, setUrl, setError, setProgress]);
6518
- useEffect13(() => {
6626
+ useEffect14(() => {
6519
6627
  if (isExpanded) {
6520
6628
  const cleanup = connect();
6521
6629
  return cleanup;
@@ -6530,7 +6638,7 @@ function useTunnelStream() {
6530
6638
  return { connect };
6531
6639
  }
6532
6640
  // src/hooks/useProviderUsage.ts
6533
- import { useEffect as useEffect14, useCallback as useCallback10, useRef as useRef7 } from "react";
6641
+ import { useEffect as useEffect15, useCallback as useCallback10, useRef as useRef7 } from "react";
6534
6642
  var POLL_INTERVAL = 60000;
6535
6643
  var STALE_THRESHOLD = 60000;
6536
6644
  var inflight = new Set;
@@ -6563,12 +6671,12 @@ function useProviderUsage(provider, authType) {
6563
6671
  }, [provider, isOAuthProvider, setUsage, setLoading, setLastFetched]);
6564
6672
  const fetchRef = useRef7(fetchUsage);
6565
6673
  fetchRef.current = fetchUsage;
6566
- useEffect14(() => {
6674
+ useEffect15(() => {
6567
6675
  if (!provider || !isOAuthProvider)
6568
6676
  return;
6569
6677
  fetchRef.current();
6570
6678
  }, [isOAuthProvider, provider]);
6571
- useEffect14(() => {
6679
+ useEffect15(() => {
6572
6680
  if (!provider || !isOAuthProvider || !isModalOpen || modalProvider !== provider) {
6573
6681
  return;
6574
6682
  }
@@ -6613,7 +6721,7 @@ function useGitDiffFullFile(file, staged = false, enabled = false) {
6613
6721
  }
6614
6722
  // src/hooks/useMCP.ts
6615
6723
  import { useQuery as useQuery12, useMutation as useMutation8, useQueryClient as useQueryClient11 } from "@tanstack/react-query";
6616
- import { useEffect as useEffect15, useRef as useRef8, useCallback as useCallback11 } from "react";
6724
+ import { useEffect as useEffect16, useRef as useRef8, useCallback as useCallback11 } from "react";
6617
6725
  import {
6618
6726
  listMcpServers,
6619
6727
  startMcpServer,
@@ -6635,7 +6743,7 @@ function useMCPServers() {
6635
6743
  },
6636
6744
  refetchInterval: 1e4
6637
6745
  });
6638
- useEffect15(() => {
6746
+ useEffect16(() => {
6639
6747
  if (query.data?.servers) {
6640
6748
  setServers(query.data.servers);
6641
6749
  }
@@ -6786,7 +6894,7 @@ function useCopilotDevicePoller() {
6786
6894
  timerRef.current = null;
6787
6895
  }
6788
6896
  }, []);
6789
- useEffect15(() => {
6897
+ useEffect16(() => {
6790
6898
  if (!copilotDevice) {
6791
6899
  stopPolling();
6792
6900
  return;
@@ -6822,7 +6930,7 @@ function useCopilotDevicePoller() {
6822
6930
  }
6823
6931
  // src/hooks/useSkills.ts
6824
6932
  import { useMutation as useMutation9, useQuery as useQuery13, useQueryClient as useQueryClient12 } from "@tanstack/react-query";
6825
- import { useEffect as useEffect16 } from "react";
6933
+ import { useEffect as useEffect17 } from "react";
6826
6934
  function useSkills() {
6827
6935
  const setSkillsConfig = useSkillsStore((s) => s.setSkillsConfig);
6828
6936
  const query = useQuery13({
@@ -6832,7 +6940,7 @@ function useSkills() {
6832
6940
  },
6833
6941
  refetchInterval: 30000
6834
6942
  });
6835
- useEffect16(() => {
6943
+ useEffect17(() => {
6836
6944
  if (query.data?.items) {
6837
6945
  setSkillsConfig({
6838
6946
  skills: query.data.items,
@@ -6894,10 +7002,10 @@ function useSkillFileContent(name, filePath) {
6894
7002
  });
6895
7003
  }
6896
7004
  // src/hooks/useContainerWidth.ts
6897
- import { useEffect as useEffect17, useState as useState6 } from "react";
7005
+ import { useEffect as useEffect18, useState as useState5 } from "react";
6898
7006
  function useContainerWidth(ref) {
6899
- const [width, setWidth] = useState6(0);
6900
- useEffect17(() => {
7007
+ const [width, setWidth] = useState5(0);
7008
+ useEffect18(() => {
6901
7009
  const el = ref.current;
6902
7010
  if (!el)
6903
7011
  return;
@@ -6909,8 +7017,402 @@ function useContainerWidth(ref) {
6909
7017
  }, [ref]);
6910
7018
  return width;
6911
7019
  }
7020
+ // src/hooks/useVoiceInput.ts
7021
+ import { useCallback as useCallback12, useEffect as useEffect19, useRef as useRef9, useState as useState6 } from "react";
7022
+ var TARGET_SAMPLE_RATE = 16000;
7023
+ var PCM_FRAME_BYTES = 3200;
7024
+ function getAudioContextConstructor() {
7025
+ if (typeof window === "undefined")
7026
+ return null;
7027
+ return window.AudioContext || window.webkitAudioContext || null;
7028
+ }
7029
+ function toLanguageCode(lang) {
7030
+ return lang.split("-", 1)[0]?.toLowerCase() || "en";
7031
+ }
7032
+ function parseServerEvent(raw) {
7033
+ try {
7034
+ const value = JSON.parse(raw);
7035
+ if (!value || typeof value.type !== "string")
7036
+ return null;
7037
+ return value;
7038
+ } catch {
7039
+ return null;
7040
+ }
7041
+ }
7042
+ function resampleLinear(input, inputSampleRate, outputSampleRate) {
7043
+ if (inputSampleRate === outputSampleRate)
7044
+ return input;
7045
+ const ratio = inputSampleRate / outputSampleRate;
7046
+ const outputLength = Math.max(1, Math.floor(input.length / ratio));
7047
+ const output = new Float32Array(outputLength);
7048
+ for (let i = 0;i < outputLength; i++) {
7049
+ const inputIndex = i * ratio;
7050
+ const before = Math.floor(inputIndex);
7051
+ const after = Math.min(before + 1, input.length - 1);
7052
+ const weight = inputIndex - before;
7053
+ output[i] = input[before] * (1 - weight) + input[after] * weight;
7054
+ }
7055
+ return output;
7056
+ }
7057
+ function floatToPcm16(samples) {
7058
+ const buffer = new ArrayBuffer(samples.length * 2);
7059
+ const view = new DataView(buffer);
7060
+ for (let i = 0;i < samples.length; i++) {
7061
+ const sample = Math.max(-1, Math.min(1, samples[i]));
7062
+ view.setInt16(i * 2, sample < 0 ? sample * 32768 : sample * 32767, true);
7063
+ }
7064
+ return buffer;
7065
+ }
7066
+ function appendBuffer(a, b) {
7067
+ const next = new Uint8Array(a.byteLength + b.byteLength);
7068
+ next.set(a, 0);
7069
+ next.set(b, a.byteLength);
7070
+ return next;
7071
+ }
7072
+ function useVoiceInput({
7073
+ onTranscript,
7074
+ onError,
7075
+ lang = "en-US"
7076
+ } = {}) {
7077
+ const [isListening, setIsListening] = useState6(false);
7078
+ const [isTranscribing, setIsTranscribing] = useState6(false);
7079
+ const [analyser, setAnalyser] = useState6(null);
7080
+ const [error, setError] = useState6(null);
7081
+ const streamRef = useRef9(null);
7082
+ const audioContextRef = useRef9(null);
7083
+ const processorRef = useRef9(null);
7084
+ const sourceRef = useRef9(null);
7085
+ const socketRef = useRef9(null);
7086
+ const frameBufferRef = useRef9(new Uint8Array(0));
7087
+ const stoppingRef = useRef9(false);
7088
+ const sessionIdRef = useRef9(null);
7089
+ const onTranscriptRef = useRef9(onTranscript);
7090
+ const onErrorRef = useRef9(onError);
7091
+ useEffect19(() => {
7092
+ onTranscriptRef.current = onTranscript;
7093
+ onErrorRef.current = onError;
7094
+ }, [onTranscript, onError]);
7095
+ const isSupported = typeof window !== "undefined" && !!navigator.mediaDevices?.getUserMedia && !!getAudioContextConstructor() && typeof WebSocket !== "undefined";
7096
+ const emitError = useCallback12((message) => {
7097
+ setError(message);
7098
+ onErrorRef.current?.(message);
7099
+ }, []);
7100
+ const cleanupAudio = useCallback12(() => {
7101
+ if (processorRef.current) {
7102
+ processorRef.current.onaudioprocess = null;
7103
+ processorRef.current.disconnect();
7104
+ processorRef.current = null;
7105
+ }
7106
+ if (sourceRef.current) {
7107
+ sourceRef.current.disconnect();
7108
+ sourceRef.current = null;
7109
+ }
7110
+ if (streamRef.current) {
7111
+ for (const track of streamRef.current.getTracks())
7112
+ track.stop();
7113
+ streamRef.current = null;
7114
+ }
7115
+ if (audioContextRef.current && audioContextRef.current.state !== "closed") {
7116
+ audioContextRef.current.close().catch(() => {});
7117
+ }
7118
+ audioContextRef.current = null;
7119
+ frameBufferRef.current = new Uint8Array(0);
7120
+ setAnalyser(null);
7121
+ }, []);
7122
+ const cleanup = useCallback12(() => {
7123
+ cleanupAudio();
7124
+ if (socketRef.current) {
7125
+ const socket = socketRef.current;
7126
+ socket.onopen = null;
7127
+ socket.onmessage = null;
7128
+ socket.onerror = null;
7129
+ socket.onclose = null;
7130
+ if (socket.readyState === WebSocket.OPEN) {
7131
+ socket.close(1000, "Dictation cleanup");
7132
+ }
7133
+ socketRef.current = null;
7134
+ }
7135
+ sessionIdRef.current = null;
7136
+ stoppingRef.current = false;
7137
+ setIsListening(false);
7138
+ setIsTranscribing(false);
7139
+ }, [cleanupAudio]);
7140
+ const flushFrameBuffer = useCallback12((force = false) => {
7141
+ const socket = socketRef.current;
7142
+ if (!socket || socket.readyState !== WebSocket.OPEN)
7143
+ return;
7144
+ let buffer = frameBufferRef.current;
7145
+ while (buffer.byteLength >= PCM_FRAME_BYTES) {
7146
+ const frame = buffer.slice(0, PCM_FRAME_BYTES);
7147
+ socket.send(frame);
7148
+ buffer = buffer.slice(PCM_FRAME_BYTES);
7149
+ }
7150
+ if (force && buffer.byteLength > 0) {
7151
+ socket.send(buffer);
7152
+ buffer = new Uint8Array(0);
7153
+ }
7154
+ frameBufferRef.current = buffer;
7155
+ }, []);
7156
+ const handleAudioProcess = useCallback12((event) => {
7157
+ const audioContext = audioContextRef.current;
7158
+ const socket = socketRef.current;
7159
+ if (!audioContext || !socket || socket.readyState !== WebSocket.OPEN || stoppingRef.current) {
7160
+ return;
7161
+ }
7162
+ const input = event.inputBuffer.getChannelData(0);
7163
+ const resampled = resampleLinear(input, audioContext.sampleRate, TARGET_SAMPLE_RATE);
7164
+ frameBufferRef.current = appendBuffer(frameBufferRef.current, new Uint8Array(floatToPcm16(resampled)));
7165
+ flushFrameBuffer(false);
7166
+ }, [flushFrameBuffer]);
7167
+ const stop = useCallback12(() => {
7168
+ stoppingRef.current = true;
7169
+ flushFrameBuffer(true);
7170
+ cleanupAudio();
7171
+ const socket = socketRef.current;
7172
+ if (socket?.readyState === WebSocket.OPEN) {
7173
+ setIsListening(false);
7174
+ setIsTranscribing(true);
7175
+ socket.send(JSON.stringify({ type: "stop" }));
7176
+ } else {
7177
+ setIsListening(false);
7178
+ setIsTranscribing(false);
7179
+ }
7180
+ }, [cleanupAudio, flushFrameBuffer]);
7181
+ const start = useCallback12(async () => {
7182
+ if (!isSupported) {
7183
+ emitError("Voice input is not supported in this browser");
7184
+ return;
7185
+ }
7186
+ cleanup();
7187
+ setError(null);
7188
+ setIsTranscribing(false);
7189
+ stoppingRef.current = false;
7190
+ try {
7191
+ const status = await apiClient.getDictationStatus();
7192
+ const model = status.models.find((item) => item.id === status.defaultModel);
7193
+ if (!model?.installed) {
7194
+ emitError("Install a local dictation model from Settings before recording.");
7195
+ return;
7196
+ }
7197
+ const session = await apiClient.createDictationSession({
7198
+ model: status.defaultModel,
7199
+ language: toLanguageCode(lang)
7200
+ });
7201
+ if (!session.modelInstalled) {
7202
+ emitError("Install a local dictation model from Settings before recording.");
7203
+ return;
7204
+ }
7205
+ const socket = new WebSocket(session.wsUrl);
7206
+ socket.binaryType = "arraybuffer";
7207
+ socketRef.current = socket;
7208
+ sessionIdRef.current = session.id;
7209
+ await new Promise((resolve, reject) => {
7210
+ const timeout = window.setTimeout(() => {
7211
+ reject(new Error("Timed out connecting to local dictation"));
7212
+ }, 5000);
7213
+ socket.onopen = () => {
7214
+ window.clearTimeout(timeout);
7215
+ socket.send(JSON.stringify({
7216
+ type: "start",
7217
+ model: session.model,
7218
+ language: toLanguageCode(lang),
7219
+ format: {
7220
+ encoding: "pcm_s16le",
7221
+ sampleRate: TARGET_SAMPLE_RATE,
7222
+ channels: 1
7223
+ },
7224
+ partialResults: false
7225
+ }));
7226
+ resolve();
7227
+ };
7228
+ socket.onerror = () => {
7229
+ window.clearTimeout(timeout);
7230
+ reject(new Error("Could not connect to local dictation"));
7231
+ };
7232
+ });
7233
+ socket.onmessage = (event) => {
7234
+ if (typeof event.data !== "string")
7235
+ return;
7236
+ const payload = parseServerEvent(event.data);
7237
+ if (!payload)
7238
+ return;
7239
+ if (payload.type === "final") {
7240
+ onTranscriptRef.current?.(payload.text.trim(), true);
7241
+ cleanup();
7242
+ return;
7243
+ }
7244
+ if (payload.type === "error") {
7245
+ emitError(payload.message);
7246
+ cleanup();
7247
+ }
7248
+ };
7249
+ socket.onclose = () => {
7250
+ if (!stoppingRef.current)
7251
+ setIsListening(false);
7252
+ setIsTranscribing(false);
7253
+ };
7254
+ const stream = await navigator.mediaDevices.getUserMedia({
7255
+ audio: {
7256
+ echoCancellation: true,
7257
+ noiseSuppression: true,
7258
+ autoGainControl: true
7259
+ }
7260
+ });
7261
+ streamRef.current = stream;
7262
+ const AudioContextCtor = getAudioContextConstructor();
7263
+ if (!AudioContextCtor)
7264
+ throw new Error("AudioContext is unavailable");
7265
+ const audioContext = new AudioContextCtor;
7266
+ const source = audioContext.createMediaStreamSource(stream);
7267
+ const analyserNode = audioContext.createAnalyser();
7268
+ analyserNode.fftSize = 256;
7269
+ analyserNode.smoothingTimeConstant = 0.55;
7270
+ const processor = audioContext.createScriptProcessor(4096, 1, 1);
7271
+ processor.onaudioprocess = handleAudioProcess;
7272
+ source.connect(analyserNode);
7273
+ source.connect(processor);
7274
+ processor.connect(audioContext.destination);
7275
+ audioContextRef.current = audioContext;
7276
+ sourceRef.current = source;
7277
+ processorRef.current = processor;
7278
+ setAnalyser(analyserNode);
7279
+ setIsListening(true);
7280
+ } catch (err) {
7281
+ const name = err instanceof Error ? err.name : "";
7282
+ const msg = name === "NotAllowedError" ? "Microphone permission denied" : err instanceof Error ? err.message : "Could not start voice input";
7283
+ emitError(msg);
7284
+ cleanup();
7285
+ }
7286
+ }, [cleanup, emitError, handleAudioProcess, isSupported, lang]);
7287
+ useEffect19(() => cleanup, [cleanup]);
7288
+ return {
7289
+ isListening,
7290
+ isTranscribing,
7291
+ isSupported,
7292
+ analyser,
7293
+ error,
7294
+ start,
7295
+ stop
7296
+ };
7297
+ }
7298
+ // src/hooks/useDictationModels.ts
7299
+ import { useCallback as useCallback13, useEffect as useEffect20, useRef as useRef10, useState as useState7 } from "react";
7300
+ import { useMutation as useMutation10, useQuery as useQuery14, useQueryClient as useQueryClient13 } from "@tanstack/react-query";
7301
+ var DICTATION_STATUS_QUERY_KEY = ["dictation", "status"];
7302
+ function mergeModelState(current, model) {
7303
+ if (!current)
7304
+ return current;
7305
+ return {
7306
+ ...current,
7307
+ models: current.models.map((item) => item.id === model.id ? model : item)
7308
+ };
7309
+ }
7310
+ function parseInstallEvent(raw) {
7311
+ try {
7312
+ const value = JSON.parse(raw);
7313
+ if (value.type !== "model" || !value.model)
7314
+ return null;
7315
+ return value;
7316
+ } catch {
7317
+ return null;
7318
+ }
7319
+ }
7320
+ function useDictationModels() {
7321
+ const queryClient = useQueryClient13();
7322
+ const eventSourceRef = useRef10(null);
7323
+ const [activeInstallModelId, setActiveInstallModelId] = useState7(null);
7324
+ const [installProgress, setInstallProgress] = useState7(null);
7325
+ const [installStreamError, setInstallStreamError] = useState7(null);
7326
+ const statusQuery = useQuery14({
7327
+ queryKey: DICTATION_STATUS_QUERY_KEY,
7328
+ queryFn: () => apiClient.getDictationStatus(),
7329
+ refetchInterval: (query) => query.state.data?.models.some((model) => model.installing) ? 1000 : 30000
7330
+ });
7331
+ const closeInstallStream = useCallback13(() => {
7332
+ if (eventSourceRef.current) {
7333
+ eventSourceRef.current.close();
7334
+ eventSourceRef.current = null;
7335
+ }
7336
+ }, []);
7337
+ const openInstallStream = useCallback13((modelId) => {
7338
+ if (typeof EventSource === "undefined")
7339
+ return;
7340
+ closeInstallStream();
7341
+ setActiveInstallModelId(modelId);
7342
+ setInstallStreamError(null);
7343
+ const eventSource = new EventSource(apiClient.getDictationModelInstallEventsUrl(modelId));
7344
+ eventSourceRef.current = eventSource;
7345
+ eventSource.onmessage = (event) => {
7346
+ const payload = parseInstallEvent(event.data);
7347
+ if (!payload)
7348
+ return;
7349
+ setInstallProgress(payload.model);
7350
+ queryClient.setQueryData(DICTATION_STATUS_QUERY_KEY, (current) => mergeModelState(current, payload.model));
7351
+ if (!payload.model.installing) {
7352
+ closeInstallStream();
7353
+ setActiveInstallModelId(null);
7354
+ queryClient.invalidateQueries({
7355
+ queryKey: DICTATION_STATUS_QUERY_KEY
7356
+ });
7357
+ }
7358
+ };
7359
+ eventSource.onerror = () => {
7360
+ setInstallStreamError("Lost dictation model install progress stream");
7361
+ closeInstallStream();
7362
+ setActiveInstallModelId(null);
7363
+ queryClient.invalidateQueries({
7364
+ queryKey: DICTATION_STATUS_QUERY_KEY
7365
+ });
7366
+ };
7367
+ }, [closeInstallStream, queryClient]);
7368
+ useEffect20(() => closeInstallStream, [closeInstallStream]);
7369
+ const installMutation = useMutation10({
7370
+ mutationFn: (input) => apiClient.installDictationModel(input),
7371
+ onSuccess: (data) => {
7372
+ setInstallProgress(data.model);
7373
+ queryClient.setQueryData(DICTATION_STATUS_QUERY_KEY, (current) => mergeModelState(current, data.model));
7374
+ if (data.model.installing) {
7375
+ openInstallStream(data.model.id);
7376
+ } else {
7377
+ closeInstallStream();
7378
+ setActiveInstallModelId(null);
7379
+ queryClient.invalidateQueries({
7380
+ queryKey: DICTATION_STATUS_QUERY_KEY
7381
+ });
7382
+ }
7383
+ }
7384
+ });
7385
+ const removeMutation = useMutation10({
7386
+ mutationFn: (model) => apiClient.removeDictationModel(model),
7387
+ onSuccess: (data) => {
7388
+ queryClient.setQueryData(DICTATION_STATUS_QUERY_KEY, (current) => mergeModelState(current, data.model));
7389
+ queryClient.invalidateQueries({
7390
+ queryKey: DICTATION_STATUS_QUERY_KEY
7391
+ });
7392
+ }
7393
+ });
7394
+ const installModel = useCallback13((model, options = {}) => installMutation.mutateAsync({ model, force: options.force }), [installMutation]);
7395
+ const removeModel = useCallback13((model) => removeMutation.mutateAsync(model), [removeMutation]);
7396
+ return {
7397
+ statusQuery,
7398
+ status: statusQuery.data,
7399
+ models: statusQuery.data?.models ?? [],
7400
+ defaultModel: statusQuery.data?.defaultModel ?? null,
7401
+ isAvailable: statusQuery.data?.available ?? false,
7402
+ activeInstallModelId,
7403
+ installProgress,
7404
+ installStreamError,
7405
+ installModel,
7406
+ removeModel,
7407
+ installMutation,
7408
+ removeMutation,
7409
+ openInstallStream,
7410
+ closeInstallStream
7411
+ };
7412
+ }
6912
7413
  export {
6913
7414
  useWorkingDirectory,
7415
+ useVoiceInput,
6914
7416
  useUpdateSkillsConfig,
6915
7417
  useUpdateSession,
6916
7418
  useUpdateDefaults,
@@ -6931,7 +7433,6 @@ export {
6931
7433
  useSkillFileContent,
6932
7434
  useSkillDetail,
6933
7435
  useShareStatus,
6934
- useSetuPayments,
6935
7436
  useSessionsInfinite,
6936
7437
  useSessions,
6937
7438
  useSessionStream,
@@ -6949,6 +7450,7 @@ export {
6949
7450
  useProviderUsage,
6950
7451
  usePreferences,
6951
7452
  useParentSession,
7453
+ useOttoRouterPayments,
6952
7454
  useOttoRouterBalance,
6953
7455
  useModels,
6954
7456
  useMessages,
@@ -6973,6 +7475,7 @@ export {
6973
7475
  useFileTree,
6974
7476
  useFileContent,
6975
7477
  useExportToSession,
7478
+ useDictationModels,
6976
7479
  useDeleteSession,
6977
7480
  useDeleteResearchSession,
6978
7481
  useDeleteFiles,
@@ -6997,4 +7500,4 @@ export {
6997
7500
  normalizeQueueState
6998
7501
  };
6999
7502
 
7000
- //# debugId=C8B7C4D04F69D1E164756E2164756E21
7503
+ //# debugId=6E5B9890DD6BFD3564756E2164756E21