codebay 0.2.1 → 0.4.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 (52) hide show
  1. package/.mochi/manifest.json +274 -246
  2. package/.mochi/svelte-client/HydratableIsland-79z9xq4b.js +1 -0
  3. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-f5rzyjm8.js +2 -0
  4. package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-3cnbjpz2.js → _hydrate-AvatarsView_257s9yq1wwr4f-fxyd5qx4.js} +1 -1
  5. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-nv31ptdp.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-r9vvc7rx.js +28 -0
  7. package/.mochi/svelte-client/chunk-6eexkr6p.js +2 -0
  8. package/.mochi/svelte-client/chunk-7znwaf7t.js +2 -0
  9. package/.mochi/svelte-client/{chunk-hcztrd75.js → chunk-dz1019cy.js} +1 -1
  10. package/.mochi/svelte-client/{chunk-qxwt25hn.js → chunk-nmf4dq86.js} +1 -1
  11. package/.mochi/svelte-client/{chunk-tr67wtwn.js → chunk-ssa0ad2f.js} +1 -1
  12. package/.mochi/svelte-client/{chunk-5jnepvzx.js → chunk-zmmb1c4b.js} +1 -1
  13. package/.mochi/svelte-compile/{App-f0q5n4rk.server.js → App-9qjy14bc.server.js} +38 -23
  14. package/.mochi/svelte-compile/{Avatars-p8gfef7x.server.js → Avatars-sq8mjn6k.server.js} +2 -2
  15. package/.mochi/svelte-compile/{Instance-atjwsczx.server.js → Instance-13j59daw.server.js} +2 -2
  16. package/.mochi/svelte-compile/{Settings-q88xw6h8.server.js → Settings-p0s2awg1.server.js} +404 -103
  17. package/.mochi/svelte-compile/{chunk-9bjk7yx6.js → chunk-6ghzacpg.js} +1 -1
  18. package/.mochi/svelte-compile/{chunk-4kwy539a.js → chunk-bacb4b0r.js} +1 -1
  19. package/.mochi/svelte-compile/{chunk-sxqys3nw.js → chunk-dfe2km6a.js} +5 -2
  20. package/.mochi/svelte-compile/{chunk-v6796ga4.js → chunk-fpe16ywq.js} +28 -6
  21. package/.mochi/svelte-css/Brand-1bh2b800g5vrn.css +1 -0
  22. package/.mochi/svelte-css/SettingsView-3uycxa8z548m4.css +1 -0
  23. package/README.md +4 -0
  24. package/bin/codebay.ts +42 -1
  25. package/package.json +8 -8
  26. package/src/avatars/index.ts +5 -0
  27. package/src/components/AppBar.svelte +8 -1
  28. package/src/components/AppShell.svelte +17 -3
  29. package/src/components/Brand.svelte +15 -4
  30. package/src/components/DashboardView.svelte +5 -2
  31. package/src/components/IdeBar.svelte +9 -1
  32. package/src/components/InstanceCard.svelte +7 -3
  33. package/src/components/InstanceView.svelte +4 -1
  34. package/src/components/SettingsCog.svelte +9 -1
  35. package/src/components/SettingsView.svelte +476 -18
  36. package/src/components/TopBar.svelte +4 -1
  37. package/src/container-injections/claude-code-models.ts +74 -0
  38. package/src/container-injections/git-identity.ts +30 -3
  39. package/src/lib/injections.server.ts +3 -0
  40. package/src/lib/instances.server.ts +10 -1
  41. package/src/lib/popup-nav.ts +37 -0
  42. package/src/pages/App.svelte +10 -3
  43. package/src/pages/Settings.svelte +31 -0
  44. package/src/routes.ts +88 -2
  45. package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +0 -1
  46. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-7xgbmxxg.js +0 -2
  47. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-2ntmdm3e.js +0 -1
  48. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-t08spym1.js +0 -23
  49. package/.mochi/svelte-client/chunk-04qv4w6w.js +0 -2
  50. package/.mochi/svelte-client/chunk-sjmcgh7f.js +0 -2
  51. package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +0 -1
  52. package/.mochi/svelte-css/SettingsView-1nfdwnm16cpp2.css +0 -1
