codebay 0.0.1 → 0.2.1

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 (204) hide show
  1. package/.mochi/import-css/index-0nwv3654.css +18 -0
  2. package/.mochi/import-css/index-xndx4980.css +54 -0
  3. package/.mochi/manifest.json +1612 -0
  4. package/.mochi/server-island.js +1 -0
  5. package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-7xgbmxxg.js +2 -0
  7. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-3cnbjpz2.js +13 -0
  8. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-2ntmdm3e.js +1 -0
  9. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-t08spym1.js +23 -0
  10. package/.mochi/svelte-client/chunk-04qv4w6w.js +2 -0
  11. package/.mochi/svelte-client/chunk-5jnepvzx.js +2 -0
  12. package/.mochi/svelte-client/chunk-hcztrd75.js +1 -0
  13. package/.mochi/svelte-client/chunk-qxwt25hn.js +4 -0
  14. package/.mochi/svelte-client/chunk-sjmcgh7f.js +2 -0
  15. package/.mochi/svelte-client/chunk-tr67wtwn.js +2 -0
  16. package/.mochi/svelte-compile/App-f0q5n4rk.server.js +1168 -0
  17. package/.mochi/svelte-compile/Avatars-p8gfef7x.server.js +332 -0
  18. package/.mochi/svelte-compile/ClientStats-z3tn9ad6.server.js +99 -0
  19. package/.mochi/svelte-compile/DefaultError-wh77vebf.server.js +52 -0
  20. package/.mochi/svelte-compile/Instance-atjwsczx.server.js +356 -0
  21. package/.mochi/svelte-compile/Settings-q88xw6h8.server.js +1173 -0
  22. package/.mochi/svelte-compile/chunk-4kwy539a.js +14 -0
  23. package/.mochi/svelte-compile/chunk-9bjk7yx6.js +81 -0
  24. package/.mochi/svelte-compile/chunk-btmcxhqy.js +58 -0
  25. package/.mochi/svelte-compile/chunk-sxqys3nw.js +1668 -0
  26. package/.mochi/svelte-compile/chunk-v6796ga4.js +3295 -0
  27. package/.mochi/svelte-compile/chunk-we0ggtws.js +34 -0
  28. package/.mochi/svelte-css/AppBar-3bxinv4pabv8l.css +1 -0
  29. package/.mochi/svelte-css/AppShell-36mpg5smnbmuw.css +1 -0
  30. package/.mochi/svelte-css/Avatar-24th63awm9mna.css +1 -0
  31. package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +1 -0
  32. package/.mochi/svelte-css/AvatarsView-3hq37u4dgtljn.css +1 -0
  33. package/.mochi/svelte-css/BackLink-2wlu0eovz9j86.css +1 -0
  34. package/.mochi/svelte-css/BranchBox-1yqu1q2mgnjwz.css +1 -0
  35. package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +1 -0
  36. package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +1 -0
  37. package/.mochi/svelte-css/CheckmarkIcon-3qlazj2oc50jc.css +1 -0
  38. package/.mochi/svelte-css/ClientStats-19qzckq40p2q9.css +1 -0
  39. package/.mochi/svelte-css/CoinButton-1rej3sh3tkjud.css +1 -0
  40. package/.mochi/svelte-css/CredMenu-9hwgeptp7crb.css +1 -0
  41. package/.mochi/svelte-css/DashboardView-1cy4xpqhja9sx.css +1 -0
  42. package/.mochi/svelte-css/DefaultError-2b0daamydxgb2.css +1 -0
  43. package/.mochi/svelte-css/ErrorIcon-2x0tzsqxskw28.css +1 -0
  44. package/.mochi/svelte-css/FolderBrowser-1x428zb3f2kti.css +1 -0
  45. package/.mochi/svelte-css/HealthBox-er2her0jukwl.css +1 -0
  46. package/.mochi/svelte-css/IdeBar-2qoqi2ya9zdn3.css +1 -0
  47. package/.mochi/svelte-css/IdeLoader-26rlko8e00j3g.css +1 -0
  48. package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +1 -0
  49. package/.mochi/svelte-css/InstanceView-wq85bxtusont.css +1 -0
  50. package/.mochi/svelte-css/LoaderIcon-soq5wt3p05ye.css +1 -0
  51. package/.mochi/svelte-css/PortsBox-1t25b5yg7jva3.css +1 -0
  52. package/.mochi/svelte-css/SettingsCog-jftyadvo64qo.css +1 -0
  53. package/.mochi/svelte-css/SettingsView-1nfdwnm16cpp2.css +1 -0
  54. package/.mochi/svelte-css/Skeleton-8k69usiju43h.css +1 -0
  55. package/.mochi/svelte-css/StatusBadge-3mq0j7z6aq4j.css +1 -0
  56. package/.mochi/svelte-css/ThemePicker-1ir8jyocm8mvi.css +1 -0
  57. package/.mochi/svelte-css/ToastBar-3havgkmtofntq.css +1 -0
  58. package/.mochi/svelte-css/ToastIcon-2z5wrkc95khg0.css +1 -0
  59. package/.mochi/svelte-css/ToastMessage-2jfwb2og2lfa8.css +1 -0
  60. package/.mochi/svelte-css/ToastWrapper-1fbnym10rm2nv.css +1 -0
  61. package/.mochi/svelte-css/Toaster-kk60j193854n.css +1 -0
  62. package/.mochi/svelte-css/TopBar-2q87li6yt8ids.css +1 -0
  63. package/LICENSE +21 -0
  64. package/README.md +34 -3
  65. package/bin/codebay.ts +22 -0
  66. package/migrations/0001_init.sql +20 -0
  67. package/migrations/0002_port_forwards.sql +9 -0
  68. package/migrations/0003_options.sql +4 -0
  69. package/migrations/0004_instance_image.sql +1 -0
  70. package/package.json +87 -9
  71. package/public/sounds/done.wav +0 -0
  72. package/public/sounds/waiting.wav +0 -0
  73. package/src/__fixtures__/HelloWorld.svelte +4 -0
  74. package/src/additional-svelte-typings.d.ts +1 -0
  75. package/src/api.ts +61 -0
  76. package/src/avatars/anchor.ts +17 -0
  77. package/src/avatars/bear.ts +17 -0
  78. package/src/avatars/bee.ts +17 -0
  79. package/src/avatars/bob-marley.ts +17 -0
  80. package/src/avatars/car.ts +17 -0
  81. package/src/avatars/carpathians.ts +17 -0
  82. package/src/avatars/cat-body.ts +17 -0
  83. package/src/avatars/cat-sits.ts +17 -0
  84. package/src/avatars/cat.ts +17 -0
  85. package/src/avatars/celtic-cross.ts +17 -0
  86. package/src/avatars/cherry.ts +17 -0
  87. package/src/avatars/cotton.ts +17 -0
  88. package/src/avatars/crab.ts +17 -0
  89. package/src/avatars/crown.ts +17 -0
  90. package/src/avatars/diamond.ts +17 -0
  91. package/src/avatars/dog.ts +17 -0
  92. package/src/avatars/elephant.ts +17 -0
  93. package/src/avatars/fish.ts +17 -0
  94. package/src/avatars/fleur-de-lis.ts +17 -0
  95. package/src/avatars/flower.ts +17 -0
  96. package/src/avatars/fox.ts +17 -0
  97. package/src/avatars/frog.ts +17 -0
  98. package/src/avatars/ghost.ts +17 -0
  99. package/src/avatars/giraffe.ts +17 -0
  100. package/src/avatars/glasses.ts +17 -0
  101. package/src/avatars/heart.ts +17 -0
  102. package/src/avatars/index.ts +128 -0
  103. package/src/avatars/invader.ts +17 -0
  104. package/src/avatars/kangaroo.ts +17 -0
  105. package/src/avatars/key.ts +17 -0
  106. package/src/avatars/lightning.ts +17 -0
  107. package/src/avatars/mushroom.ts +17 -0
  108. package/src/avatars/name-prompts.ts +30 -0
  109. package/src/avatars/octopus.ts +17 -0
  110. package/src/avatars/owl.ts +17 -0
  111. package/src/avatars/penguin.ts +17 -0
  112. package/src/avatars/planet.ts +17 -0
  113. package/src/avatars/rabbit.ts +17 -0
  114. package/src/avatars/rat.ts +17 -0
  115. package/src/avatars/robot.ts +17 -0
  116. package/src/avatars/rocket.ts +17 -0
  117. package/src/avatars/sea.ts +17 -0
  118. package/src/avatars/serialize.ts +65 -0
  119. package/src/avatars/skull.ts +17 -0
  120. package/src/avatars/snail.ts +17 -0
  121. package/src/avatars/snake.ts +17 -0
  122. package/src/avatars/snakegame.ts +17 -0
  123. package/src/avatars/star.ts +17 -0
  124. package/src/avatars/svelte.ts +17 -0
  125. package/src/avatars/target.ts +17 -0
  126. package/src/avatars/tree.ts +17 -0
  127. package/src/avatars/types.ts +24 -0
  128. package/src/avatars/ukraine.ts +17 -0
  129. package/src/avatars/whale.ts +17 -0
  130. package/src/avatars/wheat.ts +17 -0
  131. package/src/avatars/yinyan.ts +17 -0
  132. package/src/components/AppBar.svelte +35 -0
  133. package/src/components/AppShell.svelte +302 -0
  134. package/src/components/Avatar.svelte +235 -0
  135. package/src/components/AvatarEditor.svelte +297 -0
  136. package/src/components/AvatarsView.svelte +115 -0
  137. package/src/components/BranchBox.svelte +32 -0
  138. package/src/components/Brand.svelte +41 -0
  139. package/src/components/Button.svelte +161 -0
  140. package/src/components/CoinButton.svelte +121 -0
  141. package/src/components/CredMenu.svelte +171 -0
  142. package/src/components/DashboardView.svelte +198 -0
  143. package/src/components/FolderBrowser.svelte +635 -0
  144. package/src/components/HealthBox.svelte +161 -0
  145. package/src/components/IdeBar.svelte +186 -0
  146. package/src/components/IdeLoader.svelte +135 -0
  147. package/src/components/InstanceCard.svelte +220 -0
  148. package/src/components/InstanceView.svelte +567 -0
  149. package/src/components/PortsBox.svelte +74 -0
  150. package/src/components/SettingsCog.svelte +22 -0
  151. package/src/components/SettingsView.svelte +1494 -0
  152. package/src/components/Skeleton.svelte +43 -0
  153. package/src/components/StatusBadge.svelte +54 -0
  154. package/src/components/ThemePicker.svelte +77 -0
  155. package/src/components/TopBar.svelte +64 -0
  156. package/src/components/UiShowcase.svelte +578 -0
  157. package/src/components/ui-showcase/ComponentDemo.svelte +85 -0
  158. package/src/container-injections/attention-hooks.ts +117 -0
  159. package/src/container-injections/claude-aliases.ts +43 -0
  160. package/src/container-injections/claude-code-credentials.ts +145 -0
  161. package/src/container-injections/claude-code-custom.ts +125 -0
  162. package/src/container-injections/claude-no-coauthor.ts +35 -0
  163. package/src/container-injections/claude-skip-permissions.ts +38 -0
  164. package/src/container-injections/claude-statusline.ts +109 -0
  165. package/src/container-injections/git-identity.ts +63 -0
  166. package/src/container-injections/git-safe-directory.ts +18 -0
  167. package/src/container-injections/github-credentials.ts +162 -0
  168. package/src/container-injections/host-env-vars.ts +108 -0
  169. package/src/container-injections/tmux.ts +67 -0
  170. package/src/fontsource.d.ts +3 -0
  171. package/src/index.ts +81 -0
  172. package/src/lib/auth.server.ts +95 -0
  173. package/src/lib/bridge.server.ts +34 -0
  174. package/src/lib/config.server.ts +140 -0
  175. package/src/lib/crypto.server.ts +12 -0
  176. package/src/lib/db.server.ts +196 -0
  177. package/src/lib/devcontainer.server.ts +511 -0
  178. package/src/lib/docker-client.server.ts +61 -0
  179. package/src/lib/docker.server.ts +139 -0
  180. package/src/lib/exec.server.ts +92 -0
  181. package/src/lib/git.server.ts +68 -0
  182. package/src/lib/health.server.ts +105 -0
  183. package/src/lib/injections.server.ts +72 -0
  184. package/src/lib/instances.server.ts +552 -0
  185. package/src/lib/links.ts +9 -0
  186. package/src/lib/picker.server.ts +46 -0
  187. package/src/lib/ports.server.ts +9 -0
  188. package/src/lib/proxy.server.ts +170 -0
  189. package/src/lib/repo-url.ts +60 -0
  190. package/src/lib/spawn.server.ts +14 -0
  191. package/src/lib/theme.server.ts +22 -0
  192. package/src/live.ts +50 -0
  193. package/src/pages/App.svelte +14 -0
  194. package/src/pages/Avatars.svelte +9 -0
  195. package/src/pages/Instance.svelte +9 -0
  196. package/src/pages/Settings.svelte +76 -0
  197. package/src/pages/UI.svelte +10 -0
  198. package/src/routes.ts +444 -0
  199. package/src/settings.ts +14 -0
  200. package/src/shell.html +100 -0
  201. package/src/sound.ts +69 -0
  202. package/src/theme.ts +24 -0
  203. package/src/types.ts +79 -0
  204. package/svelte.config.js +5 -0
