codebay 0.9.1 → 0.10.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.
Files changed (77) hide show
  1. package/.mochi/manifest.json +213 -146
  2. package/.mochi/svelte-client/HydratableIsland-aebn1fa1.js +1 -0
  3. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-rx6arrv5.js +2 -0
  4. package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-92hepzby.js → _hydrate-AvatarsView_257s9yq1wwr4f-jpjdmgpe.js} +1 -1
  5. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-c9y47dat.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-d61feq89.js +38 -0
  7. package/.mochi/svelte-client/{chunk-2j95sv83.js → chunk-0fjx3z3r.js} +1 -1
  8. package/.mochi/svelte-client/chunk-8xvr02wh.js +2 -0
  9. package/.mochi/svelte-client/{chunk-m497q88s.js → chunk-9143wy3j.js} +2 -2
  10. package/.mochi/svelte-client/{chunk-v65n8th6.js → chunk-bn731rda.js} +1 -1
  11. package/.mochi/svelte-client/{chunk-a2psjmrm.js → chunk-j72r8pkk.js} +2 -2
  12. package/.mochi/svelte-client/chunk-k37wj1jm.js +2 -0
  13. package/.mochi/svelte-client/{chunk-mpv3qx28.js → chunk-p20tkpde.js} +1 -1
  14. package/.mochi/svelte-client/chunk-pc0fm37c.js +2 -0
  15. package/.mochi/svelte-client/{chunk-6rbs0mhx.js → chunk-t0ap7q89.js} +1 -1
  16. package/.mochi/svelte-client/{chunk-rs41by6w.js → chunk-wvtkwfsh.js} +1 -1
  17. package/.mochi/svelte-compile/{App-7vjcp38v.server.js → App-59cdj86f.server.js} +167 -103
  18. package/.mochi/svelte-compile/{Avatars-e2cfc95t.server.js → Avatars-fmagfyj2.server.js} +1 -1
  19. package/.mochi/svelte-compile/{Instance-h78n2dsv.server.js → Instance-s1gqwzmx.server.js} +16 -10
  20. package/.mochi/svelte-compile/{Settings-tav2asn7.server.js → Settings-k447mt0x.server.js} +540 -88
  21. package/.mochi/svelte-compile/{chunk-brjv9pyd.js → chunk-3t7v4hk0.js} +1 -7
  22. package/.mochi/svelte-compile/chunk-gkjgybb2.js +15 -0
  23. package/.mochi/svelte-compile/{chunk-m8ygpyhr.js → chunk-th2dbqvc.js} +20 -32
  24. package/.mochi/svelte-css/Button-oharop2xz6am.css +1 -0
  25. package/.mochi/svelte-css/IdeBar-3bgr3m5z0lpeg.css +1 -0
  26. package/.mochi/svelte-css/{InstanceView-3vzow3ttxzlh3.css → InstanceView-1pd7j16bd2b16.css} +1 -1
  27. package/.mochi/svelte-css/SettingsView-n3oy75uonzp2.css +1 -0
  28. package/.mochi/svelte-css/{TopBar-2q87li6yt8ids.css → TopBar-2y76zxwehrfos.css} +1 -1
  29. package/migrations/0007_instance_avatar.sql +1 -0
  30. package/package.json +2 -2
  31. package/src/avatars/diamond.ts +5 -6
  32. package/src/avatars/index.ts +1 -16
  33. package/src/avatars/pick.server.ts +25 -0
  34. package/src/avatars/whale.ts +7 -8
  35. package/src/components/AppShell.svelte +29 -4
  36. package/src/components/Avatar.svelte +8 -8
  37. package/src/components/Button.svelte +22 -1
  38. package/src/components/DashboardView.svelte +14 -2
  39. package/src/components/FolderBrowser.svelte +6 -2
  40. package/src/components/IdeBar.svelte +56 -13
  41. package/src/components/IdeLoader.svelte +15 -3
  42. package/src/components/InstanceCard.svelte +6 -1
  43. package/src/components/InstanceView.svelte +33 -15
  44. package/src/components/SettingsView.svelte +548 -1
  45. package/src/components/TopBar.svelte +37 -10
  46. package/src/components/UiShowcase.svelte +5 -10
  47. package/src/container-injections/claude-aliases.ts +1 -1
  48. package/src/container-injections/claude-code-custom.ts +2 -1
  49. package/src/container-injections/claude-code-ide-extension.ts +5 -0
  50. package/src/container-injections/claude-code-models.ts +2 -1
  51. package/src/container-injections/claude-code-update.ts +62 -1
  52. package/src/container-injections/claude-effort-level.ts +32 -0
  53. package/src/container-injections/claude-permission-mode.ts +40 -0
  54. package/src/container-injections/custom-env-vars.ts +82 -0
  55. package/src/container-injections/host-env-vars.ts +2 -1
  56. package/src/lib/config.server.ts +3 -0
  57. package/src/lib/db.server.ts +8 -3
  58. package/src/lib/devcontainer.server.ts +129 -42
  59. package/src/lib/health.server.ts +55 -11
  60. package/src/lib/injections.server.ts +45 -38
  61. package/src/lib/instances.server.ts +181 -19
  62. package/src/lib/log-capture.server.ts +262 -0
  63. package/src/lib/ports.server.ts +31 -0
  64. package/src/lib/proxy.server.ts +2 -2
  65. package/src/pages/Settings.svelte +22 -1
  66. package/src/routes.ts +98 -2
  67. package/src/types.ts +36 -0
  68. package/.mochi/svelte-client/HydratableIsland-k6q8esj4.js +0 -1
  69. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-sg96rp17.js +0 -2
  70. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-p3f649gk.js +0 -1
  71. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-dzmgmv78.js +0 -29
  72. package/.mochi/svelte-client/chunk-7wkvn2vx.js +0 -2
  73. package/.mochi/svelte-client/chunk-m4fzsd5b.js +0 -2
  74. package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +0 -1
  75. package/.mochi/svelte-css/IdeBar-10xodudcx57qr.css +0 -1
  76. package/.mochi/svelte-css/SettingsView-2qfauz68axro1.css +0 -1
  77. package/src/container-injections/claude-skip-permissions.ts +0 -28