@@ -4,6 +4,7 @@
4
4
  import Power from '@lucide/svelte/icons/power';
5
5
  import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
6
6
  import Volume2 from '@lucide/svelte/icons/volume-2';
7
+ import PawPrint from '@lucide/svelte/icons/paw-print';
7
8
  import SunMoon from '@lucide/svelte/icons/sun-moon';
8
9
  import ThemePicker from './ThemePicker.svelte';
9
10
  import Layers from '@lucide/svelte/icons/layers';
@@ -11,6 +12,8 @@
11
12
  import Trash2 from '@lucide/svelte/icons/trash-2';
12
13
  import Hammer from '@lucide/svelte/icons/hammer';
13
14
  import KeyRound from '@lucide/svelte/icons/key-round';
15
+ import UserCog from '@lucide/svelte/icons/user-cog';
16
+ import Cpu from '@lucide/svelte/icons/cpu';
14
17
  import Variable from '@lucide/svelte/icons/variable';
15
18
  import Plus from '@lucide/svelte/icons/plus';
16
19
  import X from '@lucide/svelte/icons/x';
@@ -22,16 +25,23 @@
22
25
  import { playChime, unlockAudio } from '../sound.ts';
23
26
  import Button from './Button.svelte';
24
27
  import CoinButton from './CoinButton.svelte';
28
+ import Avatar from './Avatar.svelte';
29
+ import { avatars, findAvatar, type AvatarArt } from '../avatars/index.ts';
30
+ import { installPopupBackTrap } from '../lib/popup-nav.ts';
25
31
 
26
32
  /** Every settings form action fails with the same `{ error }` shape. */
27
33
  type ActionFailure = { error: string };
28
34
 
