kanna-code 0.35.1 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import { cp as copyPath, copyFile, mkdir, readFile, readdir, stat, writeFile } f
5
5
  import type {
6
6
  StandaloneTranscriptAttachmentMode,
7
7
  StandaloneTranscriptBundle,
8
- StandaloneTranscriptExportResult,
8
+ StandaloneTranscriptExportCommandResult,
9
9
  StandaloneTranscriptTheme,
10
10
  TranscriptEntry,
11
11
  } from "../shared/types"
@@ -76,7 +76,7 @@ export function getStandaloneViewerDistDir() {
76
76
  export async function writeStandaloneTranscriptExport(
77
77
  args: WriteStandaloneTranscriptExportArgs,
78
78
  deps: StandaloneExportDeps = {},
79
- ): Promise<StandaloneTranscriptExportResult> {
79
+ ): Promise<StandaloneTranscriptExportCommandResult> {
80
80
  const viewerDistDir = deps.viewerDistDir ?? getStandaloneViewerDistDir()
81
81
  const ensureDir = deps.mkdir ?? mkdir
82
82
  const writeFileImpl = deps.writeFile ?? writeFile
@@ -124,21 +124,38 @@ export async function writeStandaloneTranscriptExport(
124
124
  messages: prepared.messages,
125
125
  }
126
126
 
127
+ const transcriptJson = `${JSON.stringify(bundle, null, 2)}\n`
127
128
  const transcriptJsonPath = path.join(outputDir, "transcript.json")
128
- await writeFileImpl(transcriptJsonPath, `${JSON.stringify(bundle, null, 2)}\n`, "utf8")
129
+ await writeFileImpl(transcriptJsonPath, transcriptJson, "utf8")
129
130
  const shareSlug = buildStandaloneShareSlug(args.title || args.chatId, deps.shareSlugSuffix)
130
131
  const shareUrl = buildStandaloneShareUrl(sharePublicBaseUrl, shareSlug)
131
- const uploadedFileCount = await uploadStandaloneExportDirectory({
132
- outputDir,
133
- shareSlug,
134
- uploadBaseUrl: shareUploadBaseUrl,
135
- fetch: fetchImpl,
136
- pathExists,
137
- readDir,
138
- readFile: readFileImpl,
139
- })
132
+ let uploadedFileCount = 0
133
+
134
+ try {
135
+ uploadedFileCount = await uploadStandaloneExportDirectory({
136
+ outputDir,
137
+ shareSlug,
138
+ uploadBaseUrl: shareUploadBaseUrl,
139
+ fetch: fetchImpl,
140
+ pathExists,
141
+ readDir,
142
+ readFile: readFileImpl,
143
+ })
144
+ } catch (error) {
145
+ return {
146
+ ok: false,
147
+ error: error instanceof Error ? error.message : String(error),
148
+ outputDir,
149
+ transcriptJsonPath,
150
+ transcriptFileName: `${path.basename(outputDir)}-transcript.json`,
151
+ transcriptJson,
152
+ shareSlug,
153
+ shareUrl,
154
+ }
155
+ }
140
156
 
141
157
  return {
158
+ ok: true,
142
159
  outputDir,
143
160
  indexHtmlPath: path.join(outputDir, "index.html"),
144
161
  transcriptJsonPath,
@@ -61,6 +61,37 @@ const DEFAULT_KEYBINDINGS_SNAPSHOT: KeybindingsSnapshot = {
61
61
 
62
62
  const DEFAULT_APP_SETTINGS_SNAPSHOT: AppSettingsSnapshot = {
63
63
  analyticsEnabled: true,
64
+ browserSettingsMigrated: false,
65
+ theme: "system",
66
+ chatSoundPreference: "always",
67
+ chatSoundId: "funk",
68
+ terminal: {
69
+ scrollbackLines: 1_000,
70
+ minColumnWidth: 450,
71
+ },
72
+ editor: {
73
+ preset: "cursor",
74
+ commandTemplate: "cursor {path}",
75
+ },
76
+ defaultProvider: "last_used",
77
+ providerDefaults: {
78
+ claude: {
79
+ model: "claude-opus-4-7",
80
+ modelOptions: {
81
+ reasoningEffort: "high",
82
+ contextWindow: "200k",
83
+ },
84
+ planMode: false,
85
+ },
86
+ codex: {
87
+ model: "gpt-5.5",
88
+ modelOptions: {
89
+ reasoningEffort: "high",
90
+ fastMode: false,
91
+ },
92
+ planMode: false,
93
+ },
94
+ },
64
95
  warning: null,
65
96
  filePathDisplay: "~/.kanna/data/settings.json",
66
97
  }
@@ -163,6 +194,7 @@ describe("ws-router", () => {
163
194
  updateManager: null,
164
195
  })
165
196
  const ws = new FakeWebSocket()
197
+ router.handleOpen(ws as never)
166
198
 
167
199
  await router.handleMessage(
168
200
  ws as never,
@@ -254,6 +286,7 @@ describe("ws-router", () => {
254
286
  updateManager: null,
255
287
  })
256
288
  const ws = new FakeWebSocket()
289
+ router.handleOpen(ws as never)
257
290
 
258
291
  await router.handleMessage(
259
292
  ws as never,
@@ -298,6 +331,124 @@ describe("ws-router", () => {
298
331
  expect(writes).toEqual([{ analyticsEnabled: false }])
299
332
  })
300
333
 
334
+ test("subscribes to app settings and writes patches through the router", async () => {
335
+ let snapshot: AppSettingsSnapshot = DEFAULT_APP_SETTINGS_SNAPSHOT
336
+ let listener: ((nextSnapshot: AppSettingsSnapshot) => void) | null = null
337
+ const router = createWsRouter({
338
+ store: { state: createEmptyState() } as never,
339
+ agent: { getActiveStatuses: () => new Map(), getDrainingChatIds: () => new Set() } as never,
340
+ terminals: {
341
+ getSnapshot: () => null,
342
+ onEvent: () => () => {},
343
+ } as never,
344
+ keybindings: {
345
+ getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
346
+ onChange: () => () => {},
347
+ } as never,
348
+ appSettings: {
349
+ getSnapshot: () => snapshot,
350
+ write: async (value) => {
351
+ snapshot = { ...snapshot, analyticsEnabled: value.analyticsEnabled }
352
+ return snapshot
353
+ },
354
+ writePatch: async (patch) => {
355
+ snapshot = {
356
+ ...snapshot,
357
+ analyticsEnabled: patch.analyticsEnabled ?? snapshot.analyticsEnabled,
358
+ browserSettingsMigrated: patch.browserSettingsMigrated ?? snapshot.browserSettingsMigrated,
359
+ theme: patch.theme ?? snapshot.theme,
360
+ chatSoundPreference: patch.chatSoundPreference ?? snapshot.chatSoundPreference,
361
+ chatSoundId: patch.chatSoundId ?? snapshot.chatSoundId,
362
+ defaultProvider: patch.defaultProvider ?? snapshot.defaultProvider,
363
+ terminal: { ...snapshot.terminal, ...patch.terminal },
364
+ editor: { ...snapshot.editor, ...patch.editor },
365
+ }
366
+ listener?.(snapshot)
367
+ return snapshot
368
+ },
369
+ onChange: (nextListener) => {
370
+ listener = nextListener
371
+ return () => {
372
+ listener = null
373
+ }
374
+ },
375
+ },
376
+ refreshDiscovery: async () => [],
377
+ getDiscoveredProjects: () => [],
378
+ machineDisplayName: "Local Machine",
379
+ updateManager: null,
380
+ })
381
+ const ws = new FakeWebSocket()
382
+ router.handleOpen(ws as never)
383
+
384
+ await router.handleMessage(
385
+ ws as never,
386
+ JSON.stringify({
387
+ v: 1,
388
+ type: "subscribe",
389
+ id: "app-settings-sub-1",
390
+ topic: { type: "app-settings" },
391
+ })
392
+ )
393
+
394
+ await router.handleMessage(
395
+ ws as never,
396
+ JSON.stringify({
397
+ v: 1,
398
+ type: "command",
399
+ id: "settings-patch-1",
400
+ command: {
401
+ type: "settings.writeAppSettingsPatch",
402
+ patch: {
403
+ theme: "dark",
404
+ terminal: { scrollbackLines: 2_000 },
405
+ },
406
+ },
407
+ })
408
+ )
409
+
410
+ expect(ws.sent).toEqual([
411
+ {
412
+ v: PROTOCOL_VERSION,
413
+ type: "snapshot",
414
+ id: "app-settings-sub-1",
415
+ snapshot: {
416
+ type: "app-settings",
417
+ data: DEFAULT_APP_SETTINGS_SNAPSHOT,
418
+ },
419
+ },
420
+ {
421
+ v: PROTOCOL_VERSION,
422
+ type: "snapshot",
423
+ id: "app-settings-sub-1",
424
+ snapshot: {
425
+ type: "app-settings",
426
+ data: {
427
+ ...DEFAULT_APP_SETTINGS_SNAPSHOT,
428
+ theme: "dark",
429
+ terminal: {
430
+ ...DEFAULT_APP_SETTINGS_SNAPSHOT.terminal,
431
+ scrollbackLines: 2_000,
432
+ },
433
+ },
434
+ },
435
+ },
436
+ {
437
+ v: PROTOCOL_VERSION,
438
+ type: "ack",
439
+ id: "settings-patch-1",
440
+ result: {
441
+ ...DEFAULT_APP_SETTINGS_SNAPSHOT,
442
+ theme: "dark",
443
+ terminal: {
444
+ ...DEFAULT_APP_SETTINGS_SNAPSHOT.terminal,
445
+ scrollbackLines: 2_000,
446
+ },
447
+ },
448
+ },
449
+ ])
450
+ })
451
+
301
452
  test("tracks analytics preference transitions in the correct order", async () => {
302
453
  const analyticsEvents: string[] = []
303
454
  let analyticsEnabled = true
@@ -16,7 +16,7 @@ import { writeStandaloneTranscriptExport } from "./standalone-export"
16
16
  import { TerminalManager } from "./terminal-manager"
17
17
  import type { UpdateManager } from "./update-manager"
18
18
  import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
19
- import type { AppSettingsSnapshot, LlmProviderSnapshot, LlmProviderValidationResult } from "../shared/types"
19
+ import type { AppSettingsPatch, AppSettingsSnapshot, LlmProviderSnapshot, LlmProviderValidationResult } from "../shared/types"
20
20
 
21
21
  const DEFAULT_CHAT_RECENT_LIMIT = 200
22
22
 
@@ -49,6 +49,7 @@ function countSubscriptionsByTopic(ws: ServerWebSocket<ClientState>) {
49
49
  let localProjects = 0
50
50
  let update = 0
51
51
  let keybindings = 0
52
+ let appSettings = 0
52
53
  let terminal = 0
53
54
 
54
55
  for (const topic of ws.data.subscriptions.values()) {
@@ -71,6 +72,9 @@ function countSubscriptionsByTopic(ws: ServerWebSocket<ClientState>) {
71
72
  case "keybindings":
72
73
  keybindings += 1
73
74
  break
75
+ case "app-settings":
76
+ appSettings += 1
77
+ break
74
78
  case "terminal":
75
79
  terminal += 1
76
80
  break
@@ -85,6 +89,7 @@ function countSubscriptionsByTopic(ws: ServerWebSocket<ClientState>) {
85
89
  localProjects,
86
90
  update,
87
91
  keybindings,
92
+ appSettings,
88
93
  terminal,
89
94
  }
90
95
  }
@@ -101,7 +106,7 @@ interface CreateWsRouterArgs {
101
106
  agent: AgentCoordinator
102
107
  terminals: TerminalManager
103
108
  keybindings: KeybindingsManager
104
- appSettings?: Pick<AppSettingsManager, "getSnapshot" | "write">
109
+ appSettings?: Pick<AppSettingsManager, "getSnapshot" | "write"> & Partial<Pick<AppSettingsManager, "writePatch" | "onChange">>
105
110
  analytics?: AnalyticsReporter
106
111
  llmProvider?: {
107
112
  read: () => Promise<LlmProviderSnapshot>
@@ -119,6 +124,7 @@ interface SnapshotBroadcastFilter {
119
124
  includeLocalProjects?: boolean
120
125
  includeUpdate?: boolean
121
126
  includeKeybindings?: boolean
127
+ includeAppSettings?: boolean
122
128
  chatIds?: Set<string>
123
129
  projectIds?: Set<string>
124
130
  terminalIds?: Set<string>
@@ -226,17 +232,88 @@ export function createWsRouter({
226
232
  },
227
233
  }),
228
234
  }
229
- const resolvedAppSettings = appSettings ?? {
230
- getSnapshot: () => ({
231
- analyticsEnabled: true,
232
- warning: null,
233
- filePathDisplay: "~/.kanna/data/settings.json",
234
- } satisfies AppSettingsSnapshot),
235
- write: async ({ analyticsEnabled }: { analyticsEnabled: boolean }) => ({
236
- analyticsEnabled,
237
- warning: null,
238
- filePathDisplay: "~/.kanna/data/settings.json",
239
- } satisfies AppSettingsSnapshot),
235
+ let fallbackAppSettingsSnapshot: AppSettingsSnapshot = {
236
+ analyticsEnabled: true,
237
+ browserSettingsMigrated: false,
238
+ theme: "system",
239
+ chatSoundPreference: "always",
240
+ chatSoundId: "funk",
241
+ terminal: {
242
+ scrollbackLines: 1_000,
243
+ minColumnWidth: 450,
244
+ },
245
+ editor: {
246
+ preset: "cursor",
247
+ commandTemplate: "cursor {path}",
248
+ },
249
+ defaultProvider: "last_used",
250
+ providerDefaults: {
251
+ claude: {
252
+ model: "claude-opus-4-7",
253
+ modelOptions: {
254
+ reasoningEffort: "high",
255
+ contextWindow: "200k",
256
+ },
257
+ planMode: false,
258
+ },
259
+ codex: {
260
+ model: "gpt-5.5",
261
+ modelOptions: {
262
+ reasoningEffort: "high",
263
+ fastMode: false,
264
+ },
265
+ planMode: false,
266
+ },
267
+ },
268
+ warning: null,
269
+ filePathDisplay: "~/.kanna/data/settings.json",
270
+ }
271
+ const mergeAppSettingsPatch = (snapshot: AppSettingsSnapshot, patch: AppSettingsPatch): AppSettingsSnapshot => ({
272
+ ...snapshot,
273
+ ...patch,
274
+ terminal: {
275
+ ...snapshot.terminal,
276
+ ...patch.terminal,
277
+ },
278
+ editor: {
279
+ ...snapshot.editor,
280
+ ...patch.editor,
281
+ },
282
+ providerDefaults: {
283
+ claude: {
284
+ ...snapshot.providerDefaults.claude,
285
+ ...patch.providerDefaults?.claude,
286
+ modelOptions: {
287
+ ...snapshot.providerDefaults.claude.modelOptions,
288
+ ...patch.providerDefaults?.claude?.modelOptions,
289
+ },
290
+ },
291
+ codex: {
292
+ ...snapshot.providerDefaults.codex,
293
+ ...patch.providerDefaults?.codex,
294
+ modelOptions: {
295
+ ...snapshot.providerDefaults.codex.modelOptions,
296
+ ...patch.providerDefaults?.codex?.modelOptions,
297
+ },
298
+ },
299
+ },
300
+ })
301
+ const resolvedAppSettings = {
302
+ getSnapshot: () => appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot,
303
+ write: async (value: { analyticsEnabled: boolean }) => {
304
+ if (appSettings) return await appSettings.write(value)
305
+ fallbackAppSettingsSnapshot = { ...fallbackAppSettingsSnapshot, analyticsEnabled: value.analyticsEnabled }
306
+ return fallbackAppSettingsSnapshot
307
+ },
308
+ writePatch: async (patch: AppSettingsPatch) => {
309
+ if (appSettings?.writePatch) return await appSettings.writePatch(patch)
310
+ if (appSettings && patch.analyticsEnabled !== undefined && Object.keys(patch).length === 1) {
311
+ return await appSettings.write({ analyticsEnabled: patch.analyticsEnabled })
312
+ }
313
+ fallbackAppSettingsSnapshot = mergeAppSettingsPatch(appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot, patch)
314
+ return fallbackAppSettingsSnapshot
315
+ },
316
+ onChange: (listener: (snapshot: AppSettingsSnapshot) => void) => appSettings?.onChange?.(listener) ?? (() => {}),
240
317
  }
241
318
  const resolvedAnalytics = analytics ?? NoopAnalyticsReporter
242
319
 
@@ -307,6 +384,9 @@ export function createWsRouter({
307
384
  if (topic.type === "keybindings") {
308
385
  return Boolean(filter.includeKeybindings)
309
386
  }
387
+ if (topic.type === "app-settings") {
388
+ return Boolean(filter.includeAppSettings)
389
+ }
310
390
  if (topic.type === "chat") {
311
391
  return filter.chatIds?.has(topic.chatId) ?? false
312
392
  }
@@ -398,6 +478,18 @@ export function createWsRouter({
398
478
  }
399
479
  }
400
480
 
481
+ if (topic.type === "app-settings") {
482
+ return {
483
+ v: PROTOCOL_VERSION,
484
+ type: "snapshot",
485
+ id,
486
+ snapshot: {
487
+ type: "app-settings",
488
+ data: resolvedAppSettings.getSnapshot(),
489
+ },
490
+ }
491
+ }
492
+
401
493
  if (topic.type === "update") {
402
494
  return {
403
495
  v: PROTOCOL_VERSION,
@@ -683,6 +775,21 @@ export function createWsRouter({
683
775
  }
684
776
  })
685
777
 
778
+ const disposeAppSettingsEvents = resolvedAppSettings.onChange(() => {
779
+ for (const ws of sockets) {
780
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
781
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
782
+ if (topic.type !== "app-settings") continue
783
+ const envelope = createEnvelope(id, topic)
784
+ if (envelope.type !== "snapshot") continue
785
+ const signature = JSON.stringify(envelope.snapshot)
786
+ if (snapshotSignatures.get(id) === signature) continue
787
+ snapshotSignatures.set(id, signature)
788
+ send(ws, envelope)
789
+ }
790
+ }
791
+ })
792
+
686
793
  const disposeUpdateEvents = updateManager?.onChange(() => {
687
794
  for (const ws of sockets) {
688
795
  const snapshotSignatures = ensureSnapshotSignatures(ws)
@@ -770,6 +877,18 @@ export function createWsRouter({
770
877
  }
771
878
  return
772
879
  }
880
+ case "settings.writeAppSettingsPatch": {
881
+ const previousAnalyticsEnabled = resolvedAppSettings.getSnapshot().analyticsEnabled
882
+ const snapshot = await resolvedAppSettings.writePatch(command.patch)
883
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
884
+ if (command.patch.analyticsEnabled !== undefined && previousAnalyticsEnabled && !snapshot.analyticsEnabled) {
885
+ resolvedAnalytics.track("analytics_disabled")
886
+ }
887
+ if (command.patch.analyticsEnabled !== undefined && !previousAnalyticsEnabled && snapshot.analyticsEnabled) {
888
+ resolvedAnalytics.track("analytics_enabled")
889
+ }
890
+ return
891
+ }
773
892
  case "settings.readLlmProvider": {
774
893
  send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: await resolvedLlmProvider.read() })
775
894
  return
@@ -1246,6 +1365,7 @@ export function createWsRouter({
1246
1365
  agent.setBackgroundErrorReporter?.(null)
1247
1366
  disposeTerminalEvents()
1248
1367
  disposeKeybindingEvents()
1368
+ disposeAppSettingsEvents()
1249
1369
  disposeUpdateEvents()
1250
1370
  },
1251
1371
  }
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AppSettingsSnapshot,
3
+ AppSettingsPatch,
3
4
  AgentProvider,
4
5
  ChatAttachment,
5
6
  ChatDiffSnapshot,
@@ -14,9 +15,10 @@ import type {
14
15
  StandaloneTranscriptAttachmentMode,
15
16
  StandaloneTranscriptExportResult,
16
17
  UpdateSnapshot,
18
+ EditorPreset,
17
19
  } from "./types"
18
20
 
19
- export type EditorPreset = "cursor" | "vscode" | "xcode" | "windsurf" | "custom"
21
+ export type { EditorPreset }
20
22
 
21
23
  export interface EditorOpenSettings {
22
24
  preset: EditorPreset
@@ -28,6 +30,7 @@ export type SubscriptionTopic =
28
30
  | { type: "local-projects" }
29
31
  | { type: "update" }
30
32
  | { type: "keybindings" }
33
+ | { type: "app-settings" }
31
34
  | { type: "chat"; chatId: string; recentLimit?: number }
32
35
  | { type: "project-git"; projectId: string }
33
36
  | { type: "terminal"; terminalId: string }
@@ -63,6 +66,7 @@ export type ClientCommand =
63
66
  | { type: "settings.writeKeybindings"; bindings: KeybindingsSnapshot["bindings"] }
64
67
  | { type: "settings.readAppSettings" }
65
68
  | { type: "settings.writeAppSettings"; analyticsEnabled: boolean }
69
+ | { type: "settings.writeAppSettingsPatch"; patch: AppSettingsPatch }
66
70
  | { type: "settings.readLlmProvider" }
67
71
  | {
68
72
  type: "settings.writeLlmProvider"
@@ -3,6 +3,11 @@ export const PROTOCOL_VERSION = 1 as const
3
3
 
4
4
  export type AgentProvider = "claude" | "codex"
5
5
  export type LlmProviderKind = "openai" | "openrouter" | "custom"
6
+ export type AppThemePreference = "light" | "dark" | "system"
7
+ export type ChatSoundPreference = "never" | "unfocused" | "always"
8
+ export type ChatSoundId = "blow" | "bottle" | "frog" | "funk" | "glass" | "ping" | "pop" | "purr" | "tink"
9
+ export type DefaultProviderPreference = "last_used" | AgentProvider
10
+ export type EditorPreset = "cursor" | "vscode" | "xcode" | "windsurf" | "custom"
6
11
  export const DEFAULT_OPENAI_SDK_MODEL = "gpt-5.4-mini"
7
12
  export const DEFAULT_OPENROUTER_SDK_MODEL = "moonshotai/kimi-k2.5:nitro"
8
13
 
@@ -34,6 +39,7 @@ export interface StandaloneTranscriptBundle {
34
39
  }
35
40
 
36
41
  export interface StandaloneTranscriptExportResult {
42
+ ok: true
37
43
  outputDir: string
38
44
  indexHtmlPath: string
39
45
  transcriptJsonPath: string
@@ -45,6 +51,21 @@ export interface StandaloneTranscriptExportResult {
45
51
  uploadedFileCount: number
46
52
  }
47
53
 
54
+ export interface StandaloneTranscriptExportFailureResult {
55
+ ok: false
56
+ error: string
57
+ outputDir: string
58
+ transcriptJsonPath: string
59
+ transcriptFileName: string
60
+ transcriptJson: string
61
+ shareSlug: string
62
+ shareUrl: string
63
+ }
64
+
65
+ export type StandaloneTranscriptExportCommandResult =
66
+ | StandaloneTranscriptExportResult
67
+ | StandaloneTranscriptExportFailureResult
68
+
48
69
  export interface QueuedChatMessage {
49
70
  id: string
50
71
  content: string
@@ -116,6 +137,17 @@ export interface ProviderModelOptionsByProvider {
116
137
  codex: CodexModelOptions
117
138
  }
118
139
 
140
+ export interface ProviderPreference<TModelOptions> {
141
+ model: string
142
+ modelOptions: TModelOptions
143
+ planMode: boolean
144
+ }
145
+
146
+ export type ChatProviderPreferences = {
147
+ claude: ProviderPreference<ClaudeModelOptions>
148
+ codex: ProviderPreference<CodexModelOptions>
149
+ }
150
+
119
151
  export type ModelOptions = Partial<{
120
152
  [K in AgentProvider]: Partial<ProviderModelOptionsByProvider[K]>
121
153
  }>
@@ -338,10 +370,39 @@ export interface LocalProjectsSnapshot {
338
370
 
339
371
  export interface AppSettingsSnapshot {
340
372
  analyticsEnabled: boolean
373
+ browserSettingsMigrated: boolean
374
+ theme: AppThemePreference
375
+ chatSoundPreference: ChatSoundPreference
376
+ chatSoundId: ChatSoundId
377
+ terminal: {
378
+ scrollbackLines: number
379
+ minColumnWidth: number
380
+ }
381
+ editor: {
382
+ preset: EditorPreset
383
+ commandTemplate: string
384
+ }
385
+ defaultProvider: DefaultProviderPreference
386
+ providerDefaults: ChatProviderPreferences
341
387
  warning: string | null
342
388
  filePathDisplay: string
343
389
  }
344
390
 
391
+ export interface AppSettingsPatch {
392
+ analyticsEnabled?: boolean
393
+ browserSettingsMigrated?: boolean
394
+ theme?: AppThemePreference
395
+ chatSoundPreference?: ChatSoundPreference
396
+ chatSoundId?: ChatSoundId
397
+ terminal?: Partial<AppSettingsSnapshot["terminal"]>
398
+ editor?: Partial<AppSettingsSnapshot["editor"]>
399
+ defaultProvider?: DefaultProviderPreference
400
+ providerDefaults?: {
401
+ claude?: Partial<ProviderPreference<ClaudeModelOptions>>
402
+ codex?: Partial<ProviderPreference<CodexModelOptions>>
403
+ }
404
+ }
405
+
345
406
  export interface LlmProviderFile {
346
407
  provider?: LlmProviderKind
347
408
  apiKey?: string