@@ -15,8 +15,13 @@
15
15
  import UserCog from '@lucide/svelte/icons/user-cog';
16
16
  import Cpu from '@lucide/svelte/icons/cpu';
17
17
  import Variable from '@lucide/svelte/icons/variable';
18
+ import KeySquare from '@lucide/svelte/icons/key-square';
18
19
  import Plus from '@lucide/svelte/icons/plus';
19
20
  import X from '@lucide/svelte/icons/x';
21
+ import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
22
+ import ListOrdered from '@lucide/svelte/icons/list-ordered';
23
+ import Boxes from '@lucide/svelte/icons/boxes';
24
+ import Puzzle from '@lucide/svelte/icons/puzzle';
20
25
  import { Toaster } from 'svelte-french-toast';
21
26
  import { flushSync } from 'svelte';
22
27
  import { enhance } from 'mochi-framework';
@@ -27,9 +32,17 @@
27
32
  import CoinButton from './CoinButton.svelte';
28
33
  import Avatar from './Avatar.svelte';
29
34
  import { avatars, findAvatar, type AvatarArt } from '../avatars/index.ts';
30
- import type { InstanceMode } from '../types.ts';
35
+ import {
36
+ CLAUDE_PERMISSION_MODES,
37
+ CLAUDE_EFFORT_LEVELS,
38
+ type ClaudePermissionMode,
39
+ type ClaudeEffortLevel,
40
+ type InstanceMode
41
+ } from '../types.ts';
31
42
  import Terminal from '@lucide/svelte/icons/terminal';
32
43
  import LayoutTemplate from '@lucide/svelte/icons/layout-template';
44
+ import ShieldCheck from '@lucide/svelte/icons/shield-check';
45
+ import Gauge from '@lucide/svelte/icons/gauge';
33
46
  import { installPopupBackTrap } from '../lib/popup-nav.ts';