29
35
  let {
36
+ pet,
30
37
  defaultImage,
31
38
  builtinImage,
32
39
  disableBuildCache,
33
40
  copyIgnorePatterns,
34
41
  builtinCopyIgnore,
42
+ gitIdentityEnabled,
43
+ gitIdentityName,
44
+ gitIdentityEmail,
35
45
  dockerArch,
36
46
  manualTokensEnabled,
37
47
  githubTokenSet,
@@ -44,16 +54,26 @@
44
54
  customEndpointHaikuModel,
45
55
  customEndpointSmallFastModel,
46
56
  customEndpointModel,
57
+ manualModelOverrideEnabled,
58
+ manualOpusModel,
59
+ manualSonnetModel,
60
+ manualHaikuModel,
61
+ manualSmallFastModel,
62
+ manualModel,
47
63
  hostEnvVarsEnabled,
48
64
  hostEnvVarNames,
49
65
  hostEnvVarPresence,
50
66
  version
51
67
  }: {
68
+ pet?: AvatarArt;
52
69
  defaultImage: string;
53
70
  builtinImage: string;
54
71
  disableBuildCache: boolean;
55
72
  copyIgnorePatterns: string;
56
73
  builtinCopyIgnore: string;
74
+ gitIdentityEnabled: boolean;
75
+ gitIdentityName: string;
76
+ gitIdentityEmail: string;
57
77
  dockerArch: string | null;
58
78
  manualTokensEnabled: boolean;
59
79
  githubTokenSet: boolean;
@@ -66,6 +86,12 @@
66
86
  customEndpointHaikuModel: string;
67
87
  customEndpointSmallFastModel: string;
68
88
  customEndpointModel: string;
89
+ manualModelOverrideEnabled: boolean;
90
+ manualOpusModel: string;
91
+ manualSonnetModel: string;
92
+ manualHaikuModel: string;
93
+ manualSmallFastModel: string;
94
+ manualModel: string;
69
95
  hostEnvVarsEnabled: boolean;
70
96
  hostEnvVarNames: string[];
71
97
  hostEnvVarPresence: Record<string, boolean>;
@@ -75,8 +101,16 @@
75
101
  // Defaults to on during SSR, where localStorage doesn't exist.
76
102
  let sound = $state(soundEnabled());
77
103
 
104
+ // DB-backed, so it initializes from the prop. Undefined is the off state — the header keeps its box logo.
105
+ // svelte-ignore state_referenced_locally
106
+ let petArt = $state(pet);
107
+ let savingPet = $state(false);
108
+ let petError = $state<string | null>(null);
109
+
78
110
  let shuttingDown = $state(false);
79
111
 
112
+ $effect(() => installPopupBackTrap());
113
+
80
114
  /** Reused by every plain save form below; only the per-control state setters differ. */
81
115
  function saveOpts<Success extends Record<string, unknown> = Record<string, unknown>>(handlers: {
82
116
  setSaving: (v: boolean) => void;
@@ -186,6 +220,37 @@
186
220
  copyIgnoreFormEl?.requestSubmit();
187
221
  }
188
222
 
223
+ // svelte-ignore state_referenced_locally
224
+ let gitIdentity = $state(gitIdentityEnabled);
225
+ let savingGitToggle = $state(false);
226
+ let gitToggleError = $state<string | null>(null);
227
+
228
+ const gitIdentityToggleOpts = toggleOpts({
229
+ set: (v) => (gitIdentity = v),
230
+ setSaving: (v) => (savingGitToggle = v),
231
+ setError: (v) => (gitToggleError = v)
232
+ });
233
+
234
+ // Both fields must be saved together — a lone name or email doesn't count as an override.
235
+ // svelte-ignore state_referenced_locally
236
+ let gitName = $state(gitIdentityName);
237
+ // svelte-ignore state_referenced_locally
238
+ let gitEmail = $state(gitIdentityEmail);
239
+ let savingGitIdentity = $state(false);
240
+ let gitIdentityError = $state<string | null>(null);
241
+ let gitIdentitySaved = $state(false);
242
+
243
+ const gitIdentityOpts = saveOpts<{ name: string; email: string }>({
244
+ setSaving: (v) => (savingGitIdentity = v),
245
+ setError: (v) => (gitIdentityError = v),
246
+ setMsg: (v) => (gitIdentitySaved = !!v),
247
+ onSuccess: (data) => {
248
+ gitName = data?.name ?? gitName;
249
+ gitEmail = data?.email ?? gitEmail;
250
+ gitIdentitySaved = true;
251
+ }
252
+ });
253
+
189
254
  // DB-backed rather than localStorage, so it initializes from the prop.
190
255
  // svelte-ignore state_referenced_locally
191
256
  let noCache = $state(disableBuildCache);
@@ -291,7 +356,11 @@
291
356
  const customEndpointToggleOpts = toggleOpts({
292
357
  set: (v) => (customEndpoint = v),
293
358
  setSaving: (v) => (savingCustomToggle = v),
294
- setError: (v) => (customToggleError = v)
359
+ setError: (v) => (customToggleError = v),
360
+ // Enabling LiteLLM disables the manual override server-side; keep the UI in sync.
361
+ onSuccess: (data) => {
362
+ if ((data as { enabled?: boolean } | undefined)?.enabled) manualModelOverride = false;
363
+ }
295
364
  });
296
365
 
297
366
  // svelte-ignore state_referenced_locally
@@ -350,6 +419,49 @@
350
419
  }
351
420
  });
352
421
 
