codebay 0.0.1 → 0.2.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 (202) 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 +1608 -0
  4. package/.mochi/server-island.js +1 -0
  5. package/.mochi/svelte-client/HydratableIsland-ps09gjy2.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-62w5zwww.js +2 -0
  7. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-jmm501zf.js +13 -0
  8. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-ewk4n2c0.js +1 -0
  9. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-e7e89md0.js +23 -0
  10. package/.mochi/svelte-client/chunk-jx5cekhh.js +2 -0
  11. package/.mochi/svelte-client/chunk-k3my1snp.js +2 -0
  12. package/.mochi/svelte-client/chunk-kgn1b2t1.js +4 -0
  13. package/.mochi/svelte-client/chunk-ry7q5zk5.js +1 -0
  14. package/.mochi/svelte-client/chunk-x843txjc.js +2 -0
  15. package/.mochi/svelte-client/chunk-zjcg8kw5.js +2 -0
  16. package/.mochi/svelte-compile/App-8k1gyxdc.server.js +1167 -0
  17. package/.mochi/svelte-compile/Avatars-f7c6rp96.server.js +351 -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-yvps12zg.server.js +355 -0
  21. package/.mochi/svelte-compile/Settings-jwg3httb.server.js +1169 -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-dqgstte4.js +52 -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-3bbkzsvnrth3v.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 +33 -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/octopus.ts +17 -0
  109. package/src/avatars/owl.ts +17 -0
  110. package/src/avatars/penguin.ts +17 -0
  111. package/src/avatars/planet.ts +17 -0
  112. package/src/avatars/rabbit.ts +17 -0
  113. package/src/avatars/rat.ts +17 -0
  114. package/src/avatars/robot.ts +17 -0
  115. package/src/avatars/rocket.ts +17 -0
  116. package/src/avatars/sea.ts +17 -0
  117. package/src/avatars/serialize.ts +65 -0
  118. package/src/avatars/skull.ts +17 -0
  119. package/src/avatars/snail.ts +17 -0
  120. package/src/avatars/snake.ts +17 -0
  121. package/src/avatars/snakegame.ts +17 -0
  122. package/src/avatars/star.ts +17 -0
  123. package/src/avatars/svelte.ts +17 -0
  124. package/src/avatars/target.ts +17 -0
  125. package/src/avatars/tree.ts +17 -0
  126. package/src/avatars/types.ts +24 -0
  127. package/src/avatars/ukraine.ts +17 -0
  128. package/src/avatars/whale.ts +17 -0
  129. package/src/avatars/wheat.ts +17 -0
  130. package/src/avatars/yinyan.ts +17 -0
  131. package/src/components/AppBar.svelte +35 -0
  132. package/src/components/AppShell.svelte +302 -0
  133. package/src/components/Avatar.svelte +235 -0
  134. package/src/components/AvatarEditor.svelte +321 -0
  135. package/src/components/AvatarsView.svelte +113 -0
  136. package/src/components/BranchBox.svelte +32 -0
  137. package/src/components/Brand.svelte +41 -0
  138. package/src/components/Button.svelte +161 -0
  139. package/src/components/CoinButton.svelte +121 -0
  140. package/src/components/CredMenu.svelte +171 -0
  141. package/src/components/DashboardView.svelte +198 -0
  142. package/src/components/FolderBrowser.svelte +635 -0
  143. package/src/components/HealthBox.svelte +161 -0
  144. package/src/components/IdeBar.svelte +186 -0
  145. package/src/components/IdeLoader.svelte +135 -0
  146. package/src/components/InstanceCard.svelte +220 -0
  147. package/src/components/InstanceView.svelte +565 -0
  148. package/src/components/PortsBox.svelte +73 -0
  149. package/src/components/SettingsCog.svelte +22 -0
  150. package/src/components/SettingsView.svelte +1484 -0
  151. package/src/components/Skeleton.svelte +43 -0
  152. package/src/components/StatusBadge.svelte +54 -0
  153. package/src/components/ThemePicker.svelte +77 -0
  154. package/src/components/TopBar.svelte +64 -0
  155. package/src/components/UiShowcase.svelte +578 -0
  156. package/src/components/ui-showcase/ComponentDemo.svelte +85 -0
  157. package/src/container-injections/attention-hooks.ts +117 -0
  158. package/src/container-injections/claude-aliases.ts +43 -0
  159. package/src/container-injections/claude-code-credentials.ts +145 -0
  160. package/src/container-injections/claude-code-custom.ts +125 -0
  161. package/src/container-injections/claude-no-coauthor.ts +35 -0
  162. package/src/container-injections/claude-skip-permissions.ts +38 -0
  163. package/src/container-injections/claude-statusline.ts +109 -0
  164. package/src/container-injections/git-identity.ts +63 -0
  165. package/src/container-injections/git-safe-directory.ts +18 -0
  166. package/src/container-injections/github-credentials.ts +162 -0
  167. package/src/container-injections/host-env-vars.ts +108 -0
  168. package/src/container-injections/tmux.ts +67 -0
  169. package/src/fontsource.d.ts +3 -0
  170. package/src/index.ts +81 -0
  171. package/src/lib/auth.server.ts +95 -0
  172. package/src/lib/bridge.server.ts +34 -0
  173. package/src/lib/config.server.ts +122 -0
  174. package/src/lib/crypto.server.ts +12 -0
  175. package/src/lib/db.server.ts +196 -0
  176. package/src/lib/devcontainer.server.ts +503 -0
  177. package/src/lib/docker-client.server.ts +61 -0
  178. package/src/lib/docker.server.ts +139 -0
  179. package/src/lib/exec.server.ts +92 -0
  180. package/src/lib/git.server.ts +68 -0
  181. package/src/lib/health.server.ts +105 -0
  182. package/src/lib/injections.server.ts +72 -0
  183. package/src/lib/instances.server.ts +552 -0
  184. package/src/lib/picker.server.ts +46 -0
  185. package/src/lib/ports.server.ts +9 -0
  186. package/src/lib/proxy.server.ts +170 -0
  187. package/src/lib/repo-url.ts +60 -0
  188. package/src/lib/spawn.server.ts +14 -0
  189. package/src/lib/theme.server.ts +22 -0
  190. package/src/live.ts +50 -0
  191. package/src/pages/App.svelte +14 -0
  192. package/src/pages/Avatars.svelte +7 -0
  193. package/src/pages/Instance.svelte +9 -0
  194. package/src/pages/Settings.svelte +73 -0
  195. package/src/pages/UI.svelte +10 -0
  196. package/src/routes.ts +440 -0
  197. package/src/settings.ts +14 -0
  198. package/src/shell.html +100 -0
  199. package/src/sound.ts +69 -0
  200. package/src/theme.ts +24 -0
  201. package/src/types.ts +79 -0
  202. package/svelte.config.js +5 -0