34
47
 
35
48
  /** Every settings form action fails with the same `{ error }` shape. */
@@ -38,6 +51,8 @@
38
51
  let {
39
52
  pet,
40
53
  defaultMode,
54
+ claudePermissionMode,
55
+ claudeEffortLevel,
41
56
  defaultImage,
42
57
  builtinImage,
43
58
  disableBuildCache,
@@ -67,10 +82,17 @@
67
82
  hostEnvVarsEnabled,
68
83
  hostEnvVarNames,
69
84
  hostEnvVarPresence,
85
+ customEnvVarsEnabled,
86
+ customEnvVarNames,
87
+ advancedSerialInjections,
88
+ advancedNoBuildkit,
89
+ advancedBlockingExtInstall,
70
90
  version
71
91
  }: {
72
92
  pet?: AvatarArt;
73
93
  defaultMode: InstanceMode;
94
+ claudePermissionMode: ClaudePermissionMode;
95
+ claudeEffortLevel: ClaudeEffortLevel;
74
96
  defaultImage: string;
75
97
  builtinImage: string;
76
98
  disableBuildCache: boolean;
@@ -100,6 +122,11 @@
100
122
  hostEnvVarsEnabled: boolean;
101
123
  hostEnvVarNames: string[];
102
124
  hostEnvVarPresence: Record<string, boolean>;
125
+ customEnvVarsEnabled: boolean;
126
+ customEnvVarNames: string[];
127
+ advancedSerialInjections: boolean;
128
+ advancedNoBuildkit: boolean;
129
+ advancedBlockingExtInstall: boolean;
103
130
  version: string;
104
131
  } = $props();
105
132
 
@@ -200,6 +227,63 @@
200
227
  modeFormEl?.requestSubmit();
201
228
  }
202
229
 
230
+ // svelte-ignore state_referenced_locally
231
+ let permissionChoice = $state<ClaudePermissionMode>(claudePermissionMode);
232
+ let savingPermission = $state(false);
233
+ let permissionError = $state<string | null>(null);
234
+ let permissionFormEl: HTMLFormElement | undefined;
235
+
236
+ const permissionModeOpts = saveOpts<{ mode: ClaudePermissionMode }>({
237
+ setSaving: (v) => (savingPermission = v),
238
+ setError: (v) => (permissionError = v),
239
+ setMsg: () => {},
240
+ onSuccess: (data) => {
241
+ if (data?.mode) permissionChoice = data.mode;
242
+ }
243
+ });
244
+
245
+ const PERMISSION_LABELS: Record<ClaudePermissionMode, string> = {
246
+ default: 'Default',
247
+ manual: 'Manual',
248
+ auto: 'Auto',
249
+ plan: 'Plan'
250
+ };
251
+
252
+ function choosePermissionMode(next: ClaudePermissionMode) {
253
+ if (next === permissionChoice) return;
254
+ flushSync(() => (permissionChoice = next));
255
+ permissionFormEl?.requestSubmit();
256
+ }
257
+
258
+ // svelte-ignore state_referenced_locally
259
+ let effortChoice = $state<ClaudeEffortLevel>(claudeEffortLevel);
260
+ let savingEffort = $state(false);
261
+ let effortError = $state<string | null>(null);
262
+ let effortFormEl: HTMLFormElement | undefined;
263
+
264
+ const effortLevelOpts = saveOpts<{ level: ClaudeEffortLevel }>({
265
+ setSaving: (v) => (savingEffort = v),
266
+ setError: (v) => (effortError = v),
267
+ setMsg: () => {},
268
+ onSuccess: (data) => {
269
+ if (data?.level) effortChoice = data.level;
270
+ }
271
+ });
272
+
273
+ const EFFORT_LABELS: Record<ClaudeEffortLevel, string> = {
274
+ low: 'Low',
275
+ medium: 'Medium',
276
+ high: 'High',
277
+ xhigh: 'X-High',
278
+ max: 'Max'
279
+ };
280
+
281
+ function chooseEffort(next: ClaudeEffortLevel) {
282
+ if (next === effortChoice) return;
283
+ flushSync(() => (effortChoice = next));
284
+ effortFormEl?.requestSubmit();
285
+ }
286
+
203
287
  // svelte-ignore state_referenced_locally