@@ -0,0 +1,1494 @@
1
+ <script lang="ts">
2
+ import Container from '@lucide/svelte/icons/container';
3
+ import AppBar from './AppBar.svelte';
4
+ import Power from '@lucide/svelte/icons/power';
5
+ import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
6
+ import Volume2 from '@lucide/svelte/icons/volume-2';
7
+ import SunMoon from '@lucide/svelte/icons/sun-moon';
8
+ import ThemePicker from './ThemePicker.svelte';
9
+ import Layers from '@lucide/svelte/icons/layers';
10
+ import FolderMinus from '@lucide/svelte/icons/folder-minus';
11
+ import Trash2 from '@lucide/svelte/icons/trash-2';
12
+ import Hammer from '@lucide/svelte/icons/hammer';
13
+ import KeyRound from '@lucide/svelte/icons/key-round';
14
+ import Variable from '@lucide/svelte/icons/variable';
15
+ import Plus from '@lucide/svelte/icons/plus';
16
+ import X from '@lucide/svelte/icons/x';
17
+ import { Toaster } from 'svelte-french-toast';
18
+ import { flushSync } from 'svelte';
19
+ import { enhance } from 'mochi-framework';
20
+ import type { MochiEnhanceOptions } from 'mochi-framework';
21
+ import { soundEnabled, setSoundEnabled } from '../settings.ts';
22
+ import { playChime, unlockAudio } from '../sound.ts';
23
+ import Button from './Button.svelte';
24
+ import CoinButton from './CoinButton.svelte';
25
+
26
+ /** Every settings form action fails with the same `{ error }` shape. */
27
+ type ActionFailure = { error: string };
28
+
29
+ let {
30
+ defaultImage,
31
+ builtinImage,
32
+ disableBuildCache,
33
+ copyIgnorePatterns,
34
+ builtinCopyIgnore,
35
+ dockerArch,
36
+ manualTokensEnabled,
37
+ githubTokenSet,
38
+ claudeTokenSet,
39
+ customEndpointEnabled,
40
+ customEndpointBaseUrl,
41
+ customEndpointTokenSet,
42
+ customEndpointOpusModel,
43
+ customEndpointSonnetModel,
44
+ customEndpointHaikuModel,
45
+ customEndpointSmallFastModel,
46
+ customEndpointModel,
47
+ hostEnvVarsEnabled,
48
+ hostEnvVarNames,
49
+ hostEnvVarPresence,
50
+ version
51
+ }: {
52
+ defaultImage: string;
53
+ builtinImage: string;
54
+ disableBuildCache: boolean;
55
+ copyIgnorePatterns: string;
56
+ builtinCopyIgnore: string;
57
+ dockerArch: string | null;
58
+ manualTokensEnabled: boolean;
59
+ githubTokenSet: boolean;
60
+ claudeTokenSet: boolean;
61
+ customEndpointEnabled: boolean;
62
+ customEndpointBaseUrl: string;
63
+ customEndpointTokenSet: boolean;
64
+ customEndpointOpusModel: string;
65
+ customEndpointSonnetModel: string;
66
+ customEndpointHaikuModel: string;
67
+ customEndpointSmallFastModel: string;
68
+ customEndpointModel: string;
69
+ hostEnvVarsEnabled: boolean;
70
+ hostEnvVarNames: string[];
71
+ hostEnvVarPresence: Record<string, boolean>;
72
+ version: string;
73
+ } = $props();
74
+
75
+ // Defaults to on during SSR, where localStorage doesn't exist.
76
+ let sound = $state(soundEnabled());
77
+
78
+ let shuttingDown = $state(false);
79
+
80
+ /** Reused by every plain save form below; only the per-control state setters differ. */
81
+ function saveOpts<Success extends Record<string, unknown> = Record<string, unknown>>(handlers: {
82
+ setSaving: (v: boolean) => void;
83
+ setError: (v: string | null) => void;
84
+ setMsg?: (v: string | null) => void;
85
+ onSuccess: (data: Success | undefined) => void;
86
+ confirmMessage?: string;
87
+ }): MochiEnhanceOptions<Success, ActionFailure> {
88
+ return {
89
+ onPending: handlers.setSaving,
90
+ submit: ({ cancel }) => {
91
+ if (handlers.confirmMessage && !confirm(handlers.confirmMessage)) {
92
+ cancel();
93
+ return;
94
+ }
95
+ handlers.setError(null);
96
+ handlers.setMsg?.(null);
97
+ return ({ result }) => {
98
+ if (result.type === 'success') {
99
+ handlers.onSuccess(result.data);
100
+ } else if (result.type === 'failure') {
101
+ handlers.setError(result.data?.error ?? 'Request failed');
102
+ } else if (result.type === 'error') {
103
+ handlers.setError('Network error. Try again.');
104
+ }
105
+ };
106
+ }
107
+ };
108
+ }
109
+
110
+ /**
111
+ * The optimistic flip is free: `onchange` fires before submit, so `formData`
112
+ * already carries the new value. Failure reverts to the opposite of what was sent.
113
+ */
114
+ function toggleOpts<Success extends Record<string, unknown> = Record<string, unknown>>(handlers: {
115
+ set: (v: boolean) => void;
116
+ setSaving: (v: boolean) => void;
117
+ setError: (v: string | null) => void;
118
+ onSuccess?: (data: Success | undefined) => void;
119
+ }): MochiEnhanceOptions<Success, ActionFailure> {
120
+ return {
121
+ onPending: handlers.setSaving,
122
+ submit: ({ formData }) => {
123
+ const intended = formData.get('enabled') === 'on';
124
+ handlers.setError(null);
125
+ return ({ result }) => {
126
+ if (result.type === 'success') {
127
+ handlers.onSuccess?.(result.data);
128
+ return;
129
+ }
130
+ handlers.set(!intended);
131
+ handlers.setError(
132
+ result.type === 'failure'
133
+ ? (result.data?.error ?? 'Request failed')
134
+ : 'Network error. Try again.'
135
+ );
136
+ };
137
+ }
138
+ };
139
+ }
140
+
141
+ // svelte-ignore state_referenced_locally
142
+ let image = $state(defaultImage);
143
+ let savingImage = $state(false);
144
+ let imageError = $state<string | null>(null);
145
+ let imageSaved = $state(false);
146
+ let imageFormEl: HTMLFormElement | undefined;
147
+
148
+ const imageOpts = saveOpts<{ image: string }>({
149
+ setSaving: (v) => (savingImage = v),
150
+ setError: (v) => (imageError = v),
151
+ setMsg: (v) => (imageSaved = !!v),
152
+ onSuccess: (data) => {
153
+ image = data?.image ?? image;
154
+ imageSaved = true;
155
+ }
156
+ });
157
+
158
+ // Setting the bound value then resubmitting keeps this a one-form control.
159
+ function resetImage() {
160
+ // flushSync forces the bind:value DOM update to happen before requestSubmit
161
+ // reads the input's value via FormData — otherwise it would submit the stale value.
162
+ flushSync(() => (image = builtinImage));
163
+ imageFormEl?.requestSubmit();
164
+ }
165
+
166
+ // Unlike the image field, an empty value is valid here — it means "copy everything".
167
+ // svelte-ignore state_referenced_locally
168
+ let copyIgnore = $state(copyIgnorePatterns);
169
+ let savingCopyIgnore = $state(false);
170
+ let copyIgnoreError = $state<string | null>(null);
171
+ let copyIgnoreSaved = $state(false);
172
+ let copyIgnoreFormEl: HTMLFormElement | undefined;
173
+
174
+ const copyIgnoreOpts = saveOpts<{ patterns: string }>({
175
+ setSaving: (v) => (savingCopyIgnore = v),
176
+ setError: (v) => (copyIgnoreError = v),
177
+ setMsg: (v) => (copyIgnoreSaved = !!v),
178
+ onSuccess: (data) => {
179
+ copyIgnore = data?.patterns ?? copyIgnore;
180
+ copyIgnoreSaved = true;
181
+ }
182
+ });
183
+
184
+ function resetCopyIgnore() {
185
+ flushSync(() => (copyIgnore = builtinCopyIgnore));
186
+ copyIgnoreFormEl?.requestSubmit();
187
+ }
188
+
189
+ // DB-backed rather than localStorage, so it initializes from the prop.
190
+ // svelte-ignore state_referenced_locally
191
+ let noCache = $state(disableBuildCache);
192
+ let savingCache = $state(false);
193
+ let cacheError = $state<string | null>(null);
194
+
195
+ const buildCacheToggleOpts = toggleOpts({
196
+ set: (v) => (noCache = v),
197
+ setSaving: (v) => (savingCache = v),
198
+ setError: (v) => (cacheError = v)
199
+ });
200
+
201
+ let clearing = $state(false);
202
+ let clearMsg = $state<string | null>(null);
203
+ let clearError = $state<string | null>(null);
204
+
205
+ function formatBytes(n: number): string {
206
+ if (n <= 0) return '0 B';
207
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
208
+ const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
209
+ return `${(n / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
210
+ }
211
+
212
+ const clearCacheOpts = saveOpts<{ spaceReclaimed: number }>({
213
+ setSaving: (v) => (clearing = v),
214
+ setError: (v) => (clearError = v),
215
+ setMsg: (v) => (clearMsg = v),
216
+ onSuccess: (data) => {
217
+ clearMsg = `Cleared — freed ${formatBytes(data?.spaceReclaimed ?? 0)}.`;
218
+ }
219
+ });
220
+
221
+ let rebuilding = $state(false);
222
+ let rebuildMsg = $state<string | null>(null);
223
+ let rebuildError = $state<string | null>(null);
224
+
225
+ const rebuildAllOpts = saveOpts<{ count: number }>({
226
+ setSaving: (v) => (rebuilding = v),
227
+ setError: (v) => (rebuildError = v),
228
+ setMsg: (v) => (rebuildMsg = v),
229
+ confirmMessage:
230
+ 'Rebuild every running container from scratch (no build cache)? Each will restart and may take a while.',
231
+ onSuccess: (data) => {
232
+ const n = data?.count ?? 0;
233
+ rebuildMsg = n === 0 ? 'No running containers to rebuild.' : `Rebuilding ${n} container(s)…`;
234
+ }
235
+ });
236
+
237
+ // Token values never reach the client; only whether each is set, for the placeholder.
238
+ // svelte-ignore state_referenced_locally
239
+ let manualTokens = $state(manualTokensEnabled);
240
+ let savingManualToggle = $state(false);
241
+ let manualToggleError = $state<string | null>(null);
242
+
243
+ const manualTokensToggleOpts = toggleOpts({
244
+ set: (v) => (manualTokens = v),
245
+ setSaving: (v) => (savingManualToggle = v),
246
+ setError: (v) => (manualToggleError = v)
247
+ });
248
+
249
+ // svelte-ignore state_referenced_locally
250
+ let ghSaved = $state(githubTokenSet);
251
+ let githubToken = $state('');
252
+ let savingGithub = $state(false);
253
+ let githubMsg = $state<string | null>(null);
254
+ let githubError = $state<string | null>(null);
255
+
256
+ const githubTokenOpts = saveOpts<{ set: boolean }>({
257
+ setSaving: (v) => (savingGithub = v),
258
+ setError: (v) => (githubError = v),
259
+ setMsg: (v) => (githubMsg = v),
260
+ onSuccess: (data) => {
261
+ ghSaved = data?.set ?? false;
262
+ githubToken = '';
263
+ githubMsg = ghSaved ? 'Saved.' : 'Cleared.';
264
+ }
265
+ });
266
+
267
+ // svelte-ignore state_referenced_locally
268
+ let claudeSaved = $state(claudeTokenSet);
269
+ let claudeToken = $state('');
270
+ let savingClaude = $state(false);
271
+ let claudeMsg = $state<string | null>(null);
272
+ let claudeError = $state<string | null>(null);
273
+
274
+ const claudeTokenOpts = saveOpts<{ set: boolean }>({
275
+ setSaving: (v) => (savingClaude = v),
276
+ setError: (v) => (claudeError = v),
277
+ setMsg: (v) => (claudeMsg = v),
278
+ onSuccess: (data) => {
279
+ claudeSaved = data?.set ?? false;
280
+ claudeToken = '';
281
+ claudeMsg = claudeSaved ? 'Saved.' : 'Cleared.';
282
+ }
283
+ });
284
+
285
+ // As with manual tokens, the token value never comes back from the server.
286
+ // svelte-ignore state_referenced_locally
287
+ let customEndpoint = $state(customEndpointEnabled);
288
+ let savingCustomToggle = $state(false);
289
+ let customToggleError = $state<string | null>(null);
290
+
291
+ const customEndpointToggleOpts = toggleOpts({
292
+ set: (v) => (customEndpoint = v),
293
+ setSaving: (v) => (savingCustomToggle = v),
294
+ setError: (v) => (customToggleError = v)
295
+ });
296
+
297
+ // svelte-ignore state_referenced_locally
298
+ let customBaseUrl = $state(customEndpointBaseUrl);
299
+ let savingCustomBaseUrl = $state(false);
300
+ let customBaseUrlMsg = $state<string | null>(null);
301
+ let customBaseUrlError = $state<string | null>(null);
302
+
303
+ const customBaseUrlOpts = saveOpts<{ set: boolean }>({
304
+ setSaving: (v) => (savingCustomBaseUrl = v),
305
+ setError: (v) => (customBaseUrlError = v),
306
+ setMsg: (v) => (customBaseUrlMsg = v),
307
+ onSuccess: (data) => {
308
+ customBaseUrlMsg = data?.set ? 'Saved.' : 'Cleared.';
309
+ }
310
+ });
311
+
312
+ let customToken = $state('');
313
+ // svelte-ignore state_referenced_locally
314
+ let customTokenSaved = $state(customEndpointTokenSet);
315
+ let savingCustomToken = $state(false);
316
+ let customTokenMsg = $state<string | null>(null);
317
+ let customTokenError = $state<string | null>(null);
318
+
319
+ const customTokenOpts = saveOpts<{ set: boolean }>({
320
+ setSaving: (v) => (savingCustomToken = v),
321
+ setError: (v) => (customTokenError = v),
322
+ setMsg: (v) => (customTokenMsg = v),
323
+ onSuccess: (data) => {
324
+ customTokenSaved = data?.set ?? false;
325
+ customToken = '';
326
+ customTokenMsg = customTokenSaved ? 'Saved.' : 'Cleared.';
327
+ }
328
+ });
329
+
330
+ // svelte-ignore state_referenced_locally
331
+ let customOpusModel = $state(customEndpointOpusModel);
332
+ // svelte-ignore state_referenced_locally
333
+ let customSonnetModel = $state(customEndpointSonnetModel);
334
+ // svelte-ignore state_referenced_locally
335
+ let customHaikuModel = $state(customEndpointHaikuModel);
336
+ // svelte-ignore state_referenced_locally
337
+ let customSmallFastModel = $state(customEndpointSmallFastModel);
338
+ // svelte-ignore state_referenced_locally
339
+ let customDefaultModel = $state(customEndpointModel);
340
+ let savingCustomModels = $state(false);
341
+ let customModelsMsg = $state<string | null>(null);
342
+ let customModelsError = $state<string | null>(null);
343
+
344
+ const customModelsOpts = saveOpts({
345
+ setSaving: (v) => (savingCustomModels = v),
346
+ setError: (v) => (customModelsError = v),
347
+ setMsg: (v) => (customModelsMsg = v),
348
+ onSuccess: () => {
349
+ customModelsMsg = 'Saved.';
350
+ }
351
+ });
352
+
353
+ // Only names round-trip; `hostEnvPresence` is refreshed from each save response so a
354
+ // newly-added name doesn't read as "missing" until the next full page load.
355
+ // svelte-ignore state_referenced_locally
356
+ let hostEnvVars = $state(hostEnvVarsEnabled);
357
+ let savingHostEnvToggle = $state(false);
358
+ let hostEnvToggleError = $state<string | null>(null);
359
+
360
+ const hostEnvVarsToggleOpts = toggleOpts<{ presence: Record<string, boolean> }>({
361
+ set: (v) => (hostEnvVars = v),
362
+ setSaving: (v) => (savingHostEnvToggle = v),
363
+ setError: (v) => (hostEnvToggleError = v),
364
+ onSuccess: (data) => {
365
+ hostEnvPresence = data?.presence ?? {};
366
+ }
367
+ });
368
+
369
+ // svelte-ignore state_referenced_locally
370
+ let hostEnvNames = $state([...hostEnvVarNames]);
371
+ // svelte-ignore state_referenced_locally
372
+ let hostEnvPresence = $state({ ...hostEnvVarPresence });
373
+ let newHostEnvName = $state('');
374
+ let savingHostEnvNames = $state(false);
375
+ let hostEnvNamesMsg = $state<string | null>(null);
376
+ let hostEnvNamesError = $state<string | null>(null);
377
+
378
+ /**
379
+ * Add form: submits every existing name (as hidden inputs) plus the free-text
380
+ * input, all under the repeated `names` field — the last entry is always the
381
+ * typed value. Validated and normalized (uppercased, deduped) here, in the
382
+ * submit callback, by mutating `formData` directly before the fetch fires —
383
+ * mirrors the old client-side pre-check without a second code path.
384
+ */
385
+ const addHostEnvOpts: MochiEnhanceOptions<{ presence: Record<string, boolean> }, ActionFailure> =
386
+ {
387
+ onPending: (v) => (savingHostEnvNames = v),
388
+ submit: ({ formData, cancel }) => {
389
+ hostEnvNamesError = null;
390
+ hostEnvNamesMsg = null;
391
+ const existing = formData.getAll('names').map(String);
392
+ const typed = existing.pop() ?? '';
393
+ const name = typed.trim().toUpperCase();
394
+ if (!name) {
395
+ cancel();
396
+ return;
397
+ }
398
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
399
+ hostEnvNamesError = `Invalid variable name: ${name}`;
400
+ cancel();
401
+ return;
402
+ }
403
+ newHostEnvName = '';
404
+ if (existing.includes(name)) {
405
+ cancel();
406
+ return;
407
+ }
408
+ const names = [...existing, name];
409
+ formData.delete('names');
410
+ for (const n of names) formData.append('names', n);
411
+ return ({ result }) => {
412
+ if (result.type === 'success') {
413
+ hostEnvNames = names;
414
+ hostEnvPresence = result.data?.presence ?? {};
415
+ hostEnvNamesMsg = 'Saved.';
416
+ } else if (result.type === 'failure') {
417
+ hostEnvNamesError = result.data?.error ?? 'Request failed';
418
+ } else if (result.type === 'error') {
419
+ hostEnvNamesError = 'Network error. Try again.';
420
+ }
421
+ };
422
+ }
423
+ };
424
+
425
+ /** Remove form (one per row): submits every name except this one. */
426
+ function removeHostEnvOpts(name: string) {
427
+ return saveOpts<{ presence: Record<string, boolean> }>({
428
+ setSaving: (v) => (savingHostEnvNames = v),
429
+ setError: (v) => (hostEnvNamesError = v),
430
+ setMsg: (v) => (hostEnvNamesMsg = v),
431
+ onSuccess: (data) => {
432
+ hostEnvNames = hostEnvNames.filter((n) => n !== name);
433
+ hostEnvPresence = data?.presence ?? {};
434
+ }
435
+ });
436
+ }
437
+
438
+ function toggleSound(on: boolean) {
439
+ sound = on;
440
+ setSoundEnabled(on);
441
+ // A toggle is a user gesture — unlock audio and preview when enabling.
442
+ unlockAudio();
443
+ if (on) playChime('done');
444
+ }
445
+
446
+ // The server exits mid-flight, so a dropped connection means success here too.
447
+ const shutdownOpts: MochiEnhanceOptions<Record<string, never>, ActionFailure> = {
448
+ submit: ({ cancel }) => {
449
+ if (
450
+ !confirm(
451
+ 'Delete the database, remove all instances and their containers, and shut down the server? This cannot be undone.'
452
+ )
453
+ ) {
454
+ cancel();
455
+ return;
456
+ }
457
+ shuttingDown = true;
458
+ }
459
+ };
460
+ </script>
461
+
462
+ <div class="page">
463
+ <AppBar>
464
+ <span class="title">Settings</span>
465
+ </AppBar>
466
+
467
+ <main class="content">
468
+ <section class="card">
469
+ <form
470
+ class="row image-row"
471
+ method="POST"
472
+ action="?/defaultImage"
473
+ bind:this={imageFormEl}
474
+ {@attach enhance(imageOpts)}
475
+ >
476
+ <div class="label">
477
+ <Container size={18} />
478
+ <div class="text">
479
+ <div class="name">
480
+ Default container image
481
+ {#if dockerArch}
482
+ <span class="arch" title="Docker daemon architecture">{dockerArch}</span>
483
+ {/if}
484
+ </div>
485
+ <div class="desc">
486
+ Used only when a project folder ships no devcontainer.json. Takes effect for instances
487
+ created from now on.
488
+ </div>
489
+ </div>
490
+ </div>
491
+ <div class="image-controls">
492
+ <input
493
+ type="text"
494
+ name="image"
495
+ class="image-input"
496
+ bind:value={image}
497
+ spellcheck="false"
498
+ autocapitalize="off"
499
+ autocorrect="off"
500
+ placeholder="mcr.microsoft.com/devcontainers/base:ubuntu"
501
+ />
502
+ <Button type="submit" disabled={savingImage}>Save</Button>
503
+ <Button
504
+ type="button"
505
+ icon={RotateCcw}
506
+ disabled={savingImage}
507
+ onclick={resetImage}
508
+ title="Reset to default ({builtinImage})"
509
+ aria-label="Reset to default image"
510
+ />
511
+ </div>
512
+ <div class="desc tip">
513
+ {#if dockerArch}
514
+ Your Docker daemon runs on <strong>{dockerArch}</strong> — pick an image that publishes
515
+ an <strong>{dockerArch}</strong> manifest, or the pull will fail.
516
+ {:else}
517
+ Pick an image whose manifest covers your Docker daemon's architecture, or the pull will
518
+ fail.
519
+ {/if}
520
+ </div>
521
+ {#if imageError}
522
+ <div class="msg error">{imageError}</div>
523
+ {:else if imageSaved}
524
+ <div class="msg ok">Saved.</div>
525
+ {/if}
526
+ </form>
527
+ </section>
528
+
529
+ <section class="card">
530
+ <form
531
+ class="row image-row"
532
+ method="POST"
533
+ action="?/copyIgnorePatterns"
534
+ bind:this={copyIgnoreFormEl}
535
+ {@attach enhance(copyIgnoreOpts)}
536
+ >
537
+ <div class="label">
538
+ <FolderMinus size={18} />
539
+ <div class="text">
540
+ <div class="name">Skip when copying a local folder</div>
541
+ <div class="desc">
542
+ Comma-separated folder/file names excluded when copying a local source folder into a
543
+ new instance's workspace. Leave blank to copy everything. Only applies to local
544
+ folders — cloning a Git repo URL is unaffected.
545
+ </div>
546
+ </div>
547
+ </div>
548
+ <div class="image-controls">
549
+ <input
550
+ type="text"
551
+ name="patterns"
552
+ class="image-input"
553
+ bind:value={copyIgnore}
554
+ spellcheck="false"
555
+ autocapitalize="off"
556
+ autocorrect="off"
557
+ placeholder="node_modules, .venv, dist"
558
+ />
559
+ <Button type="submit" disabled={savingCopyIgnore}>Save</Button>
560
+ <Button
561
+ type="button"
562
+ icon={RotateCcw}
563
+ disabled={savingCopyIgnore}
564
+ onclick={resetCopyIgnore}
565
+ title="Reset to default ({builtinCopyIgnore})"
566
+ aria-label="Reset to default copy-ignore patterns"
567
+ />
568
+ </div>
569
+ {#if copyIgnoreError}
570
+ <div class="msg error">{copyIgnoreError}</div>
571
+ {:else if copyIgnoreSaved}
572
+ <div class="msg ok">Saved.</div>
573
+ {/if}
574
+ </form>
575
+ </section>
576
+
577
+ <section class="card">
578
+ <form
579
+ class="row"
580
+ method="POST"
581
+ action="?/disableBuildCache"
582
+ {@attach enhance(buildCacheToggleOpts)}
583
+ >
584
+ <div class="label">
585
+ <Layers size={18} />
586
+ <div class="text">
587
+ <div class="name">Disable build cache</div>
588
+ <div class="desc">
589
+ Build every new container with <code>--build-no-cache</code>. Applies to first boot
590
+ and rebuilds — slower, but always picks up upstream image/layer changes.
591
+ </div>
592
+ </div>
593
+ </div>
594
+ <label class="switch">
595
+ <input
596
+ type="checkbox"
597
+ name="enabled"
598
+ checked={noCache}
599
+ disabled={savingCache}
600
+ onchange={(e) => {
601
+ noCache = e.currentTarget.checked;
602
+ e.currentTarget.form?.requestSubmit();
603
+ }}
604
+ />
605
+ <span class="track"><span class="thumb"></span></span>
606
+ </label>
607
+ </form>
608
+ {#if cacheError}
609
+ <div class="sub"><div class="msg error">{cacheError}</div></div>
610
+ {/if}
611
+
612
+ <form
613
+ class="row divided"
614
+ method="POST"
615
+ action="?/clearBuildCache"
616
+ {@attach enhance(clearCacheOpts)}
617
+ >
618
+ <div class="label">
619
+ <Trash2 size={18} />
620
+ <div class="text">
621
+ <div class="name">Clear build cache</div>
622
+ <div class="desc">
623
+ Purge Docker's BuildKit layer cache now, so the next build runs uncached. Doesn't
624
+ remove pulled images.
625
+ </div>
626
+ {#if clearError}
627
+ <div class="msg error">{clearError}</div>
628
+ {:else if clearMsg}
629
+ <div class="msg ok">{clearMsg}</div>
630
+ {/if}
631
+ </div>
632
+ </div>
633
+ <Button type="submit" icon={Trash2} disabled={clearing}>
634
+ {clearing ? 'Clearing…' : 'Clear cache'}
635
+ </Button>
636
+ </form>
637
+
638
+ <form
639
+ class="row divided"
640
+ method="POST"
641
+ action="?/rebuildAllNoCache"
642
+ {@attach enhance(rebuildAllOpts)}
643
+ >
644
+ <div class="label">
645
+ <Hammer size={18} />
646
+ <div class="text">
647
+ <div class="name">Rebuild running containers (no cache)</div>
648
+ <div class="desc">
649
+ Re-run <code>devcontainer up --build-no-cache</code> for every currently-running instance.
650
+ In-container edits are kept; stopped instances are left alone.
651
+ </div>
652
+ {#if rebuildError}
653
+ <div class="msg error">{rebuildError}</div>
654
+ {:else if rebuildMsg}
655
+ <div class="msg ok">{rebuildMsg}</div>
656
+ {/if}
657
+ </div>
658
+ </div>
659
+ <Button type="submit" icon={Hammer} disabled={rebuilding}>
660
+ {rebuilding ? 'Starting…' : 'Rebuild all'}
661
+ </Button>
662
+ </form>
663
+ </section>
664
+
665
+ <section class="card" class:disabled-card={customEndpoint}>
666
+ <form
667
+ class="row"
668
+ method="POST"
669
+ action="?/manualTokensToggle"
670
+ {@attach enhance(manualTokensToggleOpts)}
671
+ >
672
+ <div class="label">
673
+ <KeyRound size={18} />
674
+ <div class="text">
675
+ <div class="name">
676
+ Set tokens manually
677
+ {#if customEndpoint}
678
+ <span class="arch" title="Disabled while LiteLLM + Bedrock mode is on"
679
+ >disabled</span
680
+ >
681
+ {/if}
682
+ </div>
683
+ <div class="desc">
684
+ {#if customEndpoint}
685
+ Not available while LiteLLM + Bedrock mode is enabled — Claude credentials are
686
+ provided by the LiteLLM endpoint instead.
687
+ {:else}
688
+ Provide GitHub and Claude Code tokens yourself instead of discovering them from this
689
+ machine. Useful on a headless server or when signed in as a different identity. A
690
+ token set here is injected into every new container and overrides host credential
691
+ discovery.
692
+ {/if}
693
+ </div>
694
+ </div>
695
+ </div>
696
+ <label class="switch">
697
+ <input
698
+ type="checkbox"
699
+ name="enabled"
700
+ checked={manualTokens}
701
+ disabled={savingManualToggle || customEndpoint}
702
+ onchange={(e) => {
703
+ manualTokens = e.currentTarget.checked;
704
+ e.currentTarget.form?.requestSubmit();
705
+ }}
706
+ />
707
+ <span class="track"><span class="thumb"></span></span>
708
+ </label>
709
+ </form>
710
+ {#if manualToggleError}
711
+ <div class="sub"><div class="msg error">{manualToggleError}</div></div>
712
+ {/if}
713
+
714
+ {#if manualTokens && !customEndpoint}
715
+ <form
716
+ class="row divided token-row"
717
+ method="POST"
718
+ action="?/githubToken"
719
+ {@attach enhance(githubTokenOpts)}
720
+ >
721
+ <div class="label">
722
+ <div class="text">
723
+ <div class="name">GitHub token</div>
724
+ <div class="desc">
725
+ macOS / Linux: run <code>gh auth token</code> to print your GitHub CLI token, or
726
+ create a Personal Access Token at
727
+ <code>github.com/settings/tokens</code> (scopes: <code>repo</code>,
728
+ <code>read:org</code>). Leave blank and Save to clear.
729
+ </div>
730
+ </div>
731
+ </div>
732
+ <div class="image-controls">
733
+ <input
734
+ type="password"
735
+ name="githubToken"
736
+ class="image-input"
737
+ bind:value={githubToken}
738
+ spellcheck="false"
739
+ autocapitalize="off"
740
+ autocorrect="off"
741
+ autocomplete="off"
742
+ placeholder={ghSaved ? '•••••••• (saved)' : 'ghp_… / gho_…'}
743
+ />
744
+ <Button type="submit" disabled={savingGithub}>Save</Button>
745
+ </div>
746
+ {#if githubError}
747
+ <div class="msg error">{githubError}</div>
748
+ {:else if githubMsg}
749
+ <div class="msg ok">{githubMsg}</div>
750
+ {/if}
751
+ </form>
752
+
753
+ <form
754
+ class="row divided token-row"
755
+ method="POST"
756
+ action="?/claudeToken"
757
+ {@attach enhance(claudeTokenOpts)}
758
+ >
759
+ <div class="label">
760
+ <div class="text">
761
+ <div class="name">Claude Code token</div>
762
+ <div class="desc">
763
+ macOS / Linux: run <code>claude setup-token</code> to mint a long-lived token and
764
+ paste it here. Copying a live login out of the keychain or
765
+ <code>~/.claude/.credentials.json</code> works too, but that credential rotates —
766
+ the first <code>claude</code> to refresh it signs the other one out. Leave blank and Save
767
+ to clear.
768
+ </div>
769
+ </div>
770
+ </div>
771
+ <div class="image-controls">
772
+ <input
773
+ type="password"
774
+ name="claudeToken"
775
+ class="image-input"
776
+ bind:value={claudeToken}
777
+ spellcheck="false"
778
+ autocapitalize="off"
779
+ autocorrect="off"
780
+ autocomplete="off"
781
+ placeholder={claudeSaved ? '•••••••• (saved)' : 'sk-ant-oat…'}
782
+ />
783
+ <Button type="submit" disabled={savingClaude}>Save</Button>
784
+ </div>
785
+ {#if claudeError}
786
+ <div class="msg error">{claudeError}</div>
787
+ {:else if claudeMsg}
788
+ <div class="msg ok">{claudeMsg}</div>
789
+ {/if}
790
+ </form>
791
+ {/if}
792
+ </section>
793
+
794
+ <section class="card" class:disabled-card={manualTokens}>
795
+ <form
796
+ class="row"
797
+ method="POST"
798
+ action="?/customEndpointToggle"
799
+ {@attach enhance(customEndpointToggleOpts)}
800
+ >
801
+ <div class="label">
802
+ <KeyRound size={18} />
803
+ <div class="text">
804
+ <div class="name">
805
+ LiteLLM + Bedrock
806
+ {#if manualTokens}
807
+ <span class="arch" title="Disabled while Set tokens manually is on">disabled</span>
808
+ {/if}
809
+ </div>
810
+ <div class="desc">
811
+ {#if manualTokens}
812
+ Not available while "Set tokens manually" is enabled — these modes are mutually
813
+ incompatible.
814
+ {:else}
815
+ Route <code>claude</code> through a LiteLLM proxy fronting AWS Bedrock instead of Anthropic's
816
+ default API. When enabled, the Bedrock endpoint variables are injected into every new
817
+ container, host OAuth credentials are not used, and "Set tokens manually" is disabled.
818
+ {/if}
819
+ </div>
820
+ </div>
821
+ </div>
822
+ <label class="switch">
823
+ <input
824
+ type="checkbox"
825
+ name="enabled"
826
+ checked={customEndpoint}
827
+ disabled={savingCustomToggle || manualTokens}
828
+ onchange={(e) => {
829
+ customEndpoint = e.currentTarget.checked;
830
+ e.currentTarget.form?.requestSubmit();
831
+ }}
832
+ />
833
+ <span class="track"><span class="thumb"></span></span>
834
+ </label>
835
+ </form>
836
+ {#if customToggleError}
837
+ <div class="sub"><div class="msg error">{customToggleError}</div></div>
838
+ {/if}
839
+
840
+ {#if customEndpoint}
841
+ <form
842
+ class="row divided token-row"
843
+ method="POST"
844
+ action="?/customBaseUrl"
845
+ {@attach enhance(customBaseUrlOpts)}
846
+ >
847
+ <div class="label">
848
+ <div class="text">
849
+ <div class="name">Base URL</div>
850
+ <div class="desc">
851
+ The LiteLLM proxy endpoint, e.g.
852
+ <code>https://litellm.example.com/bedrock</code>. Passed as
853
+ <code>ANTHROPIC_BEDROCK_BASE_URL</code>.
854
+ </div>
855
+ </div>
856
+ </div>
857
+ <div class="image-controls">
858
+ <input
859
+ type="text"
860
+ name="baseUrl"
861
+ class="image-input"
862
+ bind:value={customBaseUrl}
863
+ spellcheck="false"
864
+ autocapitalize="off"
865
+ autocorrect="off"
866
+ autocomplete="off"
867
+ placeholder="https://litellm.example.com/bedrock"
868
+ />
869
+ <Button type="submit" disabled={savingCustomBaseUrl}>Save</Button>
870
+ </div>
871
+ {#if customBaseUrlError}
872
+ <div class="msg error">{customBaseUrlError}</div>
873
+ {:else if customBaseUrlMsg}
874
+ <div class="msg ok">{customBaseUrlMsg}</div>
875
+ {/if}
876
+ </form>
877
+
878
+ <form
879
+ class="row divided token-row"
880
+ method="POST"
881
+ action="?/customToken"
882
+ {@attach enhance(customTokenOpts)}
883
+ >
884
+ <div class="label">
885
+ <div class="text">
886
+ <div class="name">Auth token</div>
887
+ <div class="desc">
888
+ Your LiteLLM API key. Passed as <code>ANTHROPIC_AUTH_TOKEN</code>. Leave blank and
889
+ Save to clear.
890
+ </div>
891
+ </div>
892
+ </div>
893
+ <div class="image-controls">
894
+ <input
895
+ type="password"
896
+ name="token"
897
+ class="image-input"
898
+ bind:value={customToken}
899
+ spellcheck="false"
900
+ autocapitalize="off"
901
+ autocorrect="off"
902
+ autocomplete="off"
903
+ placeholder={customTokenSaved ? '•••••••• (saved)' : 'sk-…'}
904
+ />
905
+ <Button type="submit" disabled={savingCustomToken}>Save</Button>
906
+ </div>
907
+ {#if customTokenError}
908
+ <div class="msg error">{customTokenError}</div>
909
+ {:else if customTokenMsg}
910
+ <div class="msg ok">{customTokenMsg}</div>
911
+ {/if}
912
+ </form>
913
+
914
+ <form
915
+ class="row divided token-row"
916
+ method="POST"
917
+ action="?/customModels"
918
+ {@attach enhance(customModelsOpts)}
919
+ >
920
+ <div class="label">
921
+ <div class="text">
922
+ <div class="name">Model IDs</div>
923
+ <div class="desc">
924
+ Model aliases to use for each tier. Prefilled with defaults from the reference
925
+ launcher script. Leave a field as-is to keep its current value.
926
+ </div>
927
+ </div>
928
+ </div>
929
+ <div class="model-fields">
930
+ <label class="model-row">
931
+ <span class="model-label">Opus</span>
932
+ <input
933
+ type="text"
934
+ name="opusModel"
935
+ class="image-input"
936
+ bind:value={customOpusModel}
937
+ spellcheck="false"
938
+ autocapitalize="off"
939
+ autocorrect="off"
940
+ autocomplete="off"
941
+ />
942
+ </label>
943
+ <label class="model-row">
944
+ <span class="model-label">Sonnet</span>
945
+ <input
946
+ type="text"
947
+ name="sonnetModel"
948
+ class="image-input"
949
+ bind:value={customSonnetModel}
950
+ spellcheck="false"
951
+ autocapitalize="off"
952
+ autocorrect="off"
953
+ autocomplete="off"
954
+ />
955
+ </label>
956
+ <label class="model-row">
957
+ <span class="model-label">Haiku</span>
958
+ <input
959
+ type="text"
960
+ name="haikuModel"
961
+ class="image-input"
962
+ bind:value={customHaikuModel}
963
+ spellcheck="false"
964
+ autocapitalize="off"
965
+ autocorrect="off"
966
+ autocomplete="off"
967
+ />
968
+ </label>
969
+ <label class="model-row">
970
+ <span class="model-label">Small/fast</span>
971
+ <input
972
+ type="text"
973
+ name="smallFastModel"
974
+ class="image-input"
975
+ bind:value={customSmallFastModel}
976
+ spellcheck="false"
977
+ autocapitalize="off"
978
+ autocorrect="off"
979
+ autocomplete="off"
980
+ />
981
+ </label>
982
+ <label class="model-row">
983
+ <span class="model-label">Default</span>
984
+ <input
985
+ type="text"
986
+ name="defaultModel"
987
+ class="image-input"
988
+ bind:value={customDefaultModel}
989
+ spellcheck="false"
990
+ autocapitalize="off"
991
+ autocorrect="off"
992
+ autocomplete="off"
993
+ />
994
+ </label>
995
+ <div class="model-save-row">
996
+ <Button type="submit" disabled={savingCustomModels}>Save models</Button>
997
+ </div>
998
+ </div>
999
+ {#if customModelsError}
1000
+ <div class="msg error">{customModelsError}</div>
1001
+ {:else if customModelsMsg}
1002
+ <div class="msg ok">{customModelsMsg}</div>
1003
+ {/if}
1004
+ </form>
1005
+ {/if}
1006
+ </section>
1007
+
1008
+ <section class="card">
1009
+ <form
1010
+ class="row"
1011
+ method="POST"
1012
+ action="?/hostEnvVarsToggle"
1013
+ {@attach enhance(hostEnvVarsToggleOpts)}
1014
+ >
1015
+ <div class="label">
1016
+ <Variable size={18} />
1017
+ <div class="text">
1018
+ <div class="name">Host environment variables</div>
1019
+ <div class="desc">
1020
+ Forward selected environment variables from this machine into every new container's
1021
+ interactive shells. Values are read from this process's own environment at container
1022
+ start and are never shown here or stored — only the variable names are saved.
1023
+ </div>
1024
+ </div>
1025
+ </div>
1026
+ <label class="switch">
1027
+ <input
1028
+ type="checkbox"
1029
+ name="enabled"
1030
+ checked={hostEnvVars}
1031
+ disabled={savingHostEnvToggle}
1032
+ onchange={(e) => {
1033
+ hostEnvVars = e.currentTarget.checked;
1034
+ e.currentTarget.form?.requestSubmit();
1035
+ }}
1036
+ />
1037
+ <span class="track"><span class="thumb"></span></span>
1038
+ </label>
1039
+ </form>
1040
+ {#if hostEnvToggleError}
1041
+ <div class="sub"><div class="msg error">{hostEnvToggleError}</div></div>
1042
+ {/if}
1043
+
1044
+ {#if hostEnvVars}
1045
+ <div class="row divided var-row">
1046
+ <div class="var-list">
1047
+ {#each hostEnvNames as name (name)}
1048
+ <form
1049
+ class="var-item"
1050
+ method="POST"
1051
+ action="?/hostEnvVarNames"
1052
+ {@attach enhance(removeHostEnvOpts(name))}
1053
+ >
1054
+ {#each hostEnvNames.filter((n) => n !== name) as other (other)}
1055
+ <input type="hidden" name="names" value={other} />
1056
+ {/each}
1057
+ <span class="var-name">{name}</span>
1058
+ <span
1059
+ class="var-status"
1060
+ class:present={hostEnvPresence[name]}
1061
+ title={hostEnvPresence[name]
1062
+ ? 'Set on this host — will be injected'
1063
+ : 'Not set on this host — will be skipped'}
1064
+ >
1065
+ {hostEnvPresence[name] ? 'set' : 'missing'}
1066
+ </span>
1067
+ <button
1068
+ type="submit"
1069
+ class="var-remove"
1070
+ disabled={savingHostEnvNames}
1071
+ aria-label={`Remove ${name}`}
1072
+ >
1073
+ <X size={13} />
1074
+ </button>
1075
+ </form>
1076
+ {:else}
1077
+ <div class="var-empty">No variables added yet.</div>
1078
+ {/each}
1079
+ </div>
1080
+ <form
1081
+ class="var-add"
1082
+ method="POST"
1083
+ action="?/hostEnvVarNames"
1084
+ {@attach enhance(addHostEnvOpts)}
1085
+ >
1086
+ {#each hostEnvNames as name (name)}
1087
+ <input type="hidden" name="names" value={name} />
1088
+ {/each}
1089
+ <input
1090
+ type="text"
1091
+ name="names"
1092
+ class="image-input"
1093
+ bind:value={newHostEnvName}
1094
+ spellcheck="false"
1095
+ autocapitalize="off"
1096
+ autocorrect="off"
1097
+ autocomplete="off"
1098
+ placeholder="AWS_ACCESS_KEY_ID"
1099
+ />
1100
+ <Button type="submit" icon={Plus} disabled={savingHostEnvNames}>Add</Button>
1101
+ </form>
1102
+ {#if hostEnvNamesError}
1103
+ <div class="msg error">{hostEnvNamesError}</div>
1104
+ {:else if hostEnvNamesMsg}
1105
+ <div class="msg ok">{hostEnvNamesMsg}</div>
1106
+ {/if}
1107
+ </div>
1108
+ {/if}
1109
+ </section>
1110
+
1111
+ <section class="card">
1112
+ <div class="row">
1113
+ <div class="label">
1114
+ <SunMoon size={18} />
1115
+ <div class="text">
1116
+ <div class="name">Theme</div>
1117
+ <div class="desc">
1118
+ Light, dark, or follow the browser preference automatically (auto).
1119
+ </div>
1120
+ </div>
1121
+ </div>
1122
+ <ThemePicker />
1123
+ </div>
1124
+ </section>
1125
+
1126
+ <section class="card">
1127
+ <div class="row">
1128
+ <div class="label">
1129
+ <Volume2 size={18} />
1130
+ <div class="text">
1131
+ <div class="name">Attention sound</div>
1132
+ <div class="desc">
1133
+ Play a chime when an instance finishes a task or needs your input.
1134
+ </div>
1135
+ </div>
1136
+ </div>
1137
+ <label class="switch">
1138
+ <input
1139
+ type="checkbox"
1140
+ checked={sound}
1141
+ onchange={(e) => toggleSound(e.currentTarget.checked)}
1142
+ />
1143
+ <span class="track"><span class="thumb"></span></span>
1144
+ </label>
1145
+ </div>
1146
+ </section>
1147
+
1148
+ <section class="card danger-card">
1149
+ <form class="row" method="POST" action="?/shutdown" {@attach enhance(shutdownOpts)}>
1150
+ <div class="label">
1151
+ <Power size={18} />
1152
+ <div class="text">
1153
+ <div class="name">Delete database, containers, and shut down</div>
1154
+ <div class="desc">
1155
+ Stop and remove every instance and its container, delete all copied workspaces and the
1156
+ database, then shut down the server. This cannot be undone.
1157
+ </div>
1158
+ </div>
1159
+ </div>
1160
+ {#if shuttingDown}
1161
+ <span class="shutting">Server is shutting down — you can close this tab.</span>
1162
+ {:else}
1163
+ <Button type="submit" variant="danger">Delete &amp; shut down</Button>
1164
+ {/if}
1165
+ </form>
1166
+ </section>
1167
+
1168
+ <CoinButton />
1169
+
1170
+ <div class="version">Codebay v{version}</div>
1171
+ </main>
1172
+ </div>
1173
+
1174
+ <!-- Settings renders outside AppShell, which hosts the app-wide Toaster. -->
1175
+ <Toaster
1176
+ toastOptions={{
1177
+ style:
1178
+ 'border:1px solid var(--ink); background:var(--bg-card); color:var(--ink); box-shadow:4px 4px 0 var(--ink); font-family:var(--font-mono); font-size:13px;'
1179
+ }}
1180
+ />
1181
+
1182
+ <style>
1183
+ .page {
1184
+ display: flex;
1185
+ flex-direction: column;
1186
+ min-height: 100vh;
1187
+ }
1188
+ .title {
1189
+ display: inline-flex;
1190
+ align-items: center;
1191
+ padding: 0 14px;
1192
+ font-family: var(--font-mono);
1193
+ font-size: 12px;
1194
+ text-transform: uppercase;
1195
+ letter-spacing: 0.08em;
1196
+ color: var(--ink-soft);
1197
+ }
1198
+ .content {
1199
+ flex: 1;
1200
+ display: flex;
1201
+ flex-direction: column;
1202
+ align-items: center;
1203
+ gap: 16px;
1204
+ padding: 32px 20px;
1205
+ }
1206
+ .card {
1207
+ width: 100%;
1208
+ max-width: 560px;
1209
+ background: var(--bg-card);
1210
+ border: 1px solid var(--rule);
1211
+ height: max-content;
1212
+ }
1213
+ .danger-card {
1214
+ border-color: var(--danger);
1215
+ }
1216
+ .disabled-card {
1217
+ opacity: 0.55;
1218
+ pointer-events: none;
1219
+ }
1220
+ /* Keep the action button on one line; in the flex row it would otherwise shrink and wrap. */
1221
+ .danger-card :global(.btn) {
1222
+ flex: none;
1223
+ white-space: nowrap;
1224
+ }
1225
+ .version {
1226
+ font-family: var(--font-mono);
1227
+ font-size: 11px;
1228
+ letter-spacing: 0.08em;
1229
+ color: var(--ink-soft);
1230
+ }
1231
+ .shutting {
1232
+ flex: none;
1233
+ max-width: 200px;
1234
+ font-family: var(--font-mono);
1235
+ font-size: 12px;
1236
+ color: var(--danger);
1237
+ line-height: 1.4;
1238
+ text-align: right;
1239
+ }
1240
+ .row {
1241
+ display: flex;
1242
+ align-items: center;
1243
+ justify-content: space-between;
1244
+ gap: 16px;
1245
+ padding: 18px 18px;
1246
+ }
1247
+ .row.divided {
1248
+ border-top: 1px solid var(--rule);
1249
+ align-items: flex-start;
1250
+ }
1251
+ .row.divided :global(.btn) {
1252
+ flex: none;
1253
+ white-space: nowrap;
1254
+ }
1255
+ .sub {
1256
+ padding: 0 18px 14px;
1257
+ }
1258
+ code {
1259
+ font-family: var(--font-mono);
1260
+ font-size: 0.92em;
1261
+ padding: 1px 4px;
1262
+ background: var(--bg);
1263
+ border: 1px solid var(--rule);
1264
+ border-radius: 3px;
1265
+ }
1266
+ .label {
1267
+ display: flex;
1268
+ align-items: flex-start;
1269
+ gap: 12px;
1270
+ color: var(--ink);
1271
+ min-width: 0;
1272
+ }
1273
+ .text {
1274
+ min-width: 0;
1275
+ }
1276
+ .name {
1277
+ font-family: var(--font-mono);
1278
+ font-size: 13px;
1279
+ text-transform: uppercase;
1280
+ letter-spacing: 0.06em;
1281
+ }
1282
+ .arch {
1283
+ margin-left: 8px;
1284
+ padding: 2px 6px;
1285
+ font-size: 11px;
1286
+ letter-spacing: 0.04em;
1287
+ color: var(--ink-soft);
1288
+ border: 1px solid var(--rule);
1289
+ border-radius: 3px;
1290
+ vertical-align: middle;
1291
+ }
1292
+ .tip {
1293
+ width: 100%;
1294
+ margin-top: 4px;
1295
+ padding: 8px 10px;
1296
+ color: var(--ink-soft);
1297
+ background: color-mix(in srgb, var(--info) 10%, transparent);
1298
+ border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
1299
+ border-radius: 4px;
1300
+ }
1301
+ .desc {
1302
+ margin-top: 4px;
1303
+ font-family: var(--font-mono);
1304
+ font-size: 12px;
1305
+ color: var(--ink-faint);
1306
+ line-height: 1.4;
1307
+ }
1308
+ .image-row {
1309
+ flex-wrap: wrap;
1310
+ }
1311
+ /* Manual-token rows: same input+Save layout, allowed to wrap so the help text and
1312
+ the "Saved." message drop below the field on narrow widths. */
1313
+ .token-row {
1314
+ flex-wrap: wrap;
1315
+ }
1316
+ .image-controls {
1317
+ display: flex;
1318
+ align-items: center;
1319
+ gap: 8px;
1320
+ flex: 1;
1321
+ min-width: 220px;
1322
+ justify-content: flex-end;
1323
+ }
1324
+ .image-input {
1325
+ flex: 1;
1326
+ min-width: 0;
1327
+ padding: 8px 10px;
1328
+ font-family: var(--font-mono);
1329
+ font-size: 12px;
1330
+ color: var(--ink);
1331
+ background: var(--bg);
1332
+ border: 1px solid var(--rule);
1333
+ }
1334
+ .image-input:focus-visible {
1335
+ outline: 2px solid var(--ink);
1336
+ outline-offset: 1px;
1337
+ }
1338
+ .image-controls :global(.btn) {
1339
+ flex: none;
1340
+ }
1341
+ .msg {
1342
+ width: 100%;
1343
+ margin-top: 2px;
1344
+ font-family: var(--font-mono);
1345
+ font-size: 12px;
1346
+ line-height: 1.4;
1347
+ }
1348
+ .msg.error {
1349
+ color: var(--danger);
1350
+ }
1351
+ .msg.ok {
1352
+ color: var(--ink-soft);
1353
+ }
1354
+ .switch {
1355
+ position: relative;
1356
+ flex: none;
1357
+ cursor: pointer;
1358
+ }
1359
+ .switch input {
1360
+ position: absolute;
1361
+ opacity: 0;
1362
+ inset: 0;
1363
+ margin: 0;
1364
+ cursor: pointer;
1365
+ }
1366
+ .track {
1367
+ display: block;
1368
+ width: 44px;
1369
+ height: 24px;
1370
+ border: 1px solid var(--rule);
1371
+ background: var(--bg);
1372
+ border-radius: 999px;
1373
+ transition: background 0.15s ease;
1374
+ }
1375
+ .thumb {
1376
+ display: block;
1377
+ width: 18px;
1378
+ height: 18px;
1379
+ margin: 2px;
1380
+ background: var(--ink-faint);
1381
+ border-radius: 999px;
1382
+ transition:
1383
+ transform 0.15s ease,
1384
+ background 0.15s ease;
1385
+ }
1386
+ .switch input:checked + .track {
1387
+ background: var(--switch-on-bg);
1388
+ }
1389
+ .switch input:checked + .track .thumb {
1390
+ transform: translateX(20px);
1391
+ background: var(--ink);
1392
+ }
1393
+ .switch input:focus-visible + .track {
1394
+ outline: 2px solid var(--ink);
1395
+ outline-offset: 2px;
1396
+ }
1397
+ .model-fields {
1398
+ display: flex;
1399
+ flex-direction: column;
1400
+ gap: 6px;
1401
+ flex: 1;
1402
+ min-width: 220px;
1403
+ }
1404
+ .model-row {
1405
+ display: flex;
1406
+ align-items: center;
1407
+ gap: 8px;
1408
+ }
1409
+ .model-label {
1410
+ flex: none;
1411
+ width: 72px;
1412
+ font-family: var(--font-mono);
1413
+ font-size: 11px;
1414
+ color: var(--ink-soft);
1415
+ text-align: right;
1416
+ }
1417
+ .model-row .image-input {
1418
+ flex: 1;
1419
+ }
1420
+ .model-save-row {
1421
+ display: flex;
1422
+ justify-content: flex-end;
1423
+ margin-top: 4px;
1424
+ }
1425
+ .var-row {
1426
+ flex-direction: column;
1427
+ align-items: stretch;
1428
+ gap: 10px;
1429
+ }
1430
+ .var-list {
1431
+ display: flex;
1432
+ flex-direction: column;
1433
+ gap: 6px;
1434
+ }
1435
+ .var-item {
1436
+ display: flex;
1437
+ align-items: center;
1438
+ gap: 8px;
1439
+ padding: 6px 8px;
1440
+ background: var(--bg);
1441
+ border: 1px solid var(--rule);
1442
+ }
1443
+ .var-name {
1444
+ flex: 1;
1445
+ min-width: 0;
1446
+ font-family: var(--font-mono);
1447
+ font-size: 12px;
1448
+ color: var(--ink);
1449
+ word-break: break-all;
1450
+ }
1451
+ .var-status {
1452
+ flex: none;
1453
+ font-family: var(--font-mono);
1454
+ font-size: 11px;
1455
+ letter-spacing: 0.04em;
1456
+ color: var(--danger);
1457
+ }
1458
+ .var-status.present {
1459
+ color: var(--ink-soft);
1460
+ }
1461
+ .var-remove {
1462
+ flex: none;
1463
+ display: inline-flex;
1464
+ align-items: center;
1465
+ justify-content: center;
1466
+ width: 22px;
1467
+ height: 22px;
1468
+ padding: 0;
1469
+ color: var(--ink-soft);
1470
+ background: transparent;
1471
+ border: 1px solid var(--rule);
1472
+ cursor: pointer;
1473
+ }
1474
+ .var-remove:hover:not(:disabled) {
1475
+ color: var(--danger);
1476
+ border-color: var(--danger);
1477
+ }
1478
+ .var-remove:disabled {
1479
+ opacity: 0.4;
1480
+ cursor: not-allowed;
1481
+ }
1482
+ .var-empty {
1483
+ font-family: var(--font-mono);
1484
+ font-size: 12px;
1485
+ color: var(--ink-faint);
1486
+ }
1487
+ .var-add {
1488
+ display: flex;
1489
+ gap: 8px;
1490
+ }
1491
+ .var-add :global(.btn) {
1492
+ flex: none;
1493
+ }
1494
+ </style>