codebay 0.9.1 → 0.10.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 (77) hide show
  1. package/.mochi/manifest.json +214 -147
  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 +155 -43
  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 +246 -26
  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
@@ -1,10 +1,4 @@
1
1
  // @bun
2
- // src/types.ts
3
- function ideUrl(instance) {
4
- const base = `/p/${instance.id}/`;
5
- return instance.remote_workspace_folder ? `${base}?folder=${encodeURIComponent(instance.remote_workspace_folder)}` : base;
6
- }
7
-
8
2
  // src/api.ts
9
3
  var APP_REQUEST_HEADER = "X-Codebay-Request";
10
4
  async function apiFetch(url, init, fallbackMessage = "Request failed") {
@@ -55,4 +49,4 @@ function StatusBadge($$renderer, $$props) {
55
49
  $$renderer.push(`<span${$.attr_class(`status ${$.stringify(status)}`, "svelte-1hqr9xd")}>${$.escape(label[status])}</span>`);
56
50
  }
57
51
 
58
- export { ideUrl, apiFetch, apiPost, apiDelete, forwardedPortUrl, StatusBadge };
52
+ export { apiFetch, apiPost, apiDelete, forwardedPortUrl, StatusBadge };
@@ -0,0 +1,15 @@
1
+ // @bun
2
+ // src/types.ts
3
+ var CLAUDE_PERMISSION_MODES = [
4
+ "default",
5
+ "manual",
6
+ "auto",
7
+ "plan"
8
+ ];
9
+ var CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
10
+ function ideUrl(instance) {
11
+ const base = `/p/${instance.id}/`;
12
+ return instance.remote_workspace_folder ? `${base}?folder=${encodeURIComponent(instance.remote_workspace_folder)}` : base;
13
+ }
14
+
15
+ export { CLAUDE_PERMISSION_MODES, CLAUDE_EFFORT_LEVELS, ideUrl };
@@ -285,15 +285,14 @@ var crown_default = art17;
285
285
  // src/avatars/diamond.ts
286
286
  var art18 = {
287
287
  name: "diamond",
288
- robot: true,
289
288
  pixels: [
290
289
  ".######.",
291
- "#.#..#.#",
290
+ "##.#.#.#",
291
+ "#.#.#.#.",
292
292
  "########",
293
- "##.##.##",
294
- ".#.##.#.",
295
- ".##..##.",
296
- "..####..",
293
+ "#.######",
294
+ ".#.####.",
295
+ "..#.##..",
297
296
  "...##..."
298
297
  ]
299
298
  };
@@ -963,16 +962,15 @@ var wasp_default = art58;
963
962
  // src/avatars/whale.ts
964
963
  var art59 = {
965
964
  name: "whale",
966
- robot: true,
967
965
  pixels: [
968
- "....#...",
969
- "...###..",
970
- "..####.#",
971
- ".#.####.",
966
+ "#.#.#...",
967
+ ".#.#....",
968
+ "..#.....",
969
+ "..#.....",
970
+ ".####...",
972
971
  "######.#",
973
- ".#####..",
974
- "..####..",
975
- "...##..."
972
+ "#.####.#",
973
+ "########"
976
974
  ]
977
975
  };
978
976
  var whale_default = art59;
@@ -1106,17 +1104,6 @@ var avatars = [
1106
1104
  windmill_default,
1107
1105
  yinyan_default
1108
1106
  ];
1109
- function fnv1a(s) {
1110
- let h = 2166136261;
1111
- for (let i = 0;i < s.length; i++) {
1112
- h ^= s.charCodeAt(i);
1113
- h = Math.imul(h, 16777619);
1114
- }
1115
- return h >>> 0;
1116
- }
1117
- function pickAvatar(id) {
1118
- return avatars[fnv1a(id) % avatars.length];
1119
- }
1120
1107
  function findAvatar(name) {
1121
1108
  return name ? avatars.find((a) => a.name === name) : undefined;
1122
1109
  }
@@ -1126,7 +1113,7 @@ import"svelte/internal/flags/async";
1126
1113
  import * as $ from "svelte/internal/server";
1127
1114
  function Avatar($$renderer, $$props) {
1128
1115
  $$renderer.component(($$renderer2) => {
1129
- let { id = "", name, scale = 6, art: art63, interactive = false } = $$props;
1116
+ let { name, scale = 6, art: art63, interactive = false } = $$props;
1130
1117
  let pressed = false;
1131
1118
  let ghosting = false;
1132
1119
  let bx = "50%";
@@ -1166,19 +1153,19 @@ function Avatar($$renderer, $$props) {
1166
1153
  }
1167
1154
  const s = $.derived(() => Math.max(1, Math.round(scale)));
1168
1155
  const gap = $.derived(() => s() >= 2 ? 0.5 : 0);
1169
- const resolved = $.derived(() => art63 ?? pickAvatar(id));
1170
- const cells = $.derived(() => decode(resolved()));
1156
+ const resolved = $.derived(() => art63 ?? null);
1157
+ const cells = $.derived(() => resolved() ? decode(resolved()) : []);
1171
1158
  const grid = $.derived(() => Array.from({ length: 100 }, (_, i) => {
1172
1159
  const r = Math.floor(i / 10);
1173
1160
  const c = i % 10;
1174
1161
  const inner = r >= 1 && r <= 8 && c >= 1 && c <= 8;
1175
- return inner ? cells()[(r - 1) * 8 + (c - 1)] : 0;
1162
+ return inner ? cells()[(r - 1) * 8 + (c - 1)] ?? 0 : 0;
1176
1163
  }));
1177
1164
  $$renderer2.push(`<span${$.attr_class("avatar svelte-1pre9qb", undefined, {
1178
1165
  interactive,
1179
1166
  pressed,
1180
1167
  ghosting
1181
- })} role="img"${$.attr("aria-label", name)}${$.attr("title", resolved().name)}${$.attr_style(`width:${$.stringify(10 * s())}px;height:${$.stringify(10 * s())}px;--gap:${$.stringify(gap())}px;--bx:${$.stringify(bx)};--by:${$.stringify(by)};--spin:${$.stringify(spin)}deg;--hue:${$.stringify(hue)}deg`)}><!--[-->`);
1168
+ })} role="img"${$.attr("aria-label", name)}${$.attr("title", resolved()?.name)}${$.attr_style(`width:${$.stringify(10 * s())}px;height:${$.stringify(10 * s())}px;--gap:${$.stringify(gap())}px;--bx:${$.stringify(bx)};--by:${$.stringify(by)};--spin:${$.stringify(spin)}deg;--hue:${$.stringify(hue)}deg`)}><!--[-->`);
1182
1169
  const each_array = $.ensure_array_like(grid());
1183
1170
  for (let i = 0, $$length = each_array.length;i < $$length; i++) {
1184
1171
  let cell = each_array[i];
@@ -1203,6 +1190,7 @@ function Button($$renderer, $$props) {
1203
1190
  variant = "default",
1204
1191
  size = "md",
1205
1192
  ghost = false,
1193
+ square = false,
1206
1194
  icon,
1207
1195
  iconSize,
1208
1196
  href,
@@ -1223,7 +1211,7 @@ function Button($$renderer, $$props) {
1223
1211
  "aria-disabled": disabled || undefined,
1224
1212
  tabindex: disabled ? -1 : undefined,
1225
1213
  ...rest
1226
- }, "svelte-1klcfz0", { ghost })}>`);
1214
+ }, "svelte-1klcfz0", { ghost, square })}>`);
1227
1215
  if (Icon2()) {
1228
1216
  $$renderer.push("<!--[0-->");
1229
1217
  if (Icon2()) {
@@ -1246,7 +1234,7 @@ function Button($$renderer, $$props) {
1246
1234
  class: `btn ${$2.stringify(variant)} ${$2.stringify(size)}`,
1247
1235
  disabled,
1248
1236
  ...rest
1249
- }, "svelte-1klcfz0", { ghost })}>`);
1237
+ }, "svelte-1klcfz0", { ghost, square })}>`);
1250
1238
  if (Icon2()) {
1251
1239
  $$renderer.push("<!--[0-->");
1252
1240
  if (Icon2()) {
@@ -0,0 +1 @@
1
+ .btn.svelte-1klcfz0{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--ink);background:var(--bg-card);color:var(--ink);cursor:pointer;text-decoration:none;justify-content:center;align-items: center;gap:7px;font-weight:600}.btn.svelte-1klcfz0:disabled{opacity:.4;cursor:not-allowed}.btn[aria-disabled=true].svelte-1klcfz0{opacity:.4;cursor:not-allowed;pointer-events:none}.btn.sm.svelte-1klcfz0{padding:7px 12px;font-size:12px}.btn.md.svelte-1klcfz0{padding:9px 16px;font-size:13px}.btn.lg.svelte-1klcfz0{padding:12px 22px;font-size:15px}.btn.square.svelte-1klcfz0{padding:0}.btn.square.sm.svelte-1klcfz0{width:30px;height:30px}.btn.square.md.svelte-1klcfz0{width:35px;height:35px}.btn.square.lg.svelte-1klcfz0{width:44px;height:44px}.btn.default.svelte-1klcfz0:hover:not(:disabled){background:var(--ink);color:var(--bg)}.btn.primary.svelte-1klcfz0{background:var(--ink);color:var(--bg);font-weight:700}.btn.primary.svelte-1klcfz0:hover:not(:disabled){background:var(--bg-card);color:var(--ink)}.btn.ghost.svelte-1klcfz0{background:0 0;border-style:dashed}.btn.ghost.svelte-1klcfz0:hover:not(:disabled){background:var(--ink);color:var(--bg);border-style:solid}.btn.danger.svelte-1klcfz0{color:var(--danger);border-color:var(--danger)}.btn.danger.svelte-1klcfz0:hover:not(:disabled){color:var(--danger);background-color:var(--danger-soft);background-image:linear-gradient(90deg,var(--danger)50%,transparent 0),linear-gradient(90deg,var(--danger)50%,transparent 0),linear-gradient(0deg,var(--danger)50%,transparent 0),linear-gradient(0deg,var(--danger)50%,transparent 0);animation:svelte-1klcfz0-marching-ants 2s linear infinite;background-position:0 0,0 100%,0 0,100% 0;background-repeat:repeat-x,repeat-x,repeat-y,repeat-y;background-size:14px 2px,14px 2px,2px 14px,2px 14px;border-color:#0000}@keyframes svelte-1klcfz0-marching-ants{to{background-position:28px 0,-28px 100%,0 -28px,100% 28px}}
@@ -0,0 +1 @@
1
+ .right.svelte-r39acv{display:flex;align-items:stretch;margin-left:auto}.cog.svelte-r39acv,.reload.svelte-r39acv{display:inline-flex;color:var(--ink);border-left:1px solid var(--rule);flex:none;justify-content:center;align-items: center;width:48px}.reload.svelte-r39acv{appearance:none;cursor:pointer;background:0 0;border-top:0;border-bottom:0;border-right:0;padding:0}.cog.svelte-r39acv:hover,.reload.svelte-r39acv:hover{background:var(--ink);color:var(--bg)}.tabs.svelte-r39acv{display:flex;overflow-x:auto;align-items:stretch;min-width:0}.tab.svelte-r39acv{display:inline-flex;border-right:1px solid var(--rule-soft);align-items:stretch}.tab.active.svelte-r39acv{background:var(--ink)}.tab.attn-done.svelte-r39acv{animation:svelte-r39acv-attn-pulse-green 1.8s ease-in-out infinite}.tab.attn-waiting.svelte-r39acv{animation:svelte-r39acv-attn-pulse-amber 1.8s ease-in-out infinite}@keyframes svelte-r39acv-attn-pulse-green{0%,to{background:0 0;box-shadow:inset 0 -2px 0 color-mix(in srgb,var(--attn-done)40%,transparent)}50%{background:color-mix(in srgb,var(--attn-done)32%,transparent);box-shadow:inset 0 -2px 0 var(--attn-done)}}@keyframes svelte-r39acv-attn-pulse-amber{0%,to{background:0 0;box-shadow:inset 0 -2px 0 color-mix(in srgb,var(--attn-waiting)40%,transparent)}50%{background:color-mix(in srgb,var(--attn-waiting)32%,transparent);box-shadow:inset 0 -2px 0 var(--attn-waiting)}}@media (prefers-reduced-motion:reduce){.tab.attn-done.svelte-r39acv,.tab.attn-waiting.svelte-r39acv{animation-duration:0s;background:color-mix(in srgb,var(--attn-done)28%,transparent)}.tab.attn-waiting.svelte-r39acv{background:color-mix(in srgb,var(--attn-waiting)28%,transparent)}}.tab-label.svelte-r39acv{appearance:none;cursor:pointer;color:var(--ink-soft);font-family:var(--font-mono);display:inline-flex;text-transform:uppercase;letter-spacing:.06em;background:0 0;border:0;align-items: center;gap:10px;max-width:240px;padding:0 12px;font-size:12px}.tab-name.svelte-r39acv{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tab-mode.svelte-r39acv{display:inline-flex;opacity:.75;flex:none;align-items: center}.tab.svelte-r39acv:not(.active) .tab-label:where(.svelte-r39acv):hover{color:var(--ink)}.tab.active.svelte-r39acv .tab-label:where(.svelte-r39acv){color:var(--bg)}.tab-label.editing.svelte-r39acv{cursor:default}.tab-name-edit.svelte-r39acv{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--ink-soft);background:var(--bg);color:var(--ink);outline:none;width:130px;margin:0;padding:2px 4px;font-size:12px}
@@ -1 +1 @@
1
- .topbar.svelte-15fxkvm{display:flex;border-bottom:1px solid var(--rule);align-items: center;gap:18px;padding:16px 24px}.back.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);text-decoration:none;align-items: center;gap:5px;font-size:12px}.back.svelte-15fxkvm:hover{color:var(--ink)}.title.svelte-15fxkvm{display:flex;flex:1;align-items: center;gap:12px}.name.svelte-15fxkvm{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.03em;font-size:20px;font-weight:700}.open.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);text-decoration:none;border:1px solid var(--ink);align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.open.svelte-15fxkvm:hover{background:var(--ink);color:var(--bg)}.restart.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);background:var(--bg);border:1px solid var(--ink);cursor:pointer;align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.restart.svelte-15fxkvm:hover:not(:disabled){background:var(--ink);color:var(--bg)}.restart.svelte-15fxkvm:disabled{opacity:.5;cursor:not-allowed}.restart-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:0;padding:8px 24px 0;font-size:12px}.stage.svelte-15fxkvm{max-width:1200px;margin:0 auto;padding:20px 24px 40px}.meta.svelte-15fxkvm{display:grid;grid-template-columns:max-content 1fr;align-items: center;gap:6px 14px;margin-bottom:18px}.meta.svelte-15fxkvm .k:where(.svelte-15fxkvm){font-family:var(--font-display);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);font-size:12px;font-weight:700}.meta.svelte-15fxkvm code:where(.svelte-15fxkvm){font-family:var(--font-mono);color:var(--ink-soft);font-size:13px}.meta.svelte-15fxkvm code.muted:where(.svelte-15fxkvm){color:var(--ink-faint)}.healthslot.svelte-15fxkvm,.ports.svelte-15fxkvm{margin-bottom:18px}.ports-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.rebuild.svelte-15fxkvm{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--bg);color:var(--ink);border:1px solid var(--bg);cursor:pointer;padding:4px 9px;font-size:10px;font-weight:600}.rebuild.svelte-15fxkvm:hover:not(:disabled){color:var(--bg);background:0 0}.rebuild.svelte-15fxkvm:disabled{opacity:.5;cursor:not-allowed}.ports-body.svelte-15fxkvm{background:var(--bg-card);padding:12px 14px 14px}.port-list.svelte-15fxkvm{list-style:none;display:flex;flex-direction:column;gap:6px;margin:0 0 12px;padding:0}.port-list.svelte-15fxkvm li:where(.svelte-15fxkvm){display:flex;font-family:var(--font-mono);align-items: center;gap:8px;font-size:13px}.port-list.svelte-15fxkvm .cp:where(.svelte-15fxkvm){color:var(--ink);font-weight:600}.port-list.svelte-15fxkvm .arr:where(.svelte-15fxkvm){color:var(--ink-faint)}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm){display:inline-flex;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--ink-faint);align-items: center;gap:4px}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm):hover{border-bottom-color:var(--ink)}.rm.svelte-15fxkvm{border:1px solid var(--ink-faint);color:var(--ink-soft);cursor:pointer;background:0 0;width:20px;height:20px;margin-left:auto;font-size:15px;line-height:1}.rm.svelte-15fxkvm:hover{border-color:var(--danger);color:var(--danger)}.empty.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:0 0 12px;font-size:12px}.add.svelte-15fxkvm{display:flex;gap:8px}.add.svelte-15fxkvm input:where(.svelte-15fxkvm){font-family:var(--font-mono);border:1px solid var(--ink);background:var(--bg);color:var(--ink);outline:none;flex:1;min-width:0;padding:6px 9px;font-size:13px}.add.svelte-15fxkvm button:where(.svelte-15fxkvm){font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--ink);background:var(--bg-card);color:var(--ink);cursor:pointer;padding:6px 14px;font-size:12px;font-weight:600}.add.svelte-15fxkvm button:where(.svelte-15fxkvm):hover{background:var(--ink);color:var(--bg)}.port-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:10px 0 0;font-size:12px}.hint.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:10px 0 0;font-size:11px}.hint.pending.svelte-15fxkvm{color:var(--ink-soft)}.hint.svelte-15fxkvm code:where(.svelte-15fxkvm){color:var(--ink)}.logwrap.svelte-15fxkvm{overflow:hidden}.log-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.log-actions.svelte-15fxkvm{display:flex;align-items: center;gap:6px}.copy.svelte-15fxkvm{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--bg);color:var(--ink);border:1px solid var(--bg);cursor:pointer;padding:4px 9px;font-size:10px;font-weight:600}.copy.svelte-15fxkvm:hover:not(:disabled),.copy.active.svelte-15fxkvm{color:var(--bg);background:0 0}.copy.svelte-15fxkvm:disabled{opacity:.5;cursor:not-allowed}.logs.svelte-15fxkvm{position:relative;background:repeating-linear-gradient(0deg,var(--screen-line)0 1px,transparent 1px 3px),var(--screen-bg);color:var(--screen-ink);overflow:auto;height:calc(100vh - 320px);min-height:360px;padding:14px}.logs.svelte-15fxkvm pre:where(.svelte-15fxkvm){font-family:var(--font-mono);white-space:pre-wrap;word-break:break-word;margin:0;font-size:12.5px;line-height:1.5}@keyframes svelte-15fxkvm-lcd-blink{50%{background:0 0}}.caret.svelte-15fxkvm{display:inline-block;background:var(--screen-ink);vertical-align:text-bottom;animation:svelte-15fxkvm-lcd-blink 1.05s steps(1)infinite;width:.6em;height:1.05em;margin-left:2px}.err.svelte-15fxkvm{background:var(--bg-card);color:var(--ink);border-top:2px solid var(--ink);font-family:var(--font-mono);padding:12px 14px;font-size:13px}
1
+ .topbar.svelte-15fxkvm{display:flex;border-bottom:1px solid var(--rule);align-items: center;gap:18px;padding:16px 24px}.back.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);text-decoration:none;align-items: center;gap:5px;font-size:12px}.back.svelte-15fxkvm:hover{color:var(--ink)}.title.svelte-15fxkvm{display:flex;flex:1;align-items: center;gap:12px}.name.svelte-15fxkvm{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.03em;font-size:20px;font-weight:700}.open.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);text-decoration:none;border:1px solid var(--ink);align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.open.svelte-15fxkvm:hover{background:var(--ink);color:var(--bg)}.restart.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);background:var(--bg);border:1px solid var(--ink);cursor:pointer;align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.restart.svelte-15fxkvm:hover:not(:disabled){background:var(--ink);color:var(--bg)}.restart.svelte-15fxkvm:disabled{opacity:.5;cursor:not-allowed}.restart-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:0;padding:8px 24px 0;font-size:12px}.stage.svelte-15fxkvm{max-width:1200px;margin:0 auto;padding:20px 24px 40px}.meta.svelte-15fxkvm{display:grid;grid-template-columns:max-content 1fr;align-items: center;gap:6px 14px;margin-bottom:18px}.meta.svelte-15fxkvm .k:where(.svelte-15fxkvm){font-family:var(--font-display);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);font-size:12px;font-weight:700}.meta.svelte-15fxkvm code:where(.svelte-15fxkvm){font-family:var(--font-mono);color:var(--ink-soft);font-size:13px}.meta.svelte-15fxkvm code.muted:where(.svelte-15fxkvm){color:var(--ink-faint)}.healthslot.svelte-15fxkvm,.ports.svelte-15fxkvm{margin-bottom:18px}.ports-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.rebuild.svelte-15fxkvm{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--bg);color:var(--ink);border:1px solid var(--bg);cursor:pointer;padding:4px 9px;font-size:10px;font-weight:600}.rebuild.svelte-15fxkvm:hover:not(:disabled){color:var(--bg);background:0 0}.rebuild.svelte-15fxkvm:disabled{opacity:.5;cursor:not-allowed}.ports-body.svelte-15fxkvm{background:var(--bg-card);padding:12px 14px 14px}.port-list.svelte-15fxkvm{list-style:none;display:flex;flex-direction:column;gap:6px;margin:0 0 12px;padding:0}.port-list.svelte-15fxkvm li:where(.svelte-15fxkvm){display:flex;font-family:var(--font-mono);align-items: center;gap:8px;font-size:13px}.port-list.svelte-15fxkvm .cp:where(.svelte-15fxkvm){color:var(--ink);font-weight:600}.port-list.svelte-15fxkvm .arr:where(.svelte-15fxkvm){color:var(--ink-faint)}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm){display:inline-flex;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--ink-faint);align-items: center;gap:4px}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm):hover{border-bottom-color:var(--ink)}.rm.svelte-15fxkvm{border:1px solid var(--ink-faint);color:var(--ink-soft);cursor:pointer;background:0 0;width:20px;height:20px;margin-left:auto;font-size:15px;line-height:1}.rm.svelte-15fxkvm:hover{border-color:var(--danger);color:var(--danger)}.empty.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:0 0 12px;font-size:12px}.add.svelte-15fxkvm{display:flex;gap:8px}.add.svelte-15fxkvm input:where(.svelte-15fxkvm){font-family:var(--font-mono);border:1px solid var(--ink);background:var(--bg);color:var(--ink);outline:none;flex:1;min-width:0;padding:6px 9px;font-size:13px}.add.svelte-15fxkvm button:where(.svelte-15fxkvm){font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--ink);background:var(--bg-card);color:var(--ink);cursor:pointer;padding:6px 14px;font-size:12px;font-weight:600}.add.svelte-15fxkvm button:where(.svelte-15fxkvm):hover{background:var(--ink);color:var(--bg)}.port-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:10px 0 0;font-size:12px}.hint.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:10px 0 0;font-size:11px}.hint.pending.svelte-15fxkvm{color:var(--ink-soft)}.hint.svelte-15fxkvm code:where(.svelte-15fxkvm){color:var(--ink)}.logwrap.svelte-15fxkvm{overflow:hidden}.log-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.log-actions.svelte-15fxkvm{display:flex;align-items: center;gap:6px}.copy.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--bg);color:var(--ink);border:1px solid var(--bg);cursor:pointer;align-items: center;gap:5px;padding:4px 9px;font-size:10px;font-weight:600}.copy.svelte-15fxkvm:hover:not(:disabled),.copy.active.svelte-15fxkvm{color:var(--bg);background:0 0}.dot.svelte-15fxkvm{background:var(--attn-done);border-radius:50%;flex:none;width:6px;height:6px}.copy.svelte-15fxkvm:disabled{opacity:.5;cursor:not-allowed}.logs.svelte-15fxkvm{position:relative;background:repeating-linear-gradient(0deg,var(--screen-line)0 1px,transparent 1px 3px),var(--screen-bg);color:var(--screen-ink);overflow:auto;overflow-anchor:none;height:calc(100vh - 320px);min-height:360px;padding:14px}.logs.svelte-15fxkvm pre:where(.svelte-15fxkvm){font-family:var(--font-mono);white-space:pre-wrap;word-break:break-word;margin:0;font-size:12.5px;line-height:1.5}@keyframes svelte-15fxkvm-lcd-blink{50%{background:0 0}}.caret.svelte-15fxkvm{display:inline-block;background:var(--screen-ink);vertical-align:text-bottom;animation:svelte-15fxkvm-lcd-blink 1.05s steps(1)infinite;width:.6em;height:1.05em;margin-left:2px}.err.svelte-15fxkvm{background:var(--bg-card);color:var(--ink);border-top:2px solid var(--ink);font-family:var(--font-mono);padding:12px 14px;font-size:13px}
@@ -0,0 +1 @@
1
+ .page.svelte-1n6ht58{display:flex;flex-direction:column;min-height:100vh}.title.svelte-1n6ht58{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-soft);align-items: center;padding:0 14px;font-size:12px}.content.svelte-1n6ht58{display:flex;flex-direction:column;flex:1;align-items: center;gap:16px;padding:32px 20px}.card.svelte-1n6ht58{background:var(--bg-card);border:1px solid var(--rule);width:100%;max-width:560px;height:max-content}.danger-card.svelte-1n6ht58{border-color:var(--danger)}details.advanced.svelte-1n6ht58>summary:where(.svelte-1n6ht58){display:flex;cursor:pointer;list-style:none;font-family:var(--font-mono);align-items: center;gap:10px;padding:14px 16px}details.advanced.svelte-1n6ht58>summary:where(.svelte-1n6ht58)::-webkit-details-marker{display:none}details.advanced.svelte-1n6ht58>summary:where(.svelte-1n6ht58) .name:where(.svelte-1n6ht58){font-size:13px}details.advanced.svelte-1n6ht58>summary:where(.svelte-1n6ht58) .hint:where(.svelte-1n6ht58){color:var(--ink-soft);font-size:12px}details.advanced.svelte-1n6ht58>summary:where(.svelte-1n6ht58):after{content:"▸";color:var(--ink-soft);margin-left:auto}details.advanced[open].svelte-1n6ht58>summary:where(.svelte-1n6ht58):after{content:"▾"}.disabled-card.svelte-1n6ht58{opacity:.55;pointer-events:none}.danger-card.svelte-1n6ht58 .btn{white-space:nowrap;flex:none}.version.svelte-1n6ht58{font-family:var(--font-mono);letter-spacing:.08em;color:var(--ink-soft);font-size:11px}.shutting.svelte-1n6ht58{font-family:var(--font-mono);color:var(--danger);text-align:right;flex:none;max-width:200px;font-size:12px;line-height:1.4}.row.svelte-1n6ht58{display:flex;justify-content:space-between;align-items: center;gap:16px;padding:18px}.row.divided.svelte-1n6ht58{border-top:1px solid var(--rule);align-items: flex-start}.row.divided.svelte-1n6ht58 .btn{white-space:nowrap;flex:none}.sub.svelte-1n6ht58{padding:0 18px 14px}code.svelte-1n6ht58{font-family:var(--font-mono);background:var(--bg);border:1px solid var(--rule);border-radius:3px;padding:1px 4px;font-size:.92em}.label.svelte-1n6ht58{display:flex;color:var(--ink);align-items: flex-start;gap:12px;min-width:0}.label.svelte-1n6ht58>svg{flex:none}.text.svelte-1n6ht58{min-width:0}.name.svelte-1n6ht58{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;font-size:13px}.arch.svelte-1n6ht58{letter-spacing:.04em;color:var(--ink-soft);border:1px solid var(--rule);vertical-align:middle;border-radius:3px;margin-left:8px;padding:2px 6px;font-size:11px}.tip.svelte-1n6ht58{color:var(--ink-soft);background:color-mix(in srgb,var(--info)10%,transparent);border:1px solid color-mix(in srgb,var(--info)35%,transparent);border-radius:4px;width:100%;margin-top:4px;padding:8px 10px}.desc.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink-faint);margin-top:4px;font-size:12px;line-height:1.4}.image-row.svelte-1n6ht58,.token-row.svelte-1n6ht58{flex-wrap:wrap}.image-controls.svelte-1n6ht58{display:flex;flex:1;justify-content:flex-end;align-items: center;gap:8px;min-width:220px}.image-input.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink);background:var(--bg);border:1px solid var(--rule);flex:1;min-width:0;padding:8px 10px;font-size:12px}.image-input.svelte-1n6ht58:focus-visible{outline:2px solid var(--ink);outline-offset:1px}.image-controls.svelte-1n6ht58 .btn{flex:none}.msg.svelte-1n6ht58{font-family:var(--font-mono);width:100%;margin-top:2px;font-size:12px;line-height:1.4}.msg.error.svelte-1n6ht58{color:var(--danger)}.msg.ok.svelte-1n6ht58{color:var(--ink-soft)}.pets.svelte-1n6ht58{display:grid;grid-template-columns:repeat(auto-fill,minmax(44px,1fr));gap:6px}.pets.svelte-1n6ht58 form:where(.svelte-1n6ht58){display:contents}.pet.svelte-1n6ht58:disabled{cursor:progress}.pet.svelte-1n6ht58{display:flex;appearance:none;cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center;align-items: center;padding:4px}.pet.svelte-1n6ht58:hover{border-color:var(--rule)}.pet.selected.svelte-1n6ht58{border-color:var(--attn-done);background:var(--switch-on-bg)}.pet.svelte-1n6ht58:focus-visible{outline:2px solid var(--ink);outline-offset:1px}.mode-toggle.svelte-1n6ht58{display:flex;border:1px solid var(--ink);flex:none}.mode-btn.svelte-1n6ht58{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.05em;background:var(--bg);color:var(--ink);cursor:pointer;border:none;align-items: center;gap:6px;padding:7px 14px;font-size:12px;font-weight:700}.mode-btn.svelte-1n6ht58+.mode-btn:where(.svelte-1n6ht58){border-left:1px solid var(--ink)}.mode-btn.active.svelte-1n6ht58{background:var(--ink);color:var(--bg)}.mode-btn.svelte-1n6ht58:disabled{cursor:not-allowed}.switch.svelte-1n6ht58{position:relative;cursor:pointer;flex:none}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58){position:absolute;opacity:0;cursor:pointer;margin:0;inset:0}.track.svelte-1n6ht58{display:block;border:1px solid var(--rule);background:var(--bg);border-radius:999px;width:44px;height:24px;transition:background .15s}.thumb.svelte-1n6ht58{display:block;background:var(--ink-faint);border-radius:999px;width:18px;height:18px;margin:2px;transition:transform .15s,background .15s}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58):checked+.track:where(.svelte-1n6ht58){background:var(--switch-on-bg)}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58):checked+.track:where(.svelte-1n6ht58) .thumb:where(.svelte-1n6ht58){background:var(--ink);transform:translate(20px)}.switch.svelte-1n6ht58 input:where(.svelte-1n6ht58):focus-visible+.track:where(.svelte-1n6ht58){outline:2px solid var(--ink);outline-offset:2px}.model-fields.svelte-1n6ht58{display:flex;flex-direction:column;flex:1;gap:6px;min-width:220px}.model-row.svelte-1n6ht58{display:flex;align-items: center;gap:8px}.model-label.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink-soft);text-align:right;flex:none;width:72px;font-size:11px}.model-row.svelte-1n6ht58 .image-input:where(.svelte-1n6ht58){flex:1}.model-save-row.svelte-1n6ht58{display:flex;justify-content:flex-end;margin-top:4px}.var-row.svelte-1n6ht58{flex-direction:column;align-items:stretch;gap:10px}.var-list.svelte-1n6ht58{display:flex;flex-direction:column;gap:6px}.var-item.svelte-1n6ht58{display:flex;background:var(--bg);border:1px solid var(--rule);align-items: center;gap:8px;padding:6px 8px}.var-name.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink);word-break:break-all;flex:1;min-width:0;font-size:12px}.var-status.svelte-1n6ht58{font-family:var(--font-mono);letter-spacing:.04em;color:var(--danger);flex:none;font-size:11px}.var-status.present.svelte-1n6ht58{color:var(--ink-soft)}.var-remove.svelte-1n6ht58{display:inline-flex;color:var(--ink-soft);border:1px solid var(--rule);cursor:pointer;background:0 0;flex:none;justify-content:center;align-items: center;width:22px;height:22px;padding:0}.var-remove.svelte-1n6ht58:hover:not(:disabled){color:var(--danger);border-color:var(--danger)}.var-remove.svelte-1n6ht58:disabled{opacity:.4;cursor:not-allowed}.var-empty.svelte-1n6ht58{font-family:var(--font-mono);color:var(--ink-faint);font-size:12px}.var-add.svelte-1n6ht58{display:flex;gap:8px}.var-add.svelte-1n6ht58 .btn{flex:none}
@@ -1 +1 @@
1
- .topbar.svelte-1h259us{display:flex;border-bottom:1px solid var(--rule);justify-content:space-between;align-items: center;padding:18px 28px}.topbar-actions.svelte-1h259us{display:inline-flex;align-items: center;gap:14px}
1
+ .topbar.svelte-1h259us{display:flex;border-bottom:1px solid var(--rule);justify-content:space-between;align-items: center;padding:18px 28px}.topbar-actions.svelte-1h259us{display:inline-flex;align-items: center;gap:14px}.new-group.svelte-1h259us{display:inline-flex;align-items: center;gap:6px}
@@ -0,0 +1 @@
1
+ ALTER TABLE instances ADD COLUMN avatar TEXT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebay",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "description": "Web UI for spinning up isolated devcontainer instances from any local folder, each with a browser-based VS Code (code-server) — built for parallel Claude Code workflows.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "build": "mochi-framework build",
48
48
  "start": "bun src/index.ts",
49
49
  "prod": "bun run build && bun run start",
50
- "dev": "MODE=development DISABLE_OPEN_BROWSER=1 DATA_DIR=./.codebay bun src/index.ts",
50
+ "dev": "MODE=development DISABLE_OPEN_BROWSER=1 DATA_DIR=./.codebay HOST=0.0.0.0 bun src/index.ts",
51
51
  "typecheck": "svelte-check --tsconfig ./tsconfig.json --compiler-warnings 'attribute_illegal_colon:ignore' && tsc -p tsconfig.json --noEmit",
52
52
  "test": "bun test",
53
53
  "lint": "prettier --check . && eslint .",
@@ -2,15 +2,14 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'diamond',
5
- robot: true,
6
5
  pixels: [
7
6
  '.######.',
8
- '#.#..#.#',
7
+ '##.#.#.#',
8
+ '#.#.#.#.',
9
9
  '########',
10
- '##.##.##',
11
- '.#.##.#.',
12
- '.##..##.',
13
- '..####..',
10
+ '#.######',
11
+ '.#.####.',
12
+ '..#.##..',
14
13
  '...##...'
15
14
  ]
16
15
  };
@@ -129,22 +129,7 @@ export const avatars: AvatarArt[] = [
129
129
  yinyan
130
130
  ];
131
131
 
132
- // FNV-1a, chosen because it's pure and cheap — the mapping must hold across processes.
133
- function fnv1a(s: string): number {
134
- let h = 2166136261;
135
- for (let i = 0; i < s.length; i++) {
136
- h ^= s.charCodeAt(i);
137
- h = Math.imul(h, 16777619);
138
- }
139
- return h >>> 0;
140
- }
141
-
142
- // Duplicates are possible but rare, since the hash spreads ids evenly across the catalog.
143
- export function pickAvatar(id: string): AvatarArt {
144
- return avatars[fnv1a(id) % avatars.length]!;
145
- }
146
-
147
- /** Lookup by name, for sprites chosen by hand rather than hashed from an id. */
132
+ /** Lookup by name; the server (`pick.server.ts`) resolves an instance's stored sprite name to its art. */
148
133
  export function findAvatar(name: string | undefined): AvatarArt | undefined {
149
134
  return name ? avatars.find((a) => a.name === name) : undefined;
150
135
  }
@@ -0,0 +1,25 @@
1
+ import { avatars } from './index.ts';
2
+ import type { AvatarArt } from './types.ts';
3
+
4
+ // Server-only: uses Bun.hash (a native, non-crypto hash) so we don't hand-roll one. crc32 returns a
5
+ // plain uint32 to bucket directly. Stable across processes/versions, so an id maps to the same sprite.
6
+ function bucket(id: string): number {
7
+ return Bun.hash.crc32(id) % avatars.length;
8
+ }
9
+
10
+ /** A deterministic sprite for an id — the default before a unique one is chosen and persisted. */
11
+ export function pickAvatar(id: string): AvatarArt {
12
+ return avatars[bucket(id)]!;
13
+ }
14
+
15
+ // Prefer the id's deterministic sprite; if it's taken, probe forward so only colliding instances
16
+ // deviate from what `pickAvatar` would give. Falls back to the hashed pick once the catalog is full.
17
+ export function pickUniqueAvatar(id: string, taken: Iterable<string>): AvatarArt {
18
+ const used = new Set(taken);
19
+ const base = bucket(id);
20
+ for (let k = 0; k < avatars.length; k++) {
21
+ const art = avatars[(base + k) % avatars.length]!;
22
+ if (!used.has(art.name)) return art;
23
+ }
24
+ return avatars[base]!;
25
+ }
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'whale',
5
- robot: true,
6
5
  pixels: [
7
- '....#...',
8
- '...###..',
9
- '..####.#',
10
- '.#.####.',
6
+ '#.#.#...',
7
+ '.#.#....',
8
+ '..#.....',
9
+ '..#.....',
10
+ '.####...',
11
11
  '######.#',
12
- '.#####..',
13
- '..####..',
14
- '...##...'
12
+ '#.####.#',
13
+ '########'
15
14
  ]
16
15
  };
17
16
 
@@ -160,9 +160,23 @@
160
160
 
161
161
  const STALLED_AFTER_MS = 10_000;
162
162
 
163
+ const activeInstance = $derived(running.find((i) => i.id === active));
164
+
165
+ // A pane opened with "Open anyway" before code-server was up is stuck on the proxy's 503
166
+ // forever, and a document reload would tear down every other tab's editor.
167
+ const frames: Record<string, HTMLIFrameElement | null> = {};
168
+
169
+ function reloadActive() {
170
+ const inst = activeInstance;
171
+ if (!inst || inst.mode === 'terminal') return;
172
+ loadedFrames.delete(inst.id);
173
+ // `location.replace`, not `src =`: assigning src pushes a parent session-history entry,
174
+ // which would break the shell's pushState back button.
175
+ frames[inst.id]?.contentWindow?.location.replace(ideUrl(inst));
176
+ }
177
+
163
178
  $effect(() => {
164
- const inst = running.find((i) => i.id === active);
165
- document.title = onIde && inst ? `${inst.name} — Codebay` : 'Codebay';
179
+ document.title = onIde && activeInstance ? `${activeInstance.name} — Codebay` : 'Codebay';
166
180
  });
167
181
 
168
182
  // Browsers block audio until the page has been interacted with.
@@ -266,6 +280,9 @@
266
280
  {attention}
267
281
  {editingId}
268
282
  bind:editingName
283
+ onreload={activeInstance && activeInstance.mode !== 'terminal' && mountable(active)
284
+ ? reloadActive
285
+ : undefined}
269
286
  onselect={(id) => navigate(`/ide/${id}`)}
270
287
  onstartrename={startRename}
271
288
  oncommitrename={commitRename}
@@ -295,7 +312,11 @@
295
312
  initialOpen={inst.terminal_split === 1}
296
313
  />
297
314
  {:else}
298
- <iframe src={ideUrl(inst)} title={inst.name} onload={() => loadedFrames.add(inst.id)}
315
+ <iframe
316
+ bind:this={frames[inst.id]}
317
+ src={ideUrl(inst)}
318
+ title={inst.name}
319
+ onload={() => loadedFrames.add(inst.id)}
299
320
  ></iframe>
300
321
  {/if}
301
322
  {/if}
@@ -303,7 +324,11 @@
303
324
  iframe's own `load` is bounded and gets a plain loader. The terminal has no
304
325
  `load` event, so once it's mountable its own "connecting…" covers the gap. -->
305
326
  {#if !mountable(inst.id)}
306
- <IdeLoader stalledAfterMs={STALLED_AFTER_MS} onoverride={() => forced.add(inst.id)} />
327
+ <IdeLoader
328
+ mode={inst.mode}
329
+ stalledAfterMs={STALLED_AFTER_MS}
330
+ onoverride={() => forced.add(inst.id)}
331
+ />
307
332
  {:else if inst.mode !== 'terminal' && !loadedFrames.has(inst.id)}
308
333
  <IdeLoader />
309
334
  {/if}
@@ -1,19 +1,17 @@
1
1
  <script lang="ts">
2
- import { pickAvatar, decode, type AvatarArt } from '../avatars/index.ts';
2
+ import { decode, type AvatarArt } from '../avatars/index.ts';
3
3
 
4
4
  // `scale` is device-pixels per LED cell; the panel is always an integer multiple
5
5
  // of it (10 × scale px) so every pixel lands on a crisp boundary.
6
6
  let {
7
- id = '',
8
7
  name,
9
8
  scale = 6,
10
9
  art,
11
10
  interactive = false
12
11
  }: {
13
- id?: string;
14
12
  name: string;
15
13
  scale?: number;
16
- art?: AvatarArt;
14
+ art?: AvatarArt | null;
17
15
  interactive?: boolean;
18
16
  } = $props();
19
17
 
@@ -67,15 +65,17 @@
67
65
  // 0.5px is a crisp single device-pixel at 2×; at 1× the cells can't spare any gutter.
68
66
  const gap = $derived(s >= 2 ? 0.5 : 0);
69
67
 
70
- const resolved = $derived(art ?? pickAvatar(id));
71
- const cells = $derived(decode(resolved));
68
+ // Instance sprites are resolved server-side and handed in as `art`; a missing or unknown one
69
+ // (null/unset) renders an empty panel rather than a colliding fallback.
70
+ const resolved = $derived(art ?? null);
71
+ const cells = $derived(resolved ? decode(resolved) : []);
72
72
  // The 10×10 outer ring is unlit LED cells — a bezel of real dots, not blank padding.
73
73
  const grid = $derived(
74
74
  Array.from({ length: 100 }, (_, i) => {
75
75
  const r = Math.floor(i / 10);
76
76
  const c = i % 10;
77
77
  const inner = r >= 1 && r <= 8 && c >= 1 && c <= 8;
78
- return inner ? cells[(r - 1) * 8 + (c - 1)] : 0;
78
+ return inner ? (cells[(r - 1) * 8 + (c - 1)] ?? 0) : 0;
79
79
  })
80
80
  );
81
81
  </script>
@@ -87,7 +87,7 @@
87
87
  class:ghosting
88
88
  role="img"
89
89
  aria-label={name}
90
- title={resolved.name}
90
+ title={resolved?.name}
91
91
  style="width:{10 * s}px;height:{10 *
92
92
  s}px;--gap:{gap}px;--bx:{bx};--by:{by};--spin:{spin}deg;--hue:{hue}deg"
93
93
  {onpointerdown}
@@ -8,6 +8,7 @@
8
8
  variant = 'default',
9
9
  size = 'md',
10
10
  ghost = false,
11
+ square = false,
11
12
  icon,
12
13
  iconSize,
13
14
  href,
@@ -20,6 +21,8 @@
20
21
  size?: Size;
21
22
  /** Dashed-outline treatment that fills on hover — used for secondary links. */
22
23
  ghost?: boolean;
24
+ /** Icon-only: drops the padding for a square that matches the row's button height. */
25
+ square?: boolean;
23
26
  /** Optional leading icon component, e.g. a lucide icon like `Plus`. */
24
27
  icon?: Component<{ size?: number }>;
25
28
  /** Override the icon size; defaults to a size that scales with `size`. */
@@ -40,6 +43,7 @@
40
43
  <a
41
44
  class="btn {variant} {size}"
42
45
  class:ghost
46
+ class:square
43
47
  href={disabled ? undefined : href}
44
48
  aria-disabled={disabled || undefined}
45
49
  tabindex={disabled ? -1 : undefined}
@@ -49,7 +53,7 @@
49
53
  {@render children?.()}
50
54
  </a>
51
55
  {:else}
52
- <button class="btn {variant} {size}" class:ghost {onclick} {disabled} {...rest}>
56
+ <button class="btn {variant} {size}" class:ghost class:square {onclick} {disabled} {...rest}>
53
57
  {#if Icon}<Icon size={resolvedIconSize} />{/if}
54
58
  {@render children?.()}
55
59
  </button>
@@ -97,6 +101,23 @@
97
101
  font-size: 15px;
98
102
  }
99
103
 
104
+ /* Sized to match the text button of the same `size`, so the two sit flush in a row. */
105
+ .btn.square {
106
+ padding: 0;
107
+ }
108
+ .btn.square.sm {
109
+ width: 30px;
110
+ height: 30px;
111
+ }
112
+ .btn.square.md {
113
+ width: 35px;
114
+ height: 35px;
115
+ }
116
+ .btn.square.lg {
117
+ width: 44px;
118
+ height: 44px;
119
+ }
120
+
100
121
  .btn.default:hover:not(:disabled) {
101
122
  background: var(--ink);
102
123
  color: var(--bg);
@@ -1,5 +1,10 @@
1
1
  <script lang="ts">
2
- import { type Instance, type InstanceFilter, type Preflight } from '../types.ts';
2
+ import {
3
+ type Instance,
4
+ type InstanceFilter,
5
+ type InstanceMode,
6
+ type Preflight
7
+ } from '../types.ts';
3
8
  import type { AvatarArt } from '../avatars/index.ts';
4
9
  import FolderBrowser from './FolderBrowser.svelte';
5
10
  import InstanceCard from './InstanceCard.svelte';
@@ -28,6 +33,8 @@
28
33
  type Action = 'start' | 'stop' | 'delete' | 'rebuild';
29
34
 
30
35
  let browserOpen = $state(false);
36
+ // Set only by the mode shortcut button; null lets the picker follow the global default.
37
+ let browserMode = $state<InstanceMode | null>(null);
31
38
  let creating = $state(false);
32
39
  let actionError = $state<string | null>(null);
33
40
  let editingId = $state<string | null>(null);
@@ -148,7 +155,11 @@
148
155
  {creating}
149
156
  {filter}
150
157
  {onFilter}
151
- onNew={() => (browserOpen = true)}
158
+ defaultMode={preflight.defaultMode}
159
+ onNew={(mode) => {
160
+ browserMode = mode ?? null;
161
+ browserOpen = true;
162
+ }}
152
163
  onDeleteAll={deleteAll}
153
164
  />
154
165
 
@@ -199,6 +210,7 @@
199
210
  <FolderBrowser
200
211
  onpick={createFrom}
201
212
  defaultMode={preflight.defaultMode}
213
+ initialMode={browserMode}
202
214
  onclose={() => (browserOpen = false)}
203
215
  />
204
216
  {/if}
@@ -15,16 +15,20 @@
15
15
  let {
16
16
  onpick,
17
17
  onclose,
18
- defaultMode = 'ide'
18
+ defaultMode = 'ide',
19
+ initialMode = null
19
20
  }: {
20
21
  onpick: (source: string, opts?: { branch?: string; mode?: InstanceMode }) => void;
21
22
  onclose: () => void;
22
23
  defaultMode?: InstanceMode;
24
+ /** Opens pinned to this mode — set by the dashboard's mode shortcut button. */
25
+ initialMode?: InstanceMode | null;
23
26
  } = $props();
24
27
 
25
28
  // Follows the global default live — settings opens in its own popup, so the toggle there can
26
29
  // move while this picker is open. An explicit choice here wins and is never yanked back.
27
- let modeOverride = $state<InstanceMode | null>(null);
30
+ // svelte-ignore state_referenced_locally
31
+ let modeOverride = $state<InstanceMode | null>(initialMode);
28
32
  const mode = $derived(modeOverride ?? defaultMode);
29
33
 
30
34
  let result = $state<BrowseResult | null>(null);