@vybestack/llxprt-code-core 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7

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 (168) hide show
  1. package/dist/src/config/config.d.ts +20 -0
  2. package/dist/src/config/config.js +143 -23
  3. package/dist/src/config/config.js.map +1 -1
  4. package/dist/src/config/configBaseCore.d.ts +6 -2
  5. package/dist/src/config/configBaseCore.js +8 -0
  6. package/dist/src/config/configBaseCore.js.map +1 -1
  7. package/dist/src/config/configConstructor.d.ts +2 -0
  8. package/dist/src/config/configConstructor.js +9 -1
  9. package/dist/src/config/configConstructor.js.map +1 -1
  10. package/dist/src/config/configTestHarness.d.ts +8 -6
  11. package/dist/src/config/configTestHarness.js +10 -4
  12. package/dist/src/config/configTestHarness.js.map +1 -1
  13. package/dist/src/config/configTypes.d.ts +7 -2
  14. package/dist/src/config/configTypes.js +2 -0
  15. package/dist/src/config/configTypes.js.map +1 -1
  16. package/dist/src/config/liveTrustTransitionLifecycle.d.ts +35 -0
  17. package/dist/src/config/liveTrustTransitionLifecycle.js +180 -0
  18. package/dist/src/config/liveTrustTransitionLifecycle.js.map +1 -0
  19. package/dist/src/config/schedulerSingleton.d.ts +2 -2
  20. package/dist/src/config/schedulerSingleton.js.map +1 -1
  21. package/dist/src/core/compression/types.d.ts +48 -1
  22. package/dist/src/core/compression/types.js +41 -0
  23. package/dist/src/core/compression/types.js.map +1 -1
  24. package/dist/src/core/model-limits.json +96 -0
  25. package/dist/src/core/model-limits.schema.d.ts +123 -0
  26. package/dist/src/core/model-limits.schema.js +75 -0
  27. package/dist/src/core/model-limits.schema.js.map +1 -0
  28. package/dist/src/core/tokenLimits.d.ts +6 -8
  29. package/dist/src/core/tokenLimits.js +56 -132
  30. package/dist/src/core/tokenLimits.js.map +1 -1
  31. package/dist/src/core/turn.d.ts +20 -3
  32. package/dist/src/core/turn.js +20 -3
  33. package/dist/src/core/turn.js.map +1 -1
  34. package/dist/src/hooks/hookRegistry.d.ts +1 -1
  35. package/dist/src/hooks/hookRegistry.js +3 -1
  36. package/dist/src/hooks/hookRegistry.js.map +1 -1
  37. package/dist/src/hooks/hookRunner.d.ts +6 -0
  38. package/dist/src/hooks/hookRunner.js +22 -6
  39. package/dist/src/hooks/hookRunner.js.map +1 -1
  40. package/dist/src/hooks/hookSystem.d.ts +10 -1
  41. package/dist/src/hooks/hookSystem.js +69 -12
  42. package/dist/src/hooks/hookSystem.js.map +1 -1
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.js +1 -0
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/llm-types/geminiContent.d.ts +3 -0
  47. package/dist/src/policy/config.d.ts +4 -2
  48. package/dist/src/policy/config.js +22 -18
  49. package/dist/src/policy/config.js.map +1 -1
  50. package/dist/src/recording/ReplayEngine.js +39 -2
  51. package/dist/src/recording/ReplayEngine.js.map +1 -1
  52. package/dist/src/recording/SessionDiscovery.d.ts +8 -0
  53. package/dist/src/recording/SessionDiscovery.js +88 -3
  54. package/dist/src/recording/SessionDiscovery.js.map +1 -1
  55. package/dist/src/recording/SessionLockManager.d.ts +3 -0
  56. package/dist/src/recording/SessionLockManager.js +9 -3
  57. package/dist/src/recording/SessionLockManager.js.map +1 -1
  58. package/dist/src/recording/SessionRecordingService.d.ts +14 -1
  59. package/dist/src/recording/SessionRecordingService.js +27 -3
  60. package/dist/src/recording/SessionRecordingService.js.map +1 -1
  61. package/dist/src/recording/index.d.ts +4 -3
  62. package/dist/src/recording/index.js +4 -3
  63. package/dist/src/recording/index.js.map +1 -1
  64. package/dist/src/recording/replay-test-helpers.d.ts +5 -0
  65. package/dist/src/recording/replay-test-helpers.js +13 -0
  66. package/dist/src/recording/replay-test-helpers.js.map +1 -1
  67. package/dist/src/recording/resumeNotFoundMessages.d.ts +56 -0
  68. package/dist/src/recording/resumeNotFoundMessages.js +64 -0
  69. package/dist/src/recording/resumeNotFoundMessages.js.map +1 -0
  70. package/dist/src/recording/resumeSession.js +3 -2
  71. package/dist/src/recording/resumeSession.js.map +1 -1
  72. package/dist/src/recording/sessionManagement.d.ts +3 -0
  73. package/dist/src/recording/sessionManagement.js +38 -19
  74. package/dist/src/recording/sessionManagement.js.map +1 -1
  75. package/dist/src/recording/types.d.ts +42 -3
  76. package/dist/src/recording/types.js +1 -1
  77. package/dist/src/recording/types.js.map +1 -1
  78. package/dist/src/runtime/AgentRuntimeLoader.js +2 -1
  79. package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -1
  80. package/dist/src/scheduler/liveOutput.d.ts +10 -19
  81. package/dist/src/scheduler/liveOutput.js +21 -21
  82. package/dist/src/scheduler/liveOutput.js.map +1 -1
  83. package/dist/src/scheduler/types.d.ts +2 -2
  84. package/dist/src/services/history/ContentConverters.d.ts +4 -0
  85. package/dist/src/services/history/ContentConverters.js +77 -59
  86. package/dist/src/services/history/ContentConverters.js.map +1 -1
  87. package/dist/src/services/history/HistoryService.d.ts +13 -1
  88. package/dist/src/services/history/HistoryService.js +71 -0
  89. package/dist/src/services/history/HistoryService.js.map +1 -1
  90. package/dist/src/services/history/IContent.d.ts +10 -0
  91. package/dist/src/services/history/IContent.js.map +1 -1
  92. package/dist/src/skills/skillManager.d.ts +11 -1
  93. package/dist/src/skills/skillManager.js +21 -1
  94. package/dist/src/skills/skillManager.js.map +1 -1
  95. package/dist/src/test-utils/mock-tool.d.ts +3 -3
  96. package/dist/src/test-utils/mock-tool.js.map +1 -1
  97. package/dist/src/test-utils/tools.d.ts +2 -2
  98. package/dist/src/test-utils/tools.js.map +1 -1
  99. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js +1 -6
  100. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js.map +1 -1
  101. package/dist/src/utils/browser-profile-discovery.d.ts +40 -0
  102. package/dist/src/utils/browser-profile-discovery.js +251 -0
  103. package/dist/src/utils/browser-profile-discovery.js.map +1 -0
  104. package/dist/src/utils/env.d.ts +12 -0
  105. package/dist/src/utils/env.js +15 -0
  106. package/dist/src/utils/env.js.map +1 -0
  107. package/dist/src/utils/events.d.ts +10 -1
  108. package/dist/src/utils/events.js +22 -0
  109. package/dist/src/utils/events.js.map +1 -1
  110. package/dist/src/utils/fileUtils.js +8 -1
  111. package/dist/src/utils/fileUtils.js.map +1 -1
  112. package/dist/src/utils/retry.js +3 -4
  113. package/dist/src/utils/retry.js.map +1 -1
  114. package/dist/src/utils/secure-browser-launcher.d.ts +35 -1
  115. package/dist/src/utils/secure-browser-launcher.js +341 -1
  116. package/dist/src/utils/secure-browser-launcher.js.map +1 -1
  117. package/dist/src/utils/terminalSerializer.d.ts +12 -0
  118. package/dist/src/utils/terminalSerializer.js.map +1 -1
  119. package/dist/src/utils/thoughtUtils.d.ts +2 -0
  120. package/dist/src/utils/thoughtUtils.js.map +1 -1
  121. package/package.json +13 -9
  122. package/src/config/config.ts +161 -22
  123. package/src/config/configBaseCore.ts +10 -2
  124. package/src/config/configConstructor.ts +11 -0
  125. package/src/config/configTestHarness.ts +10 -4
  126. package/src/config/configTypes.ts +6 -1
  127. package/src/config/liveTrustTransitionLifecycle.ts +250 -0
  128. package/src/config/schedulerSingleton.ts +2 -5
  129. package/src/core/compression/types.ts +81 -1
  130. package/src/core/legacy-model-limits.expected.txt +96 -0
  131. package/src/core/model-limits.json +96 -0
  132. package/src/core/model-limits.schema.ts +87 -0
  133. package/src/core/tokenLimits.ts +73 -147
  134. package/src/core/turn.ts +20 -3
  135. package/src/hooks/hookRegistry.ts +3 -1
  136. package/src/hooks/hookRunner.ts +24 -7
  137. package/src/hooks/hookSystem.ts +73 -13
  138. package/src/index.ts +1 -0
  139. package/src/llm-types/geminiContent.ts +3 -0
  140. package/src/policy/config.ts +32 -19
  141. package/src/recording/ReplayEngine.ts +51 -2
  142. package/src/recording/SessionDiscovery.ts +99 -4
  143. package/src/recording/SessionLockManager.ts +10 -3
  144. package/src/recording/SessionRecordingService.ts +36 -3
  145. package/src/recording/index.ts +19 -2
  146. package/src/recording/replay-test-helpers.ts +14 -0
  147. package/src/recording/resumeNotFoundMessages.ts +75 -0
  148. package/src/recording/resumeSession.ts +3 -1
  149. package/src/recording/sessionManagement.ts +51 -21
  150. package/src/recording/types.ts +44 -2
  151. package/src/runtime/AgentRuntimeLoader.ts +3 -1
  152. package/src/scheduler/liveOutput.ts +25 -22
  153. package/src/scheduler/types.ts +5 -2
  154. package/src/services/history/ContentConverters.ts +91 -68
  155. package/src/services/history/HistoryService.ts +92 -1
  156. package/src/services/history/IContent.ts +13 -0
  157. package/src/skills/skillManager.ts +26 -1
  158. package/src/test-utils/mock-tool.ts +4 -3
  159. package/src/test-utils/tools.ts +4 -3
  160. package/src/tools-adapters/CoreShellToolHostAdapter.ts +1 -7
  161. package/src/utils/browser-profile-discovery.ts +358 -0
  162. package/src/utils/env.ts +15 -0
  163. package/src/utils/events.ts +31 -0
  164. package/src/utils/fileUtils.ts +11 -1
  165. package/src/utils/retry.ts +3 -4
  166. package/src/utils/secure-browser-launcher.ts +500 -2
  167. package/src/utils/terminalSerializer.ts +9 -0
  168. package/src/utils/thoughtUtils.ts +2 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import * as os from 'node:os';