204
288
  let image = $state(defaultImage);
205
289
  let savingImage = $state(false);
@@ -327,6 +411,49 @@
327
411
  }
328
412
  });
329
413
 
414
+ // DB-backed advanced escape hatches, so they initialize from the props.
415
+ // svelte-ignore state_referenced_locally
416
+ let serialInjections = $state(advancedSerialInjections);
417
+ let savingSerial = $state(false);
418
+ let serialError = $state<string | null>(null);
419
+ const serialOpts = toggleOpts({
420
+ set: (v) => (serialInjections = v),
421
+ setSaving: (v) => (savingSerial = v),
422
+ setError: (v) => (serialError = v)
423
+ });
424
+
425
+ // svelte-ignore state_referenced_locally
426
+ let noBuildkit = $state(advancedNoBuildkit);
427
+ let savingBuildkit = $state(false);
428
+ let buildkitError = $state<string | null>(null);
429
+ const buildkitOpts = toggleOpts({
430
+ set: (v) => (noBuildkit = v),
431
+ setSaving: (v) => (savingBuildkit = v),
432
+ setError: (v) => (buildkitError = v)
433
+ });
434
+
435
+ // svelte-ignore state_referenced_locally
436
+ let blockingExtInstall = $state(advancedBlockingExtInstall);
437
+ let savingBlocking = $state(false);
438
+ let blockingError = $state<string | null>(null);
439
+ const blockingOpts = toggleOpts({
440
+ set: (v) => (blockingExtInstall = v),
441
+ setSaving: (v) => (savingBlocking = v),
442
+ setError: (v) => (blockingError = v)
443
+ });
444
+
445
+ let clearingVer = $state(false);
446
+ let verMsg = $state<string | null>(null);
447
+ let verError = $state<string | null>(null);
448
+ const clearVersionOpts = saveOpts({
449
+ setSaving: (v) => (clearingVer = v),
450
+ setError: (v) => (verError = v),
451
+ setMsg: (v) => (verMsg = v),
452
+ onSuccess: () => {
453
+ verMsg = 'Cleared — the next instance boot re-checks the registry.';
454
+ }
455
+ });
456
+
330
457
  // Token values never reach the client; only whether each is set, for the placeholder.
331
458
  // svelte-ignore state_referenced_locally
332
459
  let manualTokens = $state(manualTokensEnabled);
@@ -575,6 +702,75 @@
575
702
  });
576
703
  }
577
704
 
