@jonsoc/app 1.1.34

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 (139) hide show
  1. package/AGENTS.md +30 -0
  2. package/README.md +51 -0
  3. package/bunfig.toml +2 -0
  4. package/e2e/context.spec.ts +45 -0
  5. package/e2e/file-open.spec.ts +23 -0
  6. package/e2e/file-viewer.spec.ts +35 -0
  7. package/e2e/fixtures.ts +40 -0
  8. package/e2e/home.spec.ts +21 -0
  9. package/e2e/model-picker.spec.ts +43 -0
  10. package/e2e/navigation.spec.ts +9 -0
  11. package/e2e/palette.spec.ts +15 -0
  12. package/e2e/prompt-mention.spec.ts +26 -0
  13. package/e2e/prompt-slash-open.spec.ts +22 -0
  14. package/e2e/prompt.spec.ts +62 -0
  15. package/e2e/session.spec.ts +21 -0
  16. package/e2e/settings.spec.ts +44 -0
  17. package/e2e/sidebar.spec.ts +21 -0
  18. package/e2e/terminal-init.spec.ts +25 -0
  19. package/e2e/terminal.spec.ts +16 -0
  20. package/e2e/tsconfig.json +8 -0
  21. package/e2e/utils.ts +38 -0
  22. package/happydom.ts +75 -0
  23. package/index.html +23 -0
  24. package/package.json +72 -0
  25. package/playwright.config.ts +43 -0
  26. package/public/_headers +17 -0
  27. package/public/apple-touch-icon-v3.png +1 -0
  28. package/public/apple-touch-icon.png +1 -0
  29. package/public/favicon-96x96-v3.png +1 -0
  30. package/public/favicon-96x96.png +1 -0
  31. package/public/favicon-v3.ico +1 -0
  32. package/public/favicon-v3.svg +1 -0
  33. package/public/favicon.ico +1 -0
  34. package/public/favicon.svg +1 -0
  35. package/public/oc-theme-preload.js +28 -0
  36. package/public/site.webmanifest +1 -0
  37. package/public/social-share-zen.png +1 -0
  38. package/public/social-share.png +1 -0
  39. package/public/web-app-manifest-192x192.png +1 -0
  40. package/public/web-app-manifest-512x512.png +1 -0
  41. package/script/e2e-local.ts +143 -0
  42. package/src/addons/serialize.test.ts +319 -0
  43. package/src/addons/serialize.ts +591 -0
  44. package/src/app.tsx +150 -0
  45. package/src/components/dialog-connect-provider.tsx +428 -0
  46. package/src/components/dialog-edit-project.tsx +259 -0
  47. package/src/components/dialog-fork.tsx +104 -0
  48. package/src/components/dialog-manage-models.tsx +59 -0
  49. package/src/components/dialog-select-directory.tsx +208 -0
  50. package/src/components/dialog-select-file.tsx +196 -0
  51. package/src/components/dialog-select-mcp.tsx +96 -0
  52. package/src/components/dialog-select-model-unpaid.tsx +130 -0
  53. package/src/components/dialog-select-model.tsx +162 -0
  54. package/src/components/dialog-select-provider.tsx +70 -0
  55. package/src/components/dialog-select-server.tsx +249 -0
  56. package/src/components/dialog-settings.tsx +112 -0
  57. package/src/components/file-tree.tsx +112 -0
  58. package/src/components/link.tsx +17 -0
  59. package/src/components/model-tooltip.tsx +91 -0
  60. package/src/components/prompt-input.tsx +2076 -0
  61. package/src/components/session/index.ts +5 -0
  62. package/src/components/session/session-context-tab.tsx +428 -0
  63. package/src/components/session/session-header.tsx +343 -0
  64. package/src/components/session/session-new-view.tsx +93 -0
  65. package/src/components/session/session-sortable-tab.tsx +56 -0
  66. package/src/components/session/session-sortable-terminal-tab.tsx +187 -0
  67. package/src/components/session-context-usage.tsx +113 -0
  68. package/src/components/session-lsp-indicator.tsx +42 -0
  69. package/src/components/session-mcp-indicator.tsx +34 -0
  70. package/src/components/settings-agents.tsx +15 -0
  71. package/src/components/settings-commands.tsx +15 -0
  72. package/src/components/settings-general.tsx +306 -0
  73. package/src/components/settings-keybinds.tsx +437 -0
  74. package/src/components/settings-mcp.tsx +15 -0
  75. package/src/components/settings-models.tsx +15 -0
  76. package/src/components/settings-permissions.tsx +234 -0
  77. package/src/components/settings-providers.tsx +15 -0
  78. package/src/components/terminal.tsx +315 -0
  79. package/src/components/titlebar.tsx +156 -0
  80. package/src/context/command.tsx +308 -0
  81. package/src/context/comments.tsx +140 -0
  82. package/src/context/file.tsx +409 -0
  83. package/src/context/global-sdk.tsx +106 -0
  84. package/src/context/global-sync.tsx +898 -0
  85. package/src/context/language.tsx +161 -0
  86. package/src/context/layout-scroll.test.ts +73 -0
  87. package/src/context/layout-scroll.ts +118 -0
  88. package/src/context/layout.tsx +648 -0
  89. package/src/context/local.tsx +578 -0
  90. package/src/context/notification.tsx +173 -0
  91. package/src/context/permission.tsx +167 -0
  92. package/src/context/platform.tsx +59 -0
  93. package/src/context/prompt.tsx +245 -0
  94. package/src/context/sdk.tsx +48 -0
  95. package/src/context/server.tsx +214 -0
  96. package/src/context/settings.tsx +166 -0
  97. package/src/context/sync.tsx +320 -0
  98. package/src/context/terminal.tsx +267 -0
  99. package/src/custom-elements.d.ts +17 -0
  100. package/src/entry.tsx +76 -0
  101. package/src/env.d.ts +8 -0
  102. package/src/hooks/use-providers.ts +31 -0
  103. package/src/i18n/ar.ts +656 -0
  104. package/src/i18n/br.ts +667 -0
  105. package/src/i18n/da.ts +582 -0
  106. package/src/i18n/de.ts +591 -0
  107. package/src/i18n/en.ts +665 -0
  108. package/src/i18n/es.ts +585 -0
  109. package/src/i18n/fr.ts +592 -0
  110. package/src/i18n/ja.ts +579 -0
  111. package/src/i18n/ko.ts +580 -0
  112. package/src/i18n/no.ts +602 -0
  113. package/src/i18n/pl.ts +661 -0
  114. package/src/i18n/ru.ts +664 -0
  115. package/src/i18n/zh.ts +574 -0
  116. package/src/i18n/zht.ts +570 -0
  117. package/src/index.css +57 -0
  118. package/src/index.ts +2 -0
  119. package/src/pages/directory-layout.tsx +57 -0
  120. package/src/pages/error.tsx +290 -0
  121. package/src/pages/home.tsx +125 -0
  122. package/src/pages/layout.tsx +2599 -0
  123. package/src/pages/session.tsx +2505 -0
  124. package/src/sst-env.d.ts +10 -0
  125. package/src/utils/dom.ts +51 -0
  126. package/src/utils/id.ts +99 -0
  127. package/src/utils/index.ts +1 -0
  128. package/src/utils/perf.ts +135 -0
  129. package/src/utils/persist.ts +377 -0
  130. package/src/utils/prompt.ts +203 -0
  131. package/src/utils/same.ts +6 -0
  132. package/src/utils/solid-dnd.tsx +55 -0
  133. package/src/utils/sound.ts +110 -0
  134. package/src/utils/speech.ts +302 -0
  135. package/src/utils/worktree.ts +58 -0
  136. package/sst-env.d.ts +9 -0
  137. package/tsconfig.json +26 -0
  138. package/vite.config.ts +15 -0
  139. package/vite.js +26 -0