9
+ import * as path from 'node:path';
10
+ import type { BrowserKind } from './secure-browser-launcher.js';
11
+ import { envOr } from './env.js';
12
+
13
+ /**
14
+ * A discovered browser profile.
15
+ */
16
+ export interface DiscoveredBrowserProfile {
17
+ /**
18
+ * Browser-specific profile selector: Chrome profile directory, Firefox
19
+ * profile name (for `-P`) or absolute profile path (for `-profile`).
20
+ */
21
+ directoryName: string;
22
+ displayName: string;
23
+ }
24
+
25
+ /**
26
+ * Options for profile discovery. All filesystem access is injectable
27
+ * so tests never touch real disk.
28
+ */
29
+ export interface DiscoverBrowserProfilesOptions {
30
+ platform?: NodeJS.Platform;
31
+ userDataDir?: string;
32
+ homeDir?: string;
33
+ fileExists?: (p: string) => boolean;
34
+ readFile?: (p: string) => string;
35
+ }
36
+
37
+ interface ChromeLocalState {
38
+ profile?: {
39
+ info_cache?: Record<string, { name?: string } | undefined>;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Resolve the Chrome user data directory for the given platform.
45
+ */
46
+ function chromeUserDataDir(
47
+ platformName: NodeJS.Platform,
48
+ homeDir: string,
49
+ ): string {
50
+ switch (platformName) {
51
+ case 'darwin':
52
+ return path.join(
53
+ homeDir,
54
+ 'Library',
55
+ 'Application Support',
56
+ 'Google',
57
+ 'Chrome',
58
+ );
59
+ case 'win32':
60
+ return path.join(
61
+ envOr(process.env.LOCALAPPDATA, path.join(homeDir, 'AppData', 'Local')),
62
+ 'Google',
63
+ 'Chrome',
64
+ 'User Data',
65
+ );
66
+ case 'linux':
67
+ case 'freebsd':
68
+ case 'openbsd':
69
+ return path.join(homeDir, '.config', 'google-chrome');
70
+ default:
71
+ throw new Error(`Unsupported platform for Chrome: ${platformName}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Resolve the Firefox profile root directory for the given platform.
77
+ */
78
+ function firefoxProfileRoot(
79
+ platformName: NodeJS.Platform,
80
+ homeDir: string,
81
+ ): string {
82
+ switch (platformName) {
83
+ case 'darwin':
84
+ return path.join(homeDir, 'Library', 'Application Support', 'Firefox');
85
+ case 'win32':
86
+ return path.join(
87
+ envOr(process.env.APPDATA, path.join(homeDir, 'AppData', 'Roaming')),
88
+ 'Mozilla',
89
+ 'Firefox',
90
+ );
91
+ case 'linux':
92
+ case 'freebsd':
93
+ case 'openbsd':
94
+ return path.join(homeDir, '.mozilla', 'firefox');
95
+ default:
96
+ throw new Error(`Unsupported platform for Firefox: ${platformName}`);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Discover Chrome profiles by reading the Local State JSON file.
102
+ */
103
+ function discoverChromeProfiles(
104
+ opts: Required<
105
+ Omit<DiscoverBrowserProfilesOptions, 'platform' | 'userDataDir' | 'homeDir'>
106
+ > & {
107
+ userDataDir: string;
108
+ },
109
+ ): DiscoveredBrowserProfile[] {
110
+ const localStatePath = path.join(opts.userDataDir, 'Local State');
111
+
112
+ if (!opts.fileExists(localStatePath)) {
113
+ return [];
114
+ }
115
+
116
+ let raw: string;
117
+ try {
118
+ raw = opts.readFile(localStatePath);
119
+ } catch {
120
+ return [];
121
+ }
122
+
123
+ let parsed: unknown;
124
+ try {
125
+ parsed = JSON.parse(raw);
126
+ } catch {
127
+ return [];
128
+ }
129
+
130
+ // JSON.parse can return null, primitives, or arrays for valid-but-unexpected
131
+ // content; only an object with a profile.info_cache shape is usable.
132
+ if (typeof parsed !== 'object' || parsed === null) {
133
+ return [];
134
+ }
135
+ const localState = parsed as ChromeLocalState;
136
+ const infoCache = localState.profile?.info_cache;
137
+ if (!infoCache || typeof infoCache !== 'object') {
138
+ return [];
139
+ }
140
+
141
+ const profiles: DiscoveredBrowserProfile[] = [];
142
+ for (const [directoryName, info] of Object.entries(infoCache)) {
143
+ if (!info || typeof info !== 'object') {
144
+ continue;
145
+ }
146
+ const displayName = info.name ?? directoryName;
147
+ profiles.push({ directoryName, displayName });
148
+ }
149
+
150
+ return profiles;
151
+ }
152
+
153
+ interface FirefoxProfileEntry {
154
+ name?: string;
155
+ path?: string;
156
+ }
157
+
158
+ /**
159
+ * Parse a Firefox profiles.ini file into profile entries.
160
+ *
161
+ * Each `[Profile*]` section contributes an entry built from its `Name=` and
162
+ * `Path=` keys. Sections that yield neither a name nor a path are skipped.
163
+ */
164
+ function parseFirefoxProfilesIni(iniContent: string): FirefoxProfileEntry[] {
165
+ const entries: FirefoxProfileEntry[] = [];
166
+
167
+ const sections = splitIniSections(iniContent);
168
+ for (const section of sections) {
169
+ if (!/^Profile\d+$/i.test(section.header)) {
170
+ continue;
171
+ }
172
+ const entry = buildFirefoxEntry(section.lines);
173
+ if (entry.name || entry.path) {
174
+ entries.push(entry);
175
+ }
176
+ }
177
+
178
+ return entries;
179
+ }
180
+
181
+ interface IniSection {
182
+ header: string;
183
+ lines: string[];
184
+ }
185
+
186
+ /**
187
+ * Group raw ini lines into sections keyed by their `[header]`.
188
+ * Lines before the first header are dropped.
189
+ */
190
+ function splitIniSections(iniContent: string): IniSection[] {
191
+ const sections: IniSection[] = [];
192
+ let current: IniSection | null = null;
193
+
194
+ for (const line of iniContent.split('\n')) {
195
+ const trimmed = line.trim();
196
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
197
+ current = { header: trimmed.slice(1, -1), lines: [] };
198
+ sections.push(current);
199
+ } else if (current) {
200
+ current.lines.push(trimmed);
201
+ }
202
+ }
203
+
204
+ return sections;
205
+ }
206
+
207
+ /**
208
+ * Build a Firefox profile entry from a section's key=value lines.
209
+ */
210
+ function buildFirefoxEntry(lines: string[]): FirefoxProfileEntry {
211
+ const entry: FirefoxProfileEntry = {};
212
+ for (const trimmed of lines) {
213
+ const parsed = parseIniKeyValue(trimmed);
214
+ if (!parsed) {
215
+ continue;
216
+ }
217
+ if (parsed.key === 'Name') {
218
+ entry.name = parsed.value;
219
+ } else if (parsed.key === 'Path') {
220
+ entry.path = parsed.value;
221
+ }
222
+ }
223
+ return entry;
224
+ }
225
+
226
+ function parseIniKeyValue(
227
+ line: string,
228
+ ): { key: string; value: string } | undefined {
229
+ const eqIndex = line.indexOf('=');
230
+ if (eqIndex === -1) {
231
+ return undefined;
232
+ }
233
+ return {
234
+ key: line.slice(0, eqIndex).trim(),
235
+ value: line.slice(eqIndex + 1).trim(),
236
+ };
237
+ }
238
+
239
+ function resolveRelativeFirefoxProfilePath(
240
+ rootDir: string,
241
+ profilePath: string,
242
+ ): string | undefined {
243
+ const resolvedRoot = path.resolve(rootDir);
244
+ const resolvedProfile = path.resolve(resolvedRoot, profilePath);
245
+ const relativePath = path.relative(resolvedRoot, resolvedProfile);
246
+ if (
247
+ relativePath === '..' ||
248
+ relativePath.startsWith(`..${path.sep}`) ||
249
+ path.isAbsolute(relativePath)
250
+ ) {
251
+ return undefined;
252
+ }
253
+ return resolvedProfile;
254
+ }
255
+
256
+ /**
257
+ * Discover Firefox profiles by reading profiles.ini.
258
+ */
259
+ function discoverFirefoxProfiles(
260
+ rootDir: string,
261
+ opts: Required<
262
+ Omit<DiscoverBrowserProfilesOptions, 'platform' | 'userDataDir' | 'homeDir'>
263
+ >,
264
+ ): DiscoveredBrowserProfile[] {
265
+ const iniPath = path.join(rootDir, 'profiles.ini');
266
+
267
+ if (!opts.fileExists(iniPath)) {
268
+ return [];
269
+ }
270
+
271
+ let raw: string;
272
+ try {
273
+ raw = opts.readFile(iniPath);
274
+ } catch {
275
+ return [];
276
+ }
277
+
278
+ const entries = parseFirefoxProfilesIni(raw);
279
+
280
+ const profiles: DiscoveredBrowserProfile[] = [];
281
+ for (const entry of entries) {
282
+ // Firefox's -P flag selects a profile by its Name (the [Profile*]
283
+ // "Name=" key), not by its on-disk Path. When the two differ (which they
284
+ // do whenever the user renamed a profile or Firefox relocated it),
285
+ // passing the Path would fail to select the intended profile, so prefer
286
+ // Name. Fall back to Path only for entries that omit a Name entirely;
287
+ // profiles.ini stores Path as a relative reference (e.g.
288
+ // "Profiles/abcd.default"), so resolve it against rootDir to produce a
289
+ // usable, unambiguous selector instead of a brittle relative fragment.
290
+ let directoryName = entry.name;
291
+ if (!directoryName && entry.path) {
292
+ directoryName = path.isAbsolute(entry.path)
293
+ ? entry.path
294
+ : resolveRelativeFirefoxProfilePath(rootDir, entry.path);
295
+ }
296
+ if (directoryName) {
297
+ profiles.push({
298
+ directoryName,
299
+ displayName: entry.name ?? directoryName,
300
+ });
301
+ }
302
+ }
303
+
304
+ return profiles;
305
+ }
306
+
307
+ /**
308
+ * Discover browser profiles for the specified browser kind.
309
+ *
310
+ * All filesystem access is injectable via opts so tests never touch real disk.
311
+ * When opts are omitted, real node:fs and os.homedir() and process.platform
312
+ * are used.
313
+ *
314
+ * @param browser The browser kind to discover profiles for
315
+ * @param opts Optional injectable filesystem and platform overrides
316
+ * @returns Array of discovered profiles (empty if none found or errors occur)
317
+ */
318
+ export function discoverBrowserProfiles(
319
+ browser: BrowserKind,
320
+ opts?: DiscoverBrowserProfilesOptions,
321
+ ): DiscoveredBrowserProfile[] {
322
+ const platformName = opts?.platform ?? process.platform;
323
+ const homeDir = opts?.homeDir ?? os.homedir();
324
+ const fileExists = opts?.fileExists ?? existsSync;
325
+ const readFile = opts?.readFile ?? ((p: string) => readFileSync(p, 'utf-8'));
326
+
327
+ const fsOpts = { fileExists, readFile };
328
+
329
+ switch (browser) {
330
+ case 'chrome': {
331
+ // An unsupported platform surfaces as a thrown error from
332
+ // chromeUserDataDir; discovery is best-effort, so degrade to an empty
333
+ // list rather than crashing the calling command.
334
+ try {
335
+ const userDataDir =
336
+ opts?.userDataDir ?? chromeUserDataDir(platformName, homeDir);
337
+ return discoverChromeProfiles({ ...fsOpts, userDataDir });
338
+ } catch {
339
+ return [];
340
+ }
341
+ }
342
+
343
+ case 'firefox': {
344
+ try {
345
+ const rootDir = firefoxProfileRoot(platformName, homeDir);
346
+ return discoverFirefoxProfiles(rootDir, fsOpts);
347
+ } catch {
348
+ return [];
349
+ }
350
+ }
351
+
352
+ case 'safari':
353
+ return [{ directoryName: 'Default', displayName: 'Safari' }];
354
+
355
+ default:
356
+ return [];
357
+ }
358
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * Return the given environment variable value when it is present and non-empty,
9
+ * otherwise return the fallback. An explicitly-empty (or whitespace-only)
10
+ * value is treated as absent so that misconfigured environment variables fall
11
+ * back to the default instead of producing malformed partial paths.
12
+ */
13
+ export function envOr(value: string | undefined, fallback: string): string {
14
+ return value?.trim() ? value : fallback;
15
+ }
@@ -103,6 +103,7 @@ export enum CoreEvent {
103
103
  ExternalEditorClosed = 'external-editor-closed',
104
104
  McpClientUpdate = 'mcp-client-update',
105
105
  SettingsChanged = 'settings-changed',
106
+ FolderTrustChanged = 'folder-trust-changed',
106
107
  }
107
108
 
108
109
  export class CoreEventEmitter extends EventEmitter {
@@ -143,6 +144,27 @@ export class CoreEventEmitter extends EventEmitter {
143
144
  this.emit(CoreEvent.SettingsChanged);
144
145
  }
145
146
 
147
+ /**
148
+ * Notifies subscribers that the effective folder-trust state has changed
149
+ * during the active session (e.g. via the permissions dialog).
150
+ */
151
+ emitFolderTrustChanged(trusted: boolean): void {
152
+ const failures: unknown[] = [];
153
+ for (const listener of this.rawListeners(CoreEvent.FolderTrustChanged)) {
154
+ try {
155
+ listener.call(this, trusted);
156
+ } catch (error) {
157
+ failures.push(error);
158
+ }
159
+ }
160
+ if (failures.length === 1) {
161
+ throw failures[0];
162
+ }
163
+ if (failures.length > 1) {
164
+ throw new AggregateError(failures, 'Folder trust listeners failed');
165
+ }
166
+ }
167
+
146
168
  /**
147
169
  * Flushes buffered messages. Call this immediately after primary UI listener
148
170
  * subscribes.
@@ -184,6 +206,10 @@ export class CoreEventEmitter extends EventEmitter {
184
206
  listener: (payload: OutputPayload) => void,
185
207
  ): this;
186
208
  override on(event: CoreEvent.SettingsChanged, listener: () => void): this;
209
+ override on(
210
+ event: CoreEvent.FolderTrustChanged,
211
+ listener: (trusted: boolean) => void,
212
+ ): this;
187
213
  override on(
188
214
  event: CoreEvent.McpClientUpdate,
189
215
  listener: (payload: McpClientUpdatePayload) => void,
@@ -224,6 +250,10 @@ export class CoreEventEmitter extends EventEmitter {
224
250
  listener: (payload: OutputPayload) => void,
225
251
  ): this;
226
252
  override off(event: CoreEvent.SettingsChanged, listener: () => void): this;
253
+ override off(
254
+ event: CoreEvent.FolderTrustChanged,
255
+ listener: (trusted: boolean) => void,
256
+ ): this;
227
257
  override off(
228
258
  event: CoreEvent.McpClientUpdate,
229
259
  listener: (payload: McpClientUpdatePayload) => void,
@@ -259,6 +289,7 @@ export class CoreEventEmitter extends EventEmitter {
259
289
  override emit(event: CoreEvent.Output, payload: OutputPayload): boolean;
260
290
  override emit(event: CoreEvent.ExternalEditorClosed): boolean;
261
291
  override emit(event: CoreEvent.SettingsChanged): boolean;
292
+ override emit(event: CoreEvent.FolderTrustChanged, trusted: boolean): boolean;
262
293
  override emit(
263
294
  event: CoreEvent.McpClientUpdate,
264
295
  payload: McpClientUpdatePayload,
@@ -269,8 +269,18 @@ export async function detectFileType(
269
269
  return 'svg';
270
270
  }
271
271
 
272
+ // mime-db maps the FreeHand family (.fh, .fh4, .fh5, .fh7, .fhc) to
273
+ // image/x-freehand, but these extensions are commonly shader/source text.
274
+ // Exclude them from the image short-circuit below so they fall through to
275
+ // content-based detection (text or binary).
276
+ const freehandExtensions = ['.fh', '.fh4', '.fh5', '.fh7', '.fhc'];
277
+
272
278
  const lookedUpMimeType = mime.lookup(filePath); // Returns false if not found, or the mime type string
273
- if (typeof lookedUpMimeType === 'string' && lookedUpMimeType !== '') {
279
+ if (
280
+ typeof lookedUpMimeType === 'string' &&
281
+ lookedUpMimeType !== '' &&
282
+ !freehandExtensions.includes(ext)
283
+ ) {
274
284
  if (lookedUpMimeType.startsWith('image/')) {
275
285
  return 'image';
276
286
  }
@@ -245,14 +245,13 @@ export function isNetworkTransientError(error: unknown): boolean {
245
245
  export function isRetryableError(error: Error | unknown): boolean {
246
246
  // PRIORITY 0: Self-classifying aggregate errors (e.g. the load balancer
247
247
  // LoadBalancerFailoverError). An aggregate-of-failures has already computed
248
- // its own retryability from EACH underlying backend failure, so it is
248
+ // its own retryability from its underlying backend failures, so it is
249
249
  // authoritative. Its human-readable message flattens the child messages,
250
250
  // which means the message-based heuristics below (network-transient, overload)
251
251
  // would otherwise re-interpret a sibling backend's text and override the
252
- // precise per-child decision (e.g. a mixed "socket hang up" + HTTP 400
253
- // aggregate must NOT be retried). Identified structurally — a `failures`
252
+ // precise per-child decision. Identified structurally a `failures`
254
253
  // array plus a boolean `isRetryable` — so core does not import from
255
- // providers. (issue #2450)
254
+ // providers. (issue #2450, updated #2712)
256
255
  if (
257
256
  typeof error === 'object' &&
258
257
  error !== null &&