705
+ // svelte-ignore state_referenced_locally
706
+ let customEnvEnabled = $state(customEnvVarsEnabled);
707
+ let savingCustomEnvToggle = $state(false);
708
+ let customEnvToggleError = $state<string | null>(null);
709
+
710
+ const customEnvVarsToggleOpts = toggleOpts({
711
+ set: (v) => (customEnvEnabled = v),
712
+ setSaving: (v) => (savingCustomEnvToggle = v),
713
+ setError: (v) => (customEnvToggleError = v)
714
+ });
715
+
716
+ // Only names round-trip; the secret values live server-side and are shown here as a placeholder.
717
+ // svelte-ignore state_referenced_locally
718
+ let customEnvNames = $state([...customEnvVarNames]);
719
+ let newCustomEnvName = $state('');
720
+ let newCustomEnvValue = $state('');
721
+ let savingCustomEnv = $state(false);
722
+ let customEnvMsg = $state<string | null>(null);
723
+ let customEnvError = $state<string | null>(null);
724
+
725
+ /**
726
+ * Add/update one var: name + secret value are submitted alone (the client never holds the other
727
+ * secrets to resubmit), so re-adding an existing name upserts its value server-side.
728
+ */
729
+ const addCustomEnvOpts: MochiEnhanceOptions<{ names: string[] }, ActionFailure> = {
730
+ onPending: (v) => (savingCustomEnv = v),
731
+ submit: ({ formData, cancel }) => {
732
+ customEnvError = null;
733
+ customEnvMsg = null;
734
+ const name = String(formData.get('name') ?? '').trim();
735
+ const value = String(formData.get('value') ?? '');
736
+ if (!name || !value) {
737
+ customEnvError = !name ? 'Name is required.' : 'Value is required.';
738
+ cancel();
739
+ return;
740
+ }
741
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
742
+ customEnvError = `Invalid variable name: ${name}`;
743
+ cancel();
744
+ return;
745
+ }
746
+ formData.set('name', name);
747
+ return ({ result }) => {
748
+ if (result.type === 'success') {
749
+ customEnvNames = result.data?.names ?? customEnvNames;
750
+ newCustomEnvName = '';
751
+ newCustomEnvValue = '';
752
+ customEnvMsg = 'Saved.';
753
+ } else if (result.type === 'failure') {
754
+ customEnvError = result.data?.error ?? 'Request failed';
755
+ } else if (result.type === 'error') {
756
+ customEnvError = 'Network error. Try again.';
757
+ }
758
+ };
759
+ }
760
+ };
761
+
762
+ /** Remove form (one per row): posts just this var's name. */
763
+ function removeCustomEnvOpts(name: string) {
764
+ return saveOpts<{ names: string[] }>({
765
+ setSaving: (v) => (savingCustomEnv = v),
766
+ setError: (v) => (customEnvError = v),
767
+ setMsg: (v) => (customEnvMsg = v),
768
+ onSuccess: (data) => {
769
+ customEnvNames = data?.names ?? customEnvNames.filter((n) => n !== name);
770
+ }
771
+ });
772
+ }
773
+
578
774
  function toggleSound(on: boolean) {
579
775
  sound = on;
580
776
  setSoundEnabled(on);
@@ -683,6 +879,87 @@
683
879
  {/if}
684
880
  </section>
685
881
 
882
+ <section class="card">
883
+ <form
884
+ class="row"
885
+ method="POST"
886
+ action="?/claudePermissionMode"
887
+ bind:this={permissionFormEl}
888
+ {@attach enhance(permissionModeOpts)}
889
+ >
890
+ <div class="label">
891
+ <ShieldCheck size={20} />
892
+ <div class="text">
893
+ <div class="name">Claude permission mode</div>
894
+ <div class="desc">
895
+ The mode Claude Code starts in inside new instances. <strong>Default</strong> runs
896
+ <code>--dangerously-skip-permissions</code>, which never prompts; the other three pass
897
+ <code>--permission-mode</code> instead, because the skip flag overrides them. Applies on
898
+ create and rebuild, not to already-running instances.
899
+ </div>
900
+ </div>
901
+ </div>
902
+ <input type="hidden" name="mode" value={permissionChoice} />
903
+ <div class="mode-toggle" role="group" aria-label="Claude permission mode">
904
+ {#each CLAUDE_PERMISSION_MODES as option (option)}
905
+ <button
906
+ type="button"
907
+ class="mode-btn"
908
+ class:active={permissionChoice === option}
909
+ aria-pressed={permissionChoice === option}
910
+ disabled={savingPermission}
911
+ onclick={() => choosePermissionMode(option)}
912
+ >
913
+ {PERMISSION_LABELS[option]}
914
+ </button>
915
+ {/each}
916
+ </div>
917
+ </form>
918
+ {#if permissionError}
919
+ <div class="sub"><div class="msg error">{permissionError}</div></div>
920
+ {/if}
921
+ </section>
922
+
923
+ <section class="card">
924
+ <form
925
+ class="row"
926
+ method="POST"
927
+ action="?/claudeEffortLevel"
928
+ bind:this={effortFormEl}
929
+ {@attach enhance(effortLevelOpts)}
930
+ >
931
+ <div class="label">
932
+ <Gauge size={20} />
933
+ <div class="text">
934
+ <div class="name">Claude effort level</div>
935
+ <div class="desc">
936
+ The default reasoning effort Claude Code starts new sessions at inside instances,
937
+ written to <code>~/.claude/settings.json</code>. Applies on create and rebuild, not to
938
+ already-running instances.
939
+ </div>
940
+ </div>
941
+ </div>
942
+ <input type="hidden" name="level" value={effortChoice} />
943
+ <div class="mode-toggle" role="group" aria-label="Claude effort level">
944
+ {#each CLAUDE_EFFORT_LEVELS as option (option)}
945
+ <button
946
+ type="button"
947
+ class="mode-btn"
948
+ class:active={effortChoice === option}
949
+ aria-pressed={effortChoice === option}
950
+ disabled={savingEffort}
951
+ onclick={() => chooseEffort(option)}
952
+ >
953
+ {EFFORT_LABELS[option]}
954
+ </button>
955
+ {/each}
956
+ </div>
957
+ </form>
958
+ {#if effortError}
959
+ <div class="sub"><div class="msg error">{effortError}</div></div>
960
+ {/if}
961
+ </section>
962
+
686
963
  <section class="card">
687
964
  <form
688
965
  class="row image-row"
@@ -1561,6 +1838,110 @@
1561
1838
  {/if}
1562
1839
  </section>
1563
1840
 
1841
+ <section class="card">
1842
+ <form
1843
+ class="row"
1844
+ method="POST"
1845
+ action="?/customEnvVarsToggle"
1846
+ {@attach enhance(customEnvVarsToggleOpts)}
1847
+ >
1848
+ <div class="label">
1849
+ <KeySquare size={20} />
1850
+ <div class="text">
1851
+ <div class="name">Custom environment variables</div>
1852
+ <div class="desc">
1853
+ Inject your own <code>NAME=value</code> pairs into every new container as
1854
+ <code>containerEnv</code>, available to all processes. Values are secret — stored
1855
+ locally, masked in the build log, and never shown here after saving. Re-add a name to
1856
+ update its value.
1857
+ </div>
1858
+ </div>
1859
+ </div>
1860
+ <label class="switch">
1861
+ <input
1862
+ type="checkbox"
1863
+ name="enabled"
1864
+ checked={customEnvEnabled}
1865
+ disabled={savingCustomEnvToggle}
1866
+ onchange={(e) => {
1867
+ customEnvEnabled = e.currentTarget.checked;
1868
+ e.currentTarget.form?.requestSubmit();
1869
+ }}
1870
+ />
1871
+ <span class="track"><span class="thumb"></span></span>
1872
+ </label>
1873
+ </form>
1874
+ {#if customEnvToggleError}
1875
+ <div class="sub"><div class="msg error">{customEnvToggleError}</div></div>
1876
+ {/if}
1877
+
1878
+ {#if customEnvEnabled}
1879
+ <div class="row divided var-row">
1880
+ <div class="var-list">
1881
+ {#each customEnvNames as name (name)}
1882
+ <form
1883
+ class="var-item"
1884
+ method="POST"
1885
+ action="?/removeCustomEnvVar"
1886
+ {@attach enhance(removeCustomEnvOpts(name))}
1887
+ >
1888
+ <input type="hidden" name="name" value={name} />
1889
+ <span class="var-name">{name}</span>
1890
+ <span class="var-status present" title="Set — injected into every new container">
1891
+ •••• saved
1892
+ </span>
1893
+ <button
1894
+ type="submit"
1895
+ class="var-remove"
1896
+ disabled={savingCustomEnv}
1897
+ aria-label={`Remove ${name}`}
1898
+ >
1899
+ <X size={13} />
1900
+ </button>
1901
+ </form>
1902
+ {:else}
1903
+ <div class="var-empty">No variables added yet.</div>
1904
+ {/each}
1905
+ </div>
1906
+ <form
1907
+ class="var-add"
1908
+ method="POST"
1909
+ action="?/setCustomEnvVar"
1910
+ {@attach enhance(addCustomEnvOpts)}
1911
+ >
1912
+ <input
1913
+ type="text"
1914
+ name="name"
1915
+ class="image-input"
1916
+ bind:value={newCustomEnvName}
1917
+ spellcheck="false"
1918
+ autocapitalize="off"
1919
+ autocorrect="off"
1920
+ autocomplete="off"
1921
+ placeholder="MY_SECRET"
1922
+ />
1923
+ <input
1924
+ type="password"
1925
+ name="value"
1926
+ class="image-input"
1927
+ bind:value={newCustomEnvValue}
1928
+ spellcheck="false"
1929
+ autocapitalize="off"
1930
+ autocorrect="off"
1931
+ autocomplete="off"
1932
+ placeholder="value"
1933
+ />
1934
+ <Button type="submit" icon={Plus} disabled={savingCustomEnv}>Add</Button>
1935
+ </form>
1936
+ {#if customEnvError}
1937
+ <div class="msg error">{customEnvError}</div>
1938
+ {:else if customEnvMsg}
1939
+ <div class="msg ok">{customEnvMsg}</div>
1940
+ {/if}
1941
+ </div>
1942
+ {/if}
1943
+ </section>
1944
+
1564
1945
  <section class="card">
1565
1946
  <div class="row">
1566
1947
  <div class="label">
@@ -1643,6 +2024,144 @@
1643
2024
  {/if}
1644
2025
  </section>
1645
2026
 
2027
+ <section class="card">
2028
+ <details class="advanced">
2029
+ <summary>
2030
+ <SlidersHorizontal size={20} />
2031
+ <span class="name">Advanced</span>
2032
+ <span class="hint">escape hatches — defaults are right for almost everyone</span>
2033
+ </summary>
2034
+
2035
+ <form
2036
+ class="row divided"
2037
+ method="POST"
2038
+ action="?/serialInjectionsToggle"
2039
+ {@attach enhance(serialOpts)}
2040
+ >
2041
+ <div class="label">
2042
+ <ListOrdered size={20} />
2043
+ <div class="text">
2044
+ <div class="name">Serial boot injections</div>
2045
+ <div class="desc">
2046
+ Run boot injections one at a time instead of in parallel stages. Slower boot; useful
2047
+ when diagnosing injection interference.
2048
+ </div>
2049
+ </div>
2050
+ </div>
2051
+ <label class="switch">
2052
+ <input
2053
+ type="checkbox"
2054
+ name="enabled"
2055
+ checked={serialInjections}
2056
+ disabled={savingSerial}
2057
+ onchange={(e) => {
2058
+ serialInjections = e.currentTarget.checked;
2059
+ e.currentTarget.form?.requestSubmit();
2060
+ }}
2061
+ />
2062
+ <span class="track"><span class="thumb"></span></span>
2063
+ </label>
2064
+ </form>
2065
+ {#if serialError}
2066
+ <div class="sub"><div class="msg error">{serialError}</div></div>
2067
+ {/if}
2068
+
2069
+ <form
2070
+ class="row divided"
2071
+ method="POST"
2072
+ action="?/noBuildkitToggle"
2073
+ {@attach enhance(buildkitOpts)}
2074
+ >
2075
+ <div class="label">
2076
+ <Boxes size={20} />
2077
+ <div class="text">
2078
+ <div class="name">Don't force BuildKit</div>
2079
+ <div class="desc">
2080
+ Skip setting <code>DOCKER_BUILDKIT=1</code> for container builds and let the daemon's
2081
+ default builder decide. For daemons where forcing BuildKit breaks builds.
2082
+ </div>
2083
+ </div>
2084
+ </div>
2085
+ <label class="switch">
2086
+ <input
2087
+ type="checkbox"
2088
+ name="enabled"
2089
+ checked={noBuildkit}
2090
+ disabled={savingBuildkit}
2091
+ onchange={(e) => {
2092
+ noBuildkit = e.currentTarget.checked;
2093
+ e.currentTarget.form?.requestSubmit();
2094
+ }}
2095
+ />
2096
+ <span class="track"><span class="thumb"></span></span>
2097
+ </label>
2098
+ </form>
2099
+ {#if buildkitError}
2100
+ <div class="sub"><div class="msg error">{buildkitError}</div></div>
2101
+ {/if}
2102
+
2103
+ <form
2104
+ class="row divided"
2105
+ method="POST"
2106
+ action="?/blockingExtInstallToggle"
2107
+ {@attach enhance(blockingOpts)}
2108
+ >
2109
+ <div class="label">
2110
+ <Puzzle size={20} />
2111
+ <div class="text">
2112
+ <div class="name">Blocking IDE extension install</div>
2113
+ <div class="desc">
2114
+ Install the Claude Code IDE extension before code-server starts, so the first window
2115
+ always has it active. Slower first boot.
2116
+ </div>
2117
+ </div>
2118
+ </div>
2119
+ <label class="switch">
2120
+ <input
2121
+ type="checkbox"
2122
+ name="enabled"
2123
+ checked={blockingExtInstall}
2124
+ disabled={savingBlocking}
2125
+ onchange={(e) => {
2126
+ blockingExtInstall = e.currentTarget.checked;
2127
+ e.currentTarget.form?.requestSubmit();
2128
+ }}
2129
+ />
2130
+ <span class="track"><span class="thumb"></span></span>
2131
+ </label>
2132
+ </form>
2133
+ {#if blockingError}
2134
+ <div class="sub"><div class="msg error">{blockingError}</div></div>
2135
+ {/if}
2136
+
2137
+ <form
2138
+ class="row divided"
2139
+ method="POST"
2140
+ action="?/clearVersionCache"
2141
+ {@attach enhance(clearVersionOpts)}
2142
+ >
2143
+ <div class="label">
2144
+ <Trash2 size={20} />
2145
+ <div class="text">
2146
+ <div class="name">Clear claude-code version cache</div>
2147
+ <div class="desc">
2148
+ Forget the cached latest-version check; the next instance boot asks the npm registry
2149
+ again.
2150
+ </div>
2151
+ {#if verError}
2152
+ <div class="msg error">{verError}</div>
2153
+ {:else if verMsg}
2154
+ <div class="msg ok">{verMsg}</div>
2155
+ {/if}
2156
+ </div>
2157
+ </div>
2158
+ <Button type="submit" icon={Trash2} disabled={clearingVer}>
2159
+ {clearingVer ? 'Clearing…' : 'Clear cache'}
2160
+ </Button>
2161
+ </form>
2162
+ </details>
2163
+ </section>
2164
+
1646
2165
  <section class="card danger-card">
1647
2166
  <form class="row" method="POST" action="?/shutdown" {@attach enhance(shutdownOpts)}>
1648
2167
  <div class="label">
@@ -1711,6 +2230,34 @@
1711
2230
  .danger-card {
1712
2231
  border-color: var(--danger);
1713
2232
  }
2233
+ details.advanced > summary {
2234
+ display: flex;
2235
+ align-items: center;
2236
+ gap: 10px;
2237
+ padding: 14px 16px;
2238
+ cursor: pointer;
2239
+ list-style: none;
2240
+ font-family: var(--font-mono);
2241
+ }
2242
+ details.advanced > summary::-webkit-details-marker {
2243
+ display: none;
2244
+ }
2245
+ details.advanced > summary .name {
2246
+ font-size: 13px;
2247
+ }
2248
+ details.advanced > summary .hint {
2249
+ color: var(--ink-soft);
2250
+ font-size: 12px;
2251
+ }
2252
+ /* The chevron affordance a hidden native marker would otherwise provide. */
2253
+ details.advanced > summary::after {
2254
+ content: '▸';
2255
+ margin-left: auto;
2256
+ color: var(--ink-soft);
2257
+ }
2258
+ details.advanced[open] > summary::after {
2259
+ content: '▾';
2260
+ }
1714
2261
  .disabled-card {
1715
2262
  opacity: 0.55;
1716
2263
  pointer-events: none;