@@ -0,0 +1,161 @@
1
+ <script lang="ts">
2
+ import Check from '@lucide/svelte/icons/check';
3
+ import X from '@lucide/svelte/icons/x';
4
+ import type { InstanceHealth } from '../types.ts';
5
+ import Skeleton from './Skeleton.svelte';
6
+
7
+ // Intrinsic to every instance, independent of any injection.
8
+ const FIXED_CHECKS = ['Container running', 'Code-server reachable'] as const;
9
+
10
+ let {
11
+ health = null,
12
+ lastFetchedAt = null,
13
+ active = true,
14
+ injectionChecks = 0
15
+ }: {
16
+ /** Latest health snapshot, or null while the first check is pending. */
17
+ health?: InstanceHealth | null;
18
+ /** Epoch ms of the last successful health fetch, for the "updated Ns ago" readout. */
19
+ lastFetchedAt?: number | null;
20
+ /** When false, no checks are coming — the panel grays out rather than showing a loader. */
21
+ active?: boolean;
22
+ /** Registry-derived, so the skeleton renders one row per real check. */
23
+ injectionChecks?: number;
24
+ } = $props();
25
+
26
+ // The value word stays uniform because the tick/cross icon already conveys pass vs fail.
27
+ const checks = $derived.by((): { label: string; ok: boolean; value: string }[] => {
28
+ if (!health) return [];
29
+ const v = (ok: boolean) => ({ ok, value: ok ? 'OK' : '—' });
30
+ return [
31
+ { label: FIXED_CHECKS[0], ...v(health.containerRunning) },
32
+ { label: FIXED_CHECKS[1], ...v(health.codeServerAccessible) },
33
+ ...health.injections.map((i) => ({ label: i.label, ...v(i.ok) }))
34
+ ];
35
+ });
36
+
37
+ // Before the first snapshot lands, the registry-derived count keeps the skeleton honest.
38
+ function getNumberOfChecks(): number {
39
+ return health ? checks.length : FIXED_CHECKS.length + injectionChecks;
40
+ }
41
+
42
+ // A local clock, so the "updated Ns ago" readout counts up between polls.
43
+ let now = $state(Date.now());
44
+ $effect(() => {
45
+ const timer = setInterval(() => (now = Date.now()), 1000);
46
+ return () => clearInterval(timer);
47
+ });
48
+ const agoLabel = $derived.by(() => {
49
+ if (!active && !health) return 'Container not running';
50
+ if (!lastFetchedAt) return 'Waiting for first check…';
51
+ const secs = Math.max(0, Math.round((now - lastFetchedAt) / 1000));
52
+ return `Updated ${secs}s ago`;
53
+ });
54
+ </script>
55
+
56
+ <div class="healthwrap panel" class:dim={!active && !health}>
57
+ <div class="panel-bar">Health</div>
58
+ <div class="health">
59
+ {#if health}
60
+ {#each checks as check (check.label)}
61
+ <div class="hrow">
62
+ <span class="box {check.ok ? 'ok' : 'bad'}">
63
+ {#if check.ok}<Check size={12} strokeWidth={3} />{:else}<X
64
+ size={12}
65
+ strokeWidth={3}
66
+ />{/if}
67
+ </span>
68
+ <span class="hlabel">{check.label}</span>
69
+ <span class="hvalue">{check.value}</span>
70
+ </div>
71
+ {/each}
72
+ {:else if active}
73
+ {#each Array(getNumberOfChecks()) as _, i (i)}
74
+ <div class="hrow">
75
+ <span class="box idle"></span>
76
+ <Skeleton width="120px" />
77
+ <Skeleton variant="pill" />
78
+ </div>
79
+ {/each}
80
+ {:else}
81
+ {#each FIXED_CHECKS as label (label)}
82
+ <div class="hrow">
83
+ <span class="box idle"></span>
84
+ <span class="hlabel">{label}</span>
85
+ <span class="hvalue">—</span>
86
+ </div>
87
+ {/each}
88
+ {/if}
89
+ </div>
90
+ <div class="hfoot">{agoLabel}</div>
91
+ </div>
92
+
93
+ <style>
94
+ .healthwrap {
95
+ overflow: hidden;
96
+ }
97
+ /* Container isn't running yet: gray the panel out instead of loading it. */
98
+ .healthwrap.dim {
99
+ opacity: 0.5;
100
+ }
101
+ .health {
102
+ background: var(--bg-card);
103
+ display: grid;
104
+ grid-template-columns: max-content 1fr max-content;
105
+ align-items: center;
106
+ }
107
+ .hrow {
108
+ display: grid;
109
+ grid-template-columns: subgrid;
110
+ grid-column: 1 / -1;
111
+ align-items: center;
112
+ gap: 12px;
113
+ padding: 10px 14px;
114
+ border-top: 1px solid var(--rule-soft);
115
+ }
116
+ .hrow:first-child {
117
+ border-top: none;
118
+ }
119
+ /* Pass = filled checkbox with a tick; fail = hollow box with a cross. */
120
+ .box {
121
+ width: 16px;
122
+ height: 16px;
123
+ border: 1px solid var(--ink);
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ color: var(--ink);
128
+ }
129
+ .box.ok {
130
+ background: var(--ink);
131
+ color: var(--bg);
132
+ }
133
+ .box.bad {
134
+ background: transparent;
135
+ }
136
+ .box.idle {
137
+ border-color: var(--ink-faint);
138
+ }
139
+ .hlabel {
140
+ font-family: var(--font-mono);
141
+ font-size: 13px;
142
+ color: var(--ink-soft);
143
+ }
144
+ .hvalue {
145
+ font-family: var(--font-display);
146
+ font-weight: 700;
147
+ font-size: 11px;
148
+ letter-spacing: 0.1em;
149
+ text-transform: uppercase;
150
+ color: var(--ink);
151
+ justify-self: end;
152
+ }
153
+ .hfoot {
154
+ padding: 7px 14px;
155
+ border-top: 1px solid var(--rule-soft);
156
+ background: var(--bg);
157
+ font-family: var(--font-mono);
158
+ font-size: 11px;
159
+ color: var(--ink-faint);
160
+ }
161
+ </style>
@@ -0,0 +1,186 @@
1
+ <script lang="ts">
2
+ import { type Instance } from '../types.ts';
3
+ import Settings from '@lucide/svelte/icons/settings';
4
+ import Avatar from './Avatar.svelte';
5
+ import AppBar from './AppBar.svelte';
6
+
7
+ let {
8
+ running,
9
+ active,
10
+ attention,
11
+ editingId,
12
+ editingName = $bindable(),
13
+ onselect,
14
+ onstartrename,
15
+ oncommitrename,
16
+ oncancelrename
17
+ }: {
18
+ running: Instance[];
19
+ active: string;
20
+ attention: Record<string, 'done' | 'waiting' | null>;
21
+ editingId: string | null;
22
+ editingName: string;
23
+ onselect: (id: string) => void;
24
+ onstartrename: (instance: Instance) => void;
25
+ oncommitrename: (id: string) => void;
26
+ oncancelrename: () => void;
27
+ } = $props();
28
+ </script>
29
+
30
+ <AppBar>
31
+ {#if running.length > 0}
32
+ <nav class="tabs">
33
+ {#each running as inst (inst.id)}
34
+ <div
35
+ class="tab"
36
+ class:active={inst.id === active}
37
+ class:attn-done={inst.id !== active && attention[inst.id] === 'done'}
38
+ class:attn-waiting={inst.id !== active && attention[inst.id] === 'waiting'}
39
+ >
40
+ {#if editingId === inst.id}
41
+ <div class="tab-label editing">
42
+ <Avatar id={inst.id} name={inst.name} scale={4} />
43
+ <!-- svelte-ignore a11y_autofocus -->
44
+ <input
45
+ class="tab-name-edit"
46
+ bind:value={editingName}
47
+ autofocus
48
+ onfocus={(e) => (e.currentTarget as HTMLInputElement).select()}
49
+ onblur={() => oncommitrename(inst.id)}
50
+ onkeydown={(e) => {
51
+ if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
52
+ else if (e.key === 'Escape') oncancelrename();
53
+ }}
54
+ />
55
+ </div>
56
+ {:else}
57
+ <button
58
+ type="button"
59
+ class="tab-label"
60
+ onclick={() => onselect(inst.id)}
61
+ ondblclick={() => onstartrename(inst)}
62
+ title={inst.name}
63
+ >
64
+ <Avatar id={inst.id} name={inst.name} scale={4} />
65
+ <span class="tab-name">{inst.name}</span>
66
+ </button>
67
+ {/if}
68
+ </div>
69
+ {/each}
70
+ </nav>
71
+ {/if}
72
+ <a class="cog" href="/settings" title="Settings" aria-label="Settings"><Settings size={18} /></a>
73
+ </AppBar>
74
+
75
+ <style>
76
+ .cog {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ width: 48px;
81
+ flex: none;
82
+ margin-left: auto;
83
+ color: var(--ink);
84
+ border-left: 1px solid var(--rule);
85
+ }
86
+ .cog:hover {
87
+ background: var(--ink);
88
+ color: var(--bg);
89
+ }
90
+ .tabs {
91
+ display: flex;
92
+ align-items: stretch;
93
+ overflow-x: auto;
94
+ min-width: 0;
95
+ }
96
+ .tab {
97
+ display: inline-flex;
98
+ align-items: stretch;
99
+ border-right: 1px solid var(--rule-soft);
100
+ }
101
+ .tab.active {
102
+ background: var(--ink);
103
+ }
104
+ /* The focused tab never gets these, so a pulsing tab always means "needs your eyes". */
105
+ .tab.attn-done {
106
+ animation: attn-pulse-green 1.8s ease-in-out infinite;
107
+ }
108
+ .tab.attn-waiting {
109
+ animation: attn-pulse-amber 1.8s ease-in-out infinite;
110
+ }
111
+ @keyframes attn-pulse-green {
112
+ 0%,
113
+ 100% {
114
+ background: transparent;
115
+ box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--attn-done) 40%, transparent);
116
+ }
117
+ 50% {
118
+ background: color-mix(in srgb, var(--attn-done) 32%, transparent);
119
+ box-shadow: inset 0 -2px 0 var(--attn-done);
120
+ }
121
+ }
122
+ @keyframes attn-pulse-amber {
123
+ 0%,
124
+ 100% {
125
+ background: transparent;
126
+ box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--attn-waiting) 40%, transparent);
127
+ }
128
+ 50% {
129
+ background: color-mix(in srgb, var(--attn-waiting) 32%, transparent);
130
+ box-shadow: inset 0 -2px 0 var(--attn-waiting);
131
+ }
132
+ }
133
+ @media (prefers-reduced-motion: reduce) {
134
+ .tab.attn-done,
135
+ .tab.attn-waiting {
136
+ animation-duration: 0s;
137
+ background: color-mix(in srgb, var(--attn-done) 28%, transparent);
138
+ }
139
+ .tab.attn-waiting {
140
+ background: color-mix(in srgb, var(--attn-waiting) 28%, transparent);
141
+ }
142
+ }
143
+ .tab-label {
144
+ appearance: none;
145
+ background: transparent;
146
+ border: 0;
147
+ cursor: pointer;
148
+ color: var(--ink-soft);
149
+ font-family: var(--font-mono);
150
+ display: inline-flex;
151
+ align-items: center;
152
+ gap: 10px;
153
+ padding: 0 12px;
154
+ max-width: 240px;
155
+ font-size: 12px;
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.06em;
158
+ }
159
+ .tab-name {
160
+ white-space: nowrap;
161
+ overflow: hidden;
162
+ text-overflow: ellipsis;
163
+ }
164
+ .tab:not(.active) .tab-label:hover {
165
+ color: var(--ink);
166
+ }
167
+ .tab.active .tab-label {
168
+ color: var(--bg);
169
+ }
170
+ .tab-label.editing {
171
+ cursor: default;
172
+ }
173
+ .tab-name-edit {
174
+ width: 130px;
175
+ font-family: var(--font-mono);
176
+ font-size: 12px;
177
+ text-transform: uppercase;
178
+ letter-spacing: 0.06em;
179
+ margin: 0;
180
+ padding: 2px 4px;
181
+ border: 1px solid var(--ink-soft);
182
+ background: var(--bg);
183
+ color: var(--ink);
184
+ outline: none;
185
+ }
186
+ </style>
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import Button from './Button.svelte';
3
+
4
+ // A fixed flip count per tick, rather than a per-bit probability, is what keeps
5
+ // the cadence visually even; `speed` scales only the interval, never the churn.
6
+ let {
7
+ speed = 1,
8
+ // Omit both unless the wait could in principle never end.
9
+ stalledAfterMs,
10
+ onoverride
11
+ }: { speed?: number; stalledAfterMs?: number; onoverride?: () => void } = $props();
12
+
13
+ let stalled = $state(false);
14
+ $effect(() => {
15
+ if (stalledAfterMs === undefined) return;
16
+ const timer = setTimeout(() => (stalled = true), stalledAfterMs);
17
+ return () => clearTimeout(timer);
18
+ });
19
+
20
+ const ROWS = 3;
21
+ const COLS = 22;
22
+ const BASE_TICK_MS = 140; // pace at speed = 1: slower than a flicker, calm and readable
23
+ const FLIPS_PER_TICK = 7; // constant churn per tick → stable cadence
24
+ const tickMs = $derived(Math.max(20, BASE_TICK_MS / speed));
25
+ const seed = () =>
26
+ Array.from({ length: ROWS }, () =>
27
+ Array.from({ length: COLS }, () => (Math.random() < 0.5 ? 0 : 1))
28
+ );
29
+
30
+ let grid = $state(seed());
31
+
32
+ $effect(() => {
33
+ // Reduced-motion gets a static readout rather than no loader at all.
34
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return;
35
+ const timer = setInterval(() => {
36
+ const next = grid.map((row) => row.slice());
37
+ for (let i = 0; i < FLIPS_PER_TICK; i++) {
38
+ const row = next[Math.floor(Math.random() * ROWS)]!;
39
+ const c = Math.floor(Math.random() * COLS);
40
+ row[c] = row[c] ? 0 : 1;
41
+ }
42
+ grid = next;
43
+ }, tickMs);
44
+ return () => clearInterval(timer);
45
+ });
46
+ </script>
47
+
48
+ <div class="loader">
49
+ <div class="grid" aria-hidden="true">
50
+ {#each grid as row, i (i)}
51
+ <div class="row">{row.join('')}</div>
52
+ {/each}
53
+ </div>
54
+ <div class="label">
55
+ Loading editor<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
56
+ </div>
57
+ {#if stalled}
58
+ <div class="stalled">
59
+ <p>Waiting for code-server to answer inside the container.</p>
60
+ {#if onoverride}
61
+ <Button size="sm" onclick={onoverride}>Open anyway</Button>
62
+ {/if}
63
+ </div>
64
+ {/if}
65
+ </div>
66
+
67
+ <style>
68
+ .loader {
69
+ position: absolute;
70
+ inset: 0;
71
+ display: flex;
72
+ flex-direction: column;
73
+ align-items: center;
74
+ justify-content: center;
75
+ gap: 20px;
76
+ background: var(--bg);
77
+ color: var(--ink);
78
+ }
79
+ .grid {
80
+ font-family: var(--font-display);
81
+ font-size: 30px;
82
+ line-height: 1.1;
83
+ letter-spacing: 0.22em;
84
+ color: var(--ink-faint);
85
+ }
86
+ .row {
87
+ white-space: pre;
88
+ }
89
+ .label {
90
+ font-family: var(--font-mono);
91
+ font-size: 18px;
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.18em;
94
+ color: var(--ink-soft);
95
+ }
96
+ /* Held back until the wait looks wedged, so it reads as an explanation, not boot chrome. */
97
+ .stalled {
98
+ display: flex;
99
+ flex-direction: column;
100
+ align-items: center;
101
+ gap: 12px;
102
+ max-width: 34ch;
103
+ text-align: center;
104
+ }
105
+ .stalled p {
106
+ margin: 0;
107
+ font-family: var(--font-mono);
108
+ font-size: 13px;
109
+ line-height: 1.5;
110
+ color: var(--ink-faint);
111
+ }
112
+ .dot {
113
+ animation: blink 1.2s ease-in-out infinite;
114
+ }
115
+ .dot:nth-child(2) {
116
+ animation-delay: 0.2s;
117
+ }
118
+ .dot:nth-child(3) {
119
+ animation-delay: 0.4s;
120
+ }
121
+ @keyframes blink {
122
+ 0%,
123
+ 100% {
124
+ opacity: 0.2;
125
+ }
126
+ 50% {
127
+ opacity: 1;
128
+ }
129
+ }
130
+ @media (prefers-reduced-motion: reduce) {
131
+ .dot {
132
+ animation: none;
133
+ }
134
+ }
135
+ </style>
@@ -0,0 +1,220 @@
1
+ <script lang="ts">
2
+ import { type Instance } from '../types.ts';
3
+ import Avatar from './Avatar.svelte';
4
+ import BranchBox from './BranchBox.svelte';
5
+ import PortsBox from './PortsBox.svelte';
6
+ import StatusBadge from './StatusBadge.svelte';
7
+ import Button from './Button.svelte';
8
+
9
+ let {
10
+ instance,
11
+ editing,
12
+ editingName = $bindable(),
13
+ onact,
14
+ onstartrename,
15
+ oncommitrename,
16
+ oncancelrename
17
+ }: {
18
+ instance: Instance;
19
+ editing: boolean;
20
+ editingName: string;
21
+ onact: (action: 'start' | 'stop' | 'delete' | 'rebuild') => void;
22
+ onstartrename: () => void;
23
+ oncommitrename: () => void;
24
+ oncancelrename: () => void;
25
+ } = $props();
26
+
27
+ // Start/Stop only cycle the container; rebuild is what re-runs the injections.
28
+ const canRebuild = $derived(
29
+ instance.status === 'running' ||
30
+ instance.status === 'stopped' ||
31
+ (instance.status === 'error' && !!instance.container_id)
32
+ );
33
+ </script>
34
+
35
+ <li class="card panel">
36
+ <div class="card-head">
37
+ <Avatar id={instance.id} name={instance.name} interactive />
38
+ {#if editing}
39
+ <!-- svelte-ignore a11y_autofocus -->
40
+ <input
41
+ class="name-edit"
42
+ bind:value={editingName}
43
+ autofocus
44
+ onfocus={(e) => (e.currentTarget as HTMLInputElement).select()}
45
+ onblur={oncommitrename}
46
+ onkeydown={(e) => {
47
+ if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
48
+ else if (e.key === 'Escape') oncancelrename();
49
+ }}
50
+ />
51
+ {:else}
52
+ <button class="name" title="Click to rename" onclick={onstartrename}>{instance.name}</button>
53
+ {/if}
54
+ {#if instance.attention}
55
+ <span
56
+ class="attn"
57
+ class:attn-done={instance.attention === 'done'}
58
+ class:attn-waiting={instance.attention === 'waiting'}
59
+ title={instance.attention === 'waiting' ? 'Claude is waiting for input' : 'Claude finished'}
60
+ aria-label={instance.attention === 'waiting'
61
+ ? 'Claude is waiting for input'
62
+ : 'Claude finished'}
63
+ ></span>
64
+ {/if}
65
+ <StatusBadge status={instance.status} />
66
+ </div>
67
+ <div class="path" title={instance.source_path}>{instance.source_path}</div>
68
+ {#if instance.status === 'running'}
69
+ <PortsBox ports={instance.forwarded_ports} />
70
+ {/if}
71
+ {#if instance.git_branch}
72
+ <BranchBox branch={instance.git_branch} />
73
+ {/if}
74
+ {#if instance.status === 'error' && instance.error}
75
+ <div class="card-error">{instance.error}</div>
76
+ {/if}
77
+ <div class="actions">
78
+ {#if instance.status === 'running'}
79
+ <Button variant="primary" size="sm" href={`/ide/${instance.id}`}>Open IDE</Button>
80
+ <Button size="sm" onclick={() => onact('stop')}>Stop</Button>
81
+ {:else if instance.status === 'stopped' || (instance.status === 'error' && instance.container_id)}
82
+ <Button size="sm" onclick={() => onact('start')}>Start</Button>
83
+ {:else if instance.status === 'creating'}
84
+ <Button size="sm" href={`/instances/${instance.id}`} target="_blank" rel="noopener noreferrer"
85
+ >View logs</Button
86
+ >
87
+ {/if}
88
+ {#if canRebuild}
89
+ <Button
90
+ size="sm"
91
+ title="Recreate the container and re-run setup (credentials, hooks, port forwards)"
92
+ onclick={() => onact('rebuild')}>Rebuild</Button
93
+ >
94
+ {/if}
95
+ <Button
96
+ size="sm"
97
+ ghost
98
+ href={`/instances/${instance.id}`}
99
+ target="_blank"
100
+ rel="noopener noreferrer">Details</Button
101
+ >
102
+ <Button variant="danger" size="sm" onclick={() => onact('delete')}>Delete</Button>
103
+ </div>
104
+ </li>
105
+
106
+ <style>
107
+ .card {
108
+ background: var(--bg-card);
109
+ padding: 16px 16px 14px;
110
+ transition:
111
+ transform 0.08s steps(2),
112
+ box-shadow 0.08s steps(2);
113
+ }
114
+ .card:hover {
115
+ transform: translate(-1px, -1px);
116
+ box-shadow: 6px 6px 0 var(--ink);
117
+ }
118
+ .card-head {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 11px;
122
+ }
123
+ .attn {
124
+ flex: none;
125
+ width: 14px;
126
+ height: 14px;
127
+ border: 1px solid var(--ink);
128
+ }
129
+ .attn.attn-done {
130
+ background: var(--attn-done);
131
+ animation: card-attn-pulse 1.4s ease-in-out infinite;
132
+ }
133
+ .attn.attn-waiting {
134
+ background: var(--attn-waiting);
135
+ animation: card-attn-pulse 1.4s ease-in-out infinite;
136
+ }
137
+ @keyframes card-attn-pulse {
138
+ 0%,
139
+ 100% {
140
+ box-shadow: 0 0 0 0 transparent;
141
+ filter: brightness(0.85);
142
+ }
143
+ 50% {
144
+ box-shadow: 0 0 7px 2px color-mix(in srgb, currentColor 60%, transparent);
145
+ filter: brightness(1.15);
146
+ }
147
+ }
148
+ .attn.attn-done {
149
+ color: var(--attn-done);
150
+ }
151
+ .attn.attn-waiting {
152
+ color: var(--attn-waiting);
153
+ }
154
+ @media (prefers-reduced-motion: reduce) {
155
+ .attn {
156
+ animation: none;
157
+ filter: none;
158
+ }
159
+ }
160
+ .name {
161
+ flex: 1;
162
+ min-width: 0;
163
+ font-family: var(--font-mono);
164
+ font-weight: 600;
165
+ font-size: 15px;
166
+ overflow: hidden;
167
+ text-overflow: ellipsis;
168
+ white-space: nowrap;
169
+ /* Reset button chrome so it reads as plain text until hovered. */
170
+ margin: 0;
171
+ padding: 2px 4px;
172
+ border: 1px solid transparent;
173
+ background: transparent;
174
+ color: var(--ink);
175
+ text-align: left;
176
+ cursor: pointer;
177
+ }
178
+ .name:hover {
179
+ border-color: var(--ink-faint);
180
+ }
181
+ .name-edit {
182
+ flex: 1;
183
+ min-width: 0;
184
+ font-family: var(--font-mono);
185
+ font-weight: 600;
186
+ font-size: 15px;
187
+ margin: 0;
188
+ padding: 2px 4px;
189
+ border: 1px solid var(--ink);
190
+ background: var(--bg);
191
+ color: var(--ink);
192
+ outline: none;
193
+ }
194
+ .path {
195
+ margin-top: 10px;
196
+ font-family: var(--font-mono);
197
+ font-size: 12px;
198
+ color: var(--ink-soft);
199
+ overflow: hidden;
200
+ text-overflow: ellipsis;
201
+ white-space: nowrap;
202
+ }
203
+ .card-error {
204
+ margin-top: 10px;
205
+ font-family: var(--font-mono);
206
+ font-size: 12px;
207
+ color: var(--ink);
208
+ background: var(--bg);
209
+ border: 1px solid var(--ink);
210
+ padding: 8px 10px;
211
+ max-height: 80px;
212
+ overflow: auto;
213
+ }
214
+ .actions {
215
+ margin-top: 16px;
216
+ display: flex;
217
+ flex-wrap: wrap;
218
+ gap: 8px;
219
+ }
220
+ </style>