422
+ // svelte-ignore state_referenced_locally
423
+ let manualModelOverride = $state(manualModelOverrideEnabled);
424
+ let savingManualModelToggle = $state(false);
425
+ let manualModelToggleError = $state<string | null>(null);
426
+
427
+ const manualModelToggleOpts = toggleOpts({
428
+ set: (v) => (manualModelOverride = v),
429
+ setSaving: (v) => (savingManualModelToggle = v),
430
+ setError: (v) => (manualModelToggleError = v),
431
+ // Enabling this disables LiteLLM server-side; mirror that here so the UI stays consistent.
432
+ onSuccess: (data) => {
433
+ if ((data as { enabled?: boolean } | undefined)?.enabled) customEndpoint = false;
434
+ }
435
+ });
436
+
437
+ // svelte-ignore state_referenced_locally
438
+ let manualOpus = $state(manualOpusModel);
439
+ // svelte-ignore state_referenced_locally
440
+ let manualSonnet = $state(manualSonnetModel);
441
+ // svelte-ignore state_referenced_locally
442
+ let manualHaiku = $state(manualHaikuModel);
443
+ // svelte-ignore state_referenced_locally
444
+ let manualSmallFast = $state(manualSmallFastModel);
445
+ // svelte-ignore state_referenced_locally
446
+ let manualDefault = $state(manualModel);
447
+ let savingManualModels = $state(false);
448
+ let manualModelsMsg = $state<string | null>(null);
449
+ let manualModelsError = $state<string | null>(null);
450
+
451
+ const manualModelsOpts = saveOpts({
452
+ setSaving: (v) => (savingManualModels = v),
453
+ setError: (v) => (manualModelsError = v),
454
+ setMsg: (v) => (manualModelsMsg = v),
455
+ onSuccess: () => {
456
+ manualModelsMsg = 'Saved.';
457
+ }
458
+ });
459
+
460
+ // LiteLLM is incompatible with both manual tokens and the manual model override.
461
+ let litellmBlocker = $derived(
462
+ manualTokens ? 'Set tokens manually' : manualModelOverride ? 'Override models manually' : null
463
+ );
464
+
353
465
  // Only names round-trip; `hostEnvPresence` is refreshed from each save response so a
354
466
  // newly-added name doesn't read as "missing" until the next full page load.
355
467
  // svelte-ignore state_referenced_locally
@@ -443,6 +555,34 @@
443
555
  if (on) playChime('done');
444
556
  }
445
557
 
558
+ // State is object-shaped (the chosen sprite), so this can't reuse the boolean `toggleOpts`.
559
+ const petToggleOpts: MochiEnhanceOptions<{ enabled: boolean; name?: string }, ActionFailure> = {
560
+ onPending: (v) => (savingPet = v),
561
+ submit: () => {
562
+ petError = null;
563
+ return ({ result }) => {
564
+ if (result.type === 'success') {
565
+ petArt = result.data?.name ? findAvatar(result.data.name) : undefined;
566
+ return;
567
+ }
568
+ petError =
569
+ result.type === 'failure'
570
+ ? (result.data?.error ?? 'Request failed')
571
+ : 'Network error. Try again.';
572
+ };
573
+ }
574
+ };
575
+
576
+ function petChooseOpts(art: AvatarArt) {
577
+ return saveOpts<{ name: string }>({
578
+ setSaving: (v) => (savingPet = v),
579
+ setError: (v) => (petError = v),
580
+ onSuccess: (data) => {
581
+ petArt = (data?.name ? findAvatar(data.name) : undefined) ?? art;
582
+ }
583
+ });
584
+ }
585
+
446
586
  // The server exits mid-flight, so a dropped connection means success here too.