@@ -0,0 +1,113 @@
1
+ import { Match, Show, Switch, createMemo } from "solid-js"
2
+ import { Tooltip } from "@jonsoc/ui/tooltip"
3
+ import { ProgressCircle } from "@jonsoc/ui/progress-circle"
4
+ import { Button } from "@jonsoc/ui/button"
5
+ import { useParams } from "@solidjs/router"
6
+ import { AssistantMessage } from "@jonsoc/sdk/v2/client"
7
+
8
+ import { useLayout } from "@/context/layout"
9
+ import { useSync } from "@/context/sync"
10
+ import { useLanguage } from "@/context/language"
11
+
12
+ interface SessionContextUsageProps {
13
+ variant?: "button" | "indicator"
14
+ }
15
+
16
+ export function SessionContextUsage(props: SessionContextUsageProps) {
17
+ const sync = useSync()
18
+ const params = useParams()
19
+ const layout = useLayout()
20
+ const language = useLanguage()
21
+
22
+ const variant = createMemo(() => props.variant ?? "button")
23
+ const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
24
+ const tabs = createMemo(() => layout.tabs(sessionKey))
25
+ const view = createMemo(() => layout.view(sessionKey))
26
+ const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
27
+
28
+ const cost = createMemo(() => {
29
+ const locale = language.locale()
30
+ const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
31
+ return new Intl.NumberFormat(locale, {
32
+ style: "currency",
33
+ currency: "USD",
34
+ }).format(total)
35
+ })
36
+
37
+ const context = createMemo(() => {
38
+ const locale = language.locale()
39
+ const last = messages().findLast((x) => {
40
+ if (x.role !== "assistant") return false
41
+ const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
42
+ return total > 0
43
+ }) as AssistantMessage
44
+ if (!last) return
45
+ const total =
46
+ last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
47
+ const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
48
+ return {
49
+ tokens: total.toLocaleString(locale),
50
+ percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
51
+ }
52
+ })
53
+
54
+ const openContext = () => {
55
+ if (!params.id) return
56
+ view().reviewPanel.open()
57
+ tabs().open("context")
58
+ tabs().setActive("context")
59
+ }
60
+
61
+ const circle = () => (
62
+ <div class="p-1">
63
+ <ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
64
+ </div>
65
+ )
66
+
67
+ const tooltipValue = () => (
68
+ <div>
69
+ <Show when={context()}>
70
+ {(ctx) => (
71
+ <>
72
+ <div class="flex items-center gap-2">
73
+ <span class="text-text-invert-strong">{ctx().tokens}</span>
74
+ <span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
75
+ </div>
76
+ <div class="flex items-center gap-2">
77
+ <span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span>
78
+ <span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
79
+ </div>
80
+ </>
81
+ )}
82
+ </Show>
83
+ <div class="flex items-center gap-2">
84
+ <span class="text-text-invert-strong">{cost()}</span>
85
+ <span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
86
+ </div>
87
+ <Show when={variant() === "button"}>
88
+ <div class="text-11-regular text-text-invert-base mt-1">{language.t("context.usage.clickToView")}</div>
89
+ </Show>
90
+ </div>
91
+ )
92
+
93
+ return (
94
+ <Show when={params.id}>
95
+ <Tooltip value={tooltipValue()} placement="top">
96
+ <Switch>
97
+ <Match when={variant() === "indicator"}>{circle()}</Match>
98
+ <Match when={true}>
99
+ <Button
100
+ type="button"
101
+ variant="ghost"
102
+ class="size-6"
103
+ onClick={openContext}
104
+ aria-label={language.t("context.usage.view")}
105
+ >
106
+ {circle()}
107
+ </Button>
108
+ </Match>
109
+ </Switch>
110
+ </Tooltip>
111
+ </Show>
112
+ )
113
+ }
@@ -0,0 +1,42 @@
1
+ import { createMemo, Show } from "solid-js"
2
+ import { useSync } from "@/context/sync"
3
+ import { useLanguage } from "@/context/language"
4
+ import { Tooltip } from "@jonsoc/ui/tooltip"
5
+
6
+ export function SessionLspIndicator() {
7
+ const sync = useSync()
8
+ const language = useLanguage()
9
+
10
+ const lspStats = createMemo(() => {
11
+ const lsp = sync.data.lsp ?? []
12
+ const connected = lsp.filter((s) => s.status === "connected").length
13
+ const hasError = lsp.some((s) => s.status === "error")
14
+ const total = lsp.length
15
+ return { connected, hasError, total }
16
+ })
17
+
18
+ const tooltipContent = createMemo(() => {
19
+ const lsp = sync.data.lsp ?? []
20
+ if (lsp.length === 0) return language.t("lsp.tooltip.none")
21
+ return lsp.map((s) => s.name).join(", ")
22
+ })
23
+
24
+ return (
25
+ <Show when={lspStats().total > 0}>
26
+ <Tooltip placement="top" value={tooltipContent()}>
27
+ <div class="flex items-center gap-1 px-2 cursor-default select-none">
28
+ <div
29
+ classList={{
30
+ "size-1.5 rounded-full": true,
31
+ "bg-icon-critical-base": lspStats().hasError,
32
+ "bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
33
+ }}
34
+ />
35
+ <span class="text-12-regular text-text-weak">
36
+ {language.t("lsp.label.connected", { count: lspStats().connected })}
37
+ </span>
38
+ </div>
39
+ </Tooltip>
40
+ </Show>
41
+ )
42
+ }
@@ -0,0 +1,34 @@
1
+ import { createMemo, Show } from "solid-js"
2
+ import { Button } from "@jonsoc/ui/button"
3
+ import { useDialog } from "@jonsoc/ui/context/dialog"
4
+ import { useSync } from "@/context/sync"
5
+ import { DialogSelectMcp } from "@/components/dialog-select-mcp"
6
+
7
+ export function SessionMcpIndicator() {
8
+ const sync = useSync()
9
+ const dialog = useDialog()
10
+
11
+ const mcpStats = createMemo(() => {
12
+ const mcp = sync.data.mcp ?? {}
13
+ const entries = Object.entries(mcp)
14
+ const enabled = entries.filter(([, status]) => status.status === "connected").length
15
+ const failed = entries.some(([, status]) => status.status === "failed")
16
+ const total = entries.length
17
+ return { enabled, failed, total }
18
+ })
19
+
20
+ return (
21
+ <Show when={mcpStats().total > 0}>
22
+ <Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
23
+ <div
24
+ classList={{
25
+ "size-1.5 rounded-full": true,
26
+ "bg-icon-critical-base": mcpStats().failed,
27
+ "bg-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
28
+ }}
29
+ />
30
+ <span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
31
+ </Button>
32
+ </Show>
33
+ )
34
+ }
@@ -0,0 +1,15 @@
1
+ import { Component } from "solid-js"
2
+ import { useLanguage } from "@/context/language"
3
+
4
+ export const SettingsAgents: Component = () => {
5
+ const language = useLanguage()
6
+
7
+ return (
8
+ <div class="flex flex-col h-full overflow-y-auto">
9
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
10
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.agents.title")}</h2>
11
+ <p class="text-14-regular text-text-weak">{language.t("settings.agents.description")}</p>
12
+ </div>
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,15 @@
1
+ import { Component } from "solid-js"
2
+ import { useLanguage } from "@/context/language"
3
+
4
+ export const SettingsCommands: Component = () => {
5
+ const language = useLanguage()
6
+
7
+ return (
8
+ <div class="flex flex-col h-full overflow-y-auto">
9
+ <div class="flex flex-col gap-6 p-6 max-w-[600px]">
10
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.commands.title")}</h2>
11
+ <p class="text-14-regular text-text-weak">{language.t("settings.commands.description")}</p>
12
+ </div>
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,306 @@
1
+ import { Component, createMemo, type JSX } from "solid-js"
2
+ import { Select } from "@jonsoc/ui/select"
3
+ import { Switch } from "@jonsoc/ui/switch"
4
+ import { useTheme, type ColorScheme } from "@jonsoc/ui/theme"
5
+ import { useLanguage } from "@/context/language"
6
+ import { useSettings, monoFontFamily } from "@/context/settings"
7
+ import { playSound, SOUND_OPTIONS } from "@/utils/sound"
8
+ import { Link } from "./link"
9
+
10
+ export const SettingsGeneral: Component = () => {
11
+ const theme = useTheme()
12
+ const language = useLanguage()
13
+ const settings = useSettings()
14
+
15
+ const themeOptions = createMemo(() =>
16
+ Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
17
+ )
18
+
19
+ const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
20
+ { value: "system", label: language.t("theme.scheme.system") },
21
+ { value: "light", label: language.t("theme.scheme.light") },
22
+ { value: "dark", label: language.t("theme.scheme.dark") },
23
+ ])
24
+
25
+ const languageOptions = createMemo(() =>
26
+ language.locales.map((locale) => ({
27
+ value: locale,
28
+ label: language.label(locale),
29
+ })),
30
+ )
31
+
32
+ const fontOptions = [
33
+ { value: "ibm-plex-mono", label: "font.option.ibmPlexMono" },
34
+ { value: "cascadia-code", label: "font.option.cascadiaCode" },
35
+ { value: "fira-code", label: "font.option.firaCode" },
36
+ { value: "hack", label: "font.option.hack" },
37
+ { value: "inconsolata", label: "font.option.inconsolata" },
38
+ { value: "intel-one-mono", label: "font.option.intelOneMono" },
39
+ { value: "jetbrains-mono", label: "font.option.jetbrainsMono" },
40
+ { value: "meslo-lgs", label: "font.option.mesloLgs" },
41
+ { value: "roboto-mono", label: "font.option.robotoMono" },
42
+ { value: "source-code-pro", label: "font.option.sourceCodePro" },
43
+ { value: "ubuntu-mono", label: "font.option.ubuntuMono" },
44
+ ] as const
45
+ const fontOptionsList = [...fontOptions]
46
+
47
+ const soundOptions = [...SOUND_OPTIONS]
48
+
49
+ return (
50
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
51
+ <div
52
+ class="sticky top-0 z-10"
53
+ style={{
54
+ background:
55
+ "linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
56
+ }}
57
+ >
58
+ <div class="flex flex-col gap-1 pt-6 pb-8">
59
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="flex flex-col gap-8 w-full">
64
+ {/* Appearance Section */}
65
+ <div class="flex flex-col gap-1">
66
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
67
+
68
+ <div class="bg-surface-raised-base px-4 rounded-lg">
69
+ <SettingsRow
70
+ title={language.t("settings.general.row.language.title")}
71
+ description={language.t("settings.general.row.language.description")}
72
+ >
73
+ <Select
74
+ options={languageOptions()}
75
+ current={languageOptions().find((o) => o.value === language.locale())}
76
+ value={(o) => o.value}
77
+ label={(o) => o.label}
78
+ onSelect={(option) => option && language.setLocale(option.value)}
79
+ variant="secondary"
80
+ size="small"
81
+ triggerVariant="settings"
82
+ />
83
+ </SettingsRow>
84
+
85
+ <SettingsRow
86
+ title={language.t("settings.general.row.appearance.title")}
87
+ description={language.t("settings.general.row.appearance.description")}
88
+ >
89
+ <Select
90
+ options={colorSchemeOptions()}
91
+ current={colorSchemeOptions().find((o) => o.value === theme.colorScheme())}
92
+ value={(o) => o.value}
93
+ label={(o) => o.label}
94
+ onSelect={(option) => option && theme.setColorScheme(option.value)}
95
+ onHighlight={(option) => {
96
+ if (!option) return
97
+ theme.previewColorScheme(option.value)
98
+ return () => theme.cancelPreview()
99
+ }}
100
+ variant="secondary"
101
+ size="small"
102
+ triggerVariant="settings"
103
+ />
104
+ </SettingsRow>
105
+
106
+ <SettingsRow
107
+ title={language.t("settings.general.row.theme.title")}
108
+ description={
109
+ <>
110
+ {language.t("settings.general.row.theme.description")}{" "}
111
+ <Link href="https://jonsoc.com/docs/themes/">{language.t("common.learnMore")}</Link>
112
+ </>
113
+ }
114
+ >
115
+ <Select
116
+ options={themeOptions()}
117
+ current={themeOptions().find((o) => o.id === theme.themeId())}
118
+ value={(o) => o.id}
119
+ label={(o) => o.name}
120
+ onSelect={(option) => {
121
+ if (!option) return
122
+ theme.setTheme(option.id)
123
+ }}
124
+ onHighlight={(option) => {
125
+ if (!option) return
126
+ theme.previewTheme(option.id)
127
+ return () => theme.cancelPreview()
128
+ }}
129
+ variant="secondary"
130
+ size="small"
131
+ triggerVariant="settings"
132
+ />
133
+ </SettingsRow>
134
+
135
+ <SettingsRow
136
+ title={language.t("settings.general.row.font.title")}
137
+ description={language.t("settings.general.row.font.description")}
138
+ >
139
+ <Select
140
+ options={fontOptionsList}
141
+ current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
142
+ value={(o) => o.value}
143
+ label={(o) => language.t(o.label)}
144
+ onSelect={(option) => option && settings.appearance.setFont(option.value)}
145
+ variant="secondary"
146
+ size="small"
147
+ triggerVariant="settings"
148
+ triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }}
149
+ >
150
+ {(option) => (
151
+ <span style={{ "font-family": monoFontFamily(option?.value) }}>
152
+ {option ? language.t(option.label) : ""}
153
+ </span>
154
+ )}
155
+ </Select>
156
+ </SettingsRow>
157
+
158
+ <SettingsRow
159
+ title={language.t("settings.general.row.navigatorAlwaysOpen.title")}
160
+ description={language.t("settings.general.row.navigatorAlwaysOpen.description")}
161
+ >
162
+ <Switch
163
+ checked={settings.general.navigatorAlwaysOpen()}
164
+ onChange={(checked) => settings.general.setNavigatorAlwaysOpen(checked)}
165
+ />
166
+ </SettingsRow>
167
+ </div>
168
+ </div>
169
+
170
+ {/* System notifications Section */}
171
+ <div class="flex flex-col gap-1">
172
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
173
+
174
+ <div class="bg-surface-raised-base px-4 rounded-lg">
175
+ <SettingsRow
176
+ title={language.t("settings.general.notifications.agent.title")}
177
+ description={language.t("settings.general.notifications.agent.description")}
178
+ >
179
+ <Switch
180
+ checked={settings.notifications.agent()}
181
+ onChange={(checked) => settings.notifications.setAgent(checked)}
182
+ />
183
+ </SettingsRow>
184
+
185
+ <SettingsRow
186
+ title={language.t("settings.general.notifications.permissions.title")}
187
+ description={language.t("settings.general.notifications.permissions.description")}
188
+ >
189
+ <Switch
190
+ checked={settings.notifications.permissions()}
191
+ onChange={(checked) => settings.notifications.setPermissions(checked)}
192
+ />
193
+ </SettingsRow>
194
+
195
+ <SettingsRow
196
+ title={language.t("settings.general.notifications.errors.title")}
197
+ description={language.t("settings.general.notifications.errors.description")}
198
+ >
199
+ <Switch
200
+ checked={settings.notifications.errors()}
201
+ onChange={(checked) => settings.notifications.setErrors(checked)}
202
+ />
203
+ </SettingsRow>
204
+ </div>
205
+ </div>
206
+
207
+ {/* Sound effects Section */}
208
+ <div class="flex flex-col gap-1">
209
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
210
+
211
+ <div class="bg-surface-raised-base px-4 rounded-lg">
212
+ <SettingsRow
213
+ title={language.t("settings.general.sounds.agent.title")}
214
+ description={language.t("settings.general.sounds.agent.description")}
215
+ >
216
+ <Select
217
+ options={soundOptions}
218
+ current={soundOptions.find((o) => o.id === settings.sounds.agent())}
219
+ value={(o) => o.id}
220
+ label={(o) => language.t(o.label)}
221
+ onHighlight={(option) => {
222
+ if (!option) return
223
+ playSound(option.src)
224
+ }}
225
+ onSelect={(option) => {
226
+ if (!option) return
227
+ settings.sounds.setAgent(option.id)
228
+ playSound(option.src)
229
+ }}
230
+ variant="secondary"
231
+ size="small"
232
+ triggerVariant="settings"
233
+ />
234
+ </SettingsRow>
235
+
236
+ <SettingsRow
237
+ title={language.t("settings.general.sounds.permissions.title")}
238
+ description={language.t("settings.general.sounds.permissions.description")}
239
+ >
240
+ <Select
241
+ options={soundOptions}
242
+ current={soundOptions.find((o) => o.id === settings.sounds.permissions())}
243
+ value={(o) => o.id}
244
+ label={(o) => language.t(o.label)}
245
+ onHighlight={(option) => {
246
+ if (!option) return
247
+ playSound(option.src)
248
+ }}
249
+ onSelect={(option) => {
250
+ if (!option) return
251
+ settings.sounds.setPermissions(option.id)
252
+ playSound(option.src)
253
+ }}
254
+ variant="secondary"
255
+ size="small"
256
+ triggerVariant="settings"
257
+ />
258
+ </SettingsRow>
259
+
260
+ <SettingsRow
261
+ title={language.t("settings.general.sounds.errors.title")}
262
+ description={language.t("settings.general.sounds.errors.description")}
263
+ >
264
+ <Select
265
+ options={soundOptions}
266
+ current={soundOptions.find((o) => o.id === settings.sounds.errors())}
267
+ value={(o) => o.id}
268
+ label={(o) => language.t(o.label)}
269
+ onHighlight={(option) => {
270
+ if (!option) return
271
+ playSound(option.src)
272
+ }}
273
+ onSelect={(option) => {
274
+ if (!option) return
275
+ settings.sounds.setErrors(option.id)
276
+ playSound(option.src)
277
+ }}
278
+ variant="secondary"
279
+ size="small"
280
+ triggerVariant="settings"
281
+ />
282
+ </SettingsRow>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ )
288
+ }
289
+
290
+ interface SettingsRowProps {
291
+ title: string
292
+ description: string | JSX.Element
293
+ children: JSX.Element
294
+ }
295
+
296
+ const SettingsRow: Component<SettingsRowProps> = (props) => {
297
+ return (
298
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
299
+ <div class="flex flex-col gap-0.5">
300
+ <span class="text-14-medium text-text-strong">{props.title}</span>
301
+ <span class="text-12-regular text-text-weak">{props.description}</span>
302
+ </div>
303
+ <div class="flex-shrink-0">{props.children}</div>
304
+ </div>
305
+ )
306
+ }