447
587
  const shutdownOpts: MochiEnhanceOptions<Record<string, never>, ActionFailure> = {
448
588
  submit: ({ cancel }) => {
@@ -474,7 +614,7 @@
474
614
  {@attach enhance(imageOpts)}
475
615
  >
476
616
  <div class="label">
477
- <Container size={18} />
617
+ <Container size={20} />
478
618
  <div class="text">
479
619
  <div class="name">
480
620
  Default container image
@@ -535,7 +675,7 @@
535
675
  {@attach enhance(copyIgnoreOpts)}
536
676
  >
537
677
  <div class="label">
538
- <FolderMinus size={18} />
678
+ <FolderMinus size={20} />
539
679
  <div class="text">
540
680
  <div class="name">Skip when copying a local folder</div>
541
681
  <div class="desc">
@@ -574,6 +714,98 @@
574
714
  </form>
575
715
  </section>
576
716
 
717
+ <section class="card">
718
+ <form
719
+ class="row"
720
+ method="POST"
721
+ action="?/gitIdentityToggle"
722
+ {@attach enhance(gitIdentityToggleOpts)}
723
+ >
724
+ <div class="label">
725
+ <UserCog size={20} />
726
+ <div class="text">
727
+ <div class="name">Override git identity</div>
728
+ <div class="desc">
729
+ Inject a name and email as <code>git config --global user.name/user.email</code> in every
730
+ new container, taking precedence over the host's own git config. When off, each container
731
+ falls back to the host's identity.
732
+ </div>
733
+ </div>
734
+ </div>
735
+ <label class="switch">
736
+ <input
737
+ type="checkbox"
738
+ name="enabled"
739
+ checked={gitIdentity}
740
+ disabled={savingGitToggle}
741
+ onchange={(e) => {
742
+ gitIdentity = e.currentTarget.checked;
743
+ e.currentTarget.form?.requestSubmit();
744
+ }}
745
+ />
746
+ <span class="track"><span class="thumb"></span></span>
747
+ </label>
748
+ </form>
749
+ {#if gitToggleError}
750
+ <div class="sub"><div class="msg error">{gitToggleError}</div></div>
751
+ {/if}
752
+
753
+ {#if gitIdentity}
754
+ <form
755
+ class="row divided token-row"
756
+ method="POST"
757
+ action="?/gitIdentityOverride"
758
+ {@attach enhance(gitIdentityOpts)}
759
+ >
760
+ <div class="label">
761
+ <div class="text">
762
+ <div class="name">Identity</div>
763
+ <div class="desc">
764
+ Both fields are required — leave either blank and the container falls back to the
765
+ host's identity.
766
+ </div>
767
+ </div>
768
+ </div>
769
+ <div class="model-fields">
770
+ <label class="model-row">
771
+ <span class="model-label">Name</span>
772
+ <input
773
+ type="text"
774
+ name="name"
775
+ class="image-input"
776
+ bind:value={gitName}
777
+ spellcheck="false"
778
+ autocapitalize="off"
779
+ autocorrect="off"
780
+ placeholder="Jane Doe"
781
+ />
782
+ </label>
783
+ <label class="model-row">
784
+ <span class="model-label">Email</span>
785
+ <input
786
+ type="text"
787
+ name="email"
788
+ class="image-input"
789
+ bind:value={gitEmail}
790
+ spellcheck="false"
791
+ autocapitalize="off"
792
+ autocorrect="off"
793
+ placeholder="jane@example.com"
794
+ />
795
+ </label>
796
+ <div class="model-save-row">
797
+ <Button type="submit" disabled={savingGitIdentity}>Save</Button>
798
+ </div>
799
+ </div>
800
+ {#if gitIdentityError}
801
+ <div class="msg error">{gitIdentityError}</div>
802
+ {:else if gitIdentitySaved}
803
+ <div class="msg ok">Saved.</div>
804
+ {/if}
805
+ </form>
806
+ {/if}
807
+ </section>
808
+
577
809
  <section class="card">
578
810
  <form
579
811
  class="row"
@@ -582,7 +814,7 @@
582
814
  {@attach enhance(buildCacheToggleOpts)}
583
815
  >
584
816
  <div class="label">
585
- <Layers size={18} />
817
+ <Layers size={20} />
586
818
  <div class="text">
587
819
  <div class="name">Disable build cache</div>
588
820
  <div class="desc">
@@ -616,7 +848,7 @@
616
848
  {@attach enhance(clearCacheOpts)}
617
849
  >
618
850
  <div class="label">
619
- <Trash2 size={18} />
851
+ <Trash2 size={20} />
620
852
  <div class="text">
621
853
  <div class="name">Clear build cache</div>
622
854
  <div class="desc">
@@ -642,7 +874,7 @@
642
874
  {@attach enhance(rebuildAllOpts)}
643
875
  >
644
876
  <div class="label">
645
- <Hammer size={18} />
877
+ <Hammer size={20} />
646
878
  <div class="text">
647
879
  <div class="name">Rebuild running containers (no cache)</div>
648
880
  <div class="desc">
@@ -670,7 +902,7 @@
670
902
  {@attach enhance(manualTokensToggleOpts)}
671
903
  >
672
904
  <div class="label">
673
- <KeyRound size={18} />
905
+ <KeyRound size={20} />
674
906
  <div class="text">
675
907
  <div class="name">
676
908
  Set tokens manually
@@ -791,7 +1023,7 @@
791
1023
  {/if}
792
1024
  </section>
793
1025
 
794
- <section class="card" class:disabled-card={manualTokens}>
1026
+ <section class="card" class:disabled-card={litellmBlocker}>
795
1027
  <form
796
1028
  class="row"
797
1029
  method="POST"
@@ -799,17 +1031,17 @@
799
1031
  {@attach enhance(customEndpointToggleOpts)}
800
1032
  >
801
1033
  <div class="label">
802
- <KeyRound size={18} />
1034
+ <KeyRound size={20} />
803
1035
  <div class="text">
804
1036
  <div class="name">
805
1037
  LiteLLM + Bedrock
806
- {#if manualTokens}
807
- <span class="arch" title="Disabled while Set tokens manually is on">disabled</span>
1038
+ {#if litellmBlocker}
1039
+ <span class="arch" title="Disabled while {litellmBlocker} is on">disabled</span>
808
1040
  {/if}
809
1041
  </div>
810
1042
  <div class="desc">
811
- {#if manualTokens}
812
- Not available while "Set tokens manually" is enabled — these modes are mutually
1043
+ {#if litellmBlocker}
1044
+ Not available while "{litellmBlocker}" is enabled — these modes are mutually
813
1045
  incompatible.
814
1046
  {:else}
815
1047
  Route <code>claude</code> through a LiteLLM proxy fronting AWS Bedrock instead of Anthropic's
@@ -824,7 +1056,7 @@
824
1056
  type="checkbox"
825
1057
  name="enabled"
826
1058
  checked={customEndpoint}
827
- disabled={savingCustomToggle || manualTokens}
1059
+ disabled={savingCustomToggle || !!litellmBlocker}
828
1060
  onchange={(e) => {
829
1061
  customEndpoint = e.currentTarget.checked;
830
1062
  e.currentTarget.form?.requestSubmit();
@@ -1005,6 +1237,149 @@
1005
1237
  {/if}
1006
1238
  </section>
1007
1239
 
1240
+ <section class="card" class:disabled-card={customEndpoint}>
1241
+ <form
1242
+ class="row"
1243
+ method="POST"
1244
+ action="?/manualModelToggle"
1245
+ {@attach enhance(manualModelToggleOpts)}
1246
+ >
1247
+ <div class="label">
1248
+ <Cpu size={18} />
1249
+ <div class="text">
1250
+ <div class="name">
1251
+ Override models manually
1252
+ {#if customEndpoint}
1253
+ <span class="arch" title="Disabled while LiteLLM + Bedrock is on">disabled</span>
1254
+ {/if}
1255
+ </div>
1256
+ <div class="desc">
1257
+ {#if customEndpoint}
1258
+ Not available while "LiteLLM + Bedrock" is enabled — these modes are mutually
1259
+ incompatible.
1260
+ {:else}
1261
+ Pin which model <code>claude</code> uses on the standard (subscription) path. Only
1262
+ the fields you fill are injected into new containers as
1263
+ <code>ANTHROPIC_*_MODEL</code>
1264
+ variables; blanks fall through to Claude Code's own defaults.
1265
+ {/if}
1266
+ </div>
1267
+ </div>
1268
+ </div>
1269
+ <label class="switch">
1270
+ <input
1271
+ type="checkbox"
1272
+ name="enabled"
1273
+ checked={manualModelOverride}
1274
+ disabled={savingManualModelToggle || customEndpoint}
1275
+ onchange={(e) => {
1276
+ manualModelOverride = e.currentTarget.checked;
1277
+ e.currentTarget.form?.requestSubmit();
1278
+ }}
1279
+ />
1280
+ <span class="track"><span class="thumb"></span></span>
1281
+ </label>
1282
+ </form>
1283
+ {#if manualModelToggleError}
1284
+ <div class="sub"><div class="msg error">{manualModelToggleError}</div></div>
1285
+ {/if}
1286
+
1287
+ {#if manualModelOverride}
1288
+ <form
1289
+ class="row divided token-row"
1290
+ method="POST"
1291
+ action="?/manualModels"
1292
+ {@attach enhance(manualModelsOpts)}
1293
+ >
1294
+ <div class="label">
1295
+ <div class="text">
1296
+ <div class="name">Model IDs</div>
1297
+ <div class="desc">
1298
+ A model name or alias per tier, e.g. <code>opusplan</code>, <code>opus</code>,
1299
+ <code>sonnet</code>, or a full
1300
+ <code>claude-…</code> id. Leave a field blank to skip it.
1301
+ </div>
1302
+ </div>
1303
+ </div>
1304
+ <div class="model-fields">
1305
+ <label class="model-row">
1306
+ <span class="model-label">Opus</span>
1307
+ <input
1308
+ type="text"
1309
+ name="opusModel"
1310
+ class="image-input"
1311
+ bind:value={manualOpus}
1312
+ spellcheck="false"
1313
+ autocapitalize="off"
1314
+ autocorrect="off"
1315
+ autocomplete="off"
1316
+ />
1317
+ </label>
1318
+ <label class="model-row">
1319
+ <span class="model-label">Sonnet</span>
1320
+ <input
1321
+ type="text"
1322
+ name="sonnetModel"
1323
+ class="image-input"
1324
+ bind:value={manualSonnet}
1325
+ spellcheck="false"
1326
+ autocapitalize="off"
1327
+ autocorrect="off"
1328
+ autocomplete="off"
1329
+ />
1330
+ </label>
1331
+ <label class="model-row">
1332
+ <span class="model-label">Haiku</span>
1333
+ <input
1334
+ type="text"
1335
+ name="haikuModel"
1336
+ class="image-input"
1337
+ bind:value={manualHaiku}
1338
+ spellcheck="false"
1339
+ autocapitalize="off"
1340
+ autocorrect="off"
1341
+ autocomplete="off"
1342
+ />
1343
+ </label>
1344
+ <label class="model-row">
1345
+ <span class="model-label">Small/fast</span>
1346
+ <input
1347
+ type="text"
1348
+ name="smallFastModel"
1349
+ class="image-input"
1350
+ bind:value={manualSmallFast}
1351
+ spellcheck="false"
1352
+ autocapitalize="off"
1353
+ autocorrect="off"
1354
+ autocomplete="off"
1355
+ />
1356
+ </label>
1357
+ <label class="model-row">
1358
+ <span class="model-label">Default</span>
1359
+ <input
1360
+ type="text"
1361
+ name="defaultModel"
1362
+ class="image-input"
1363
+ bind:value={manualDefault}
1364
+ spellcheck="false"
1365
+ autocapitalize="off"
1366
+ autocorrect="off"
1367
+ autocomplete="off"
1368
+ />
1369
+ </label>
1370
+ <div class="model-save-row">
1371
+ <Button type="submit" disabled={savingManualModels}>Save models</Button>
1372
+ </div>
1373
+ </div>
1374
+ {#if manualModelsError}
1375
+ <div class="msg error">{manualModelsError}</div>
1376
+ {:else if manualModelsMsg}
1377
+ <div class="msg ok">{manualModelsMsg}</div>
1378
+ {/if}
1379
+ </form>
1380
+ {/if}
1381
+ </section>
1382
+
1008
1383
  <section class="card">
1009
1384
  <form
1010
1385
  class="row"
@@ -1013,7 +1388,7 @@
1013
1388
  {@attach enhance(hostEnvVarsToggleOpts)}
1014
1389
  >
1015
1390
  <div class="label">
1016
- <Variable size={18} />
1391
+ <Variable size={20} />
1017
1392
  <div class="text">
1018
1393
  <div class="name">Host environment variables</div>
1019
1394
  <div class="desc">
@@ -1111,7 +1486,7 @@
1111
1486
  <section class="card">
1112
1487
  <div class="row">
1113
1488
  <div class="label">
1114
- <SunMoon size={18} />
1489
+ <SunMoon size={20} />
1115
1490
  <div class="text">
1116
1491
  <div class="name">Theme</div>
1117
1492
  <div class="desc">
@@ -1126,7 +1501,7 @@
1126
1501
  <section class="card">
1127
1502
  <div class="row">
1128
1503
  <div class="label">
1129
- <Volume2 size={18} />
1504
+ <Volume2 size={20} />
1130
1505
  <div class="text">
1131
1506
  <div class="name">Attention sound</div>
1132
1507
  <div class="desc">
@@ -1145,10 +1520,55 @@
1145
1520
  </div>
1146
1521
  </section>
1147
1522
 
1523
+ <section class="card">
1524
+ <form class="row" method="POST" action="?/petToggle" {@attach enhance(petToggleOpts)}>
1525
+ <div class="label">
1526
+ <PawPrint size={18} />
1527
+ <div class="text">
1528
+ <div class="name">Pet logo</div>
1529
+ <div class="desc">Swap the box in the header for a pixel pet.</div>
1530
+ </div>
1531
+ </div>
1532
+ <label class="switch">
1533
+ <input
1534
+ type="checkbox"
1535
+ name="enabled"
1536
+ checked={!!petArt}
1537
+ disabled={savingPet}
1538
+ onchange={(e) => e.currentTarget.form?.requestSubmit()}
1539
+ />
1540
+ <span class="track"><span class="thumb"></span></span>
1541
+ </label>
1542
+ </form>
1543
+ {#if petError}
1544
+ <div class="sub"><div class="msg error">{petError}</div></div>
1545
+ {/if}
1546
+ {#if petArt}
1547
+ <div class="sub pets">
1548
+ {#each avatars as art (art.name)}
1549
+ <form method="POST" action="?/petChoose" {@attach enhance(petChooseOpts(art))}>
1550
+ <input type="hidden" name="name" value={art.name} />
1551
+ <button
1552
+ type="submit"
1553
+ class="pet"
1554
+ class:selected={art.name === petArt.name}
1555
+ disabled={savingPet}
1556
+ title={art.name}
1557
+ aria-label={art.name}
1558
+ aria-pressed={art.name === petArt.name}
1559
+ >
1560
+ <Avatar {art} name={art.name} scale={4} />
1561
+ </button>
1562
+ </form>
1563
+ {/each}
1564
+ </div>
1565
+ {/if}
1566
+ </section>
1567
+
1148
1568
  <section class="card danger-card">
1149
1569
  <form class="row" method="POST" action="?/shutdown" {@attach enhance(shutdownOpts)}>
1150
1570
  <div class="label">
1151
- <Power size={18} />
1571
+ <Power size={20} />
1152
1572
  <div class="text">
1153
1573
  <div class="name">Delete database, containers, and shut down</div>
1154
1574
  <div class="desc">
@@ -1270,6 +1690,10 @@
1270
1690
  color: var(--ink);
1271
1691
  min-width: 0;
1272
1692
  }
1693
+ /* Keep the leading icon at its intrinsic size; the flex row would otherwise shrink busier glyphs below 18px. */
1694
+ .label > :global(svg) {
1695
+ flex: none;
1696
+ }
1273
1697
  .text {
1274
1698
  min-width: 0;
1275
1699
  }
@@ -1351,6 +1775,40 @@
1351
1775
  .msg.ok {
1352
1776
  color: var(--ink-soft);
1353
1777
  }
1778
+ /* auto-fill so the grid reflows as the catalog grows, with no column count to keep in sync. */
1779
+ .pets {
1780
+ display: grid;
1781
+ grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
1782
+ gap: 6px;
1783
+ }
1784
+ /* Each pet is wrapped in its own form; let the button stay the grid item. */
1785
+ .pets form {
1786
+ display: contents;
1787
+ }
1788
+ .pet:disabled {
1789
+ cursor: progress;
1790
+ }
1791
+ .pet {
1792
+ display: flex;
1793
+ align-items: center;
1794
+ justify-content: center;
1795
+ padding: 4px;
1796
+ appearance: none;
1797
+ background: transparent;
1798
+ border: 1px solid transparent;
1799
+ cursor: pointer;
1800
+ }
1801
+ .pet:hover {
1802
+ border-color: var(--rule);
1803
+ }
1804
+ .pet.selected {
1805
+ border-color: var(--attn-done);
1806
+ background: var(--switch-on-bg);
1807
+ }
1808
+ .pet:focus-visible {
1809
+ outline: 2px solid var(--ink);
1810
+ outline-offset: 1px;
1811
+ }
1354
1812
  .switch {
1355
1813
  position: relative;
1356
1814
  flex: none;