codebay 0.3.0 → 0.5.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 (56) hide show
  1. package/.mochi/manifest.json +296 -268
  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-3w16y9jh.js → _hydrate-AvatarsView_257s9yq1wwr4f-fxyd5qx4.js} +2 -2
  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-dz1019cy.js +1 -0
  10. package/.mochi/svelte-client/{chunk-qxwt25hn.js → chunk-nmf4dq86.js} +1 -1
  11. package/.mochi/svelte-client/{chunk-0jtmkpx4.js → chunk-ssa0ad2f.js} +2 -2
  12. package/.mochi/svelte-client/chunk-zmmb1c4b.js +2 -0
  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/package.json +8 -8
  25. package/src/avatars/index.ts +5 -0
  26. package/src/components/AppBar.svelte +8 -1
  27. package/src/components/AppShell.svelte +17 -3
  28. package/src/components/Brand.svelte +15 -4
  29. package/src/components/DashboardView.svelte +5 -2
  30. package/src/components/IdeBar.svelte +9 -1
  31. package/src/components/InstanceCard.svelte +7 -3
  32. package/src/components/InstanceView.svelte +4 -1
  33. package/src/components/SettingsCog.svelte +9 -1
  34. package/src/components/SettingsView.svelte +476 -18
  35. package/src/components/TopBar.svelte +4 -1
  36. package/src/container-injections/claude-code-models.ts +74 -0
  37. package/src/container-injections/claude-model.ts +66 -0
  38. package/src/container-injections/claude-no-coauthor.ts +11 -1
  39. package/src/container-injections/claude-statusline.ts +24 -9
  40. package/src/container-injections/git-identity.ts +30 -3
  41. package/src/lib/injections.server.ts +5 -0
  42. package/src/lib/instances.server.ts +10 -1
  43. package/src/lib/popup-nav.ts +37 -0
  44. package/src/pages/App.svelte +10 -3
  45. package/src/pages/Settings.svelte +31 -0
  46. package/src/routes.ts +88 -2
  47. package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +0 -1
  48. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-faqf4hmc.js +0 -2
  49. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-5g8meb6a.js +0 -1
  50. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-jsw4edxb.js +0 -23
  51. package/.mochi/svelte-client/chunk-04qv4w6w.js +0 -2
  52. package/.mochi/svelte-client/chunk-597d17x0.js +0 -1
  53. package/.mochi/svelte-client/chunk-5p31aqh0.js +0 -2
  54. package/.mochi/svelte-client/chunk-gjjhya9m.js +0 -2
  55. package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +0 -1
  56. package/.mochi/svelte-css/SettingsView-1nfdwnm16cpp2.css +0 -1
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  Icon
4
- } from "./chunk-v6796ga4.js";
4
+ } from "./chunk-fpe16ywq.js";
5
5
 
6
6
  // node_modules/@lucide/svelte/dist/icons/plus.svelte
7
7
  import"svelte/internal/flags/async";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  Icon
4
- } from "./chunk-v6796ga4.js";
4
+ } from "./chunk-fpe16ywq.js";
5
5
 
6
6
  // node_modules/@lucide/svelte/dist/icons/x.svelte
7
7
  import"svelte/internal/flags/async";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  Icon
4
- } from "./chunk-v6796ga4.js";
4
+ } from "./chunk-fpe16ywq.js";
5
5
 
6
6
  // src/avatars/anchor.ts
7
7
  var art = {
@@ -916,6 +916,9 @@ function fnv1a(s) {
916
916
  function pickAvatar(id) {
917
917
  return avatars[fnv1a(id) % avatars.length];
918
918
  }
919
+ function findAvatar(name) {
920
+ return name ? avatars.find((a) => a.name === name) : undefined;
921
+ }
919
922
 
920
923
  // src/components/Avatar.svelte
921
924
  import"svelte/internal/flags/async";
@@ -1665,4 +1668,4 @@ function AppBar($$renderer, $$props) {
1665
1668
  $$renderer.push(`<!----></header>`);
1666
1669
  }
1667
1670
 
1668
- export { OFF, ON, ROWS, COLS, avatars, Avatar, Button, Toaster, dist_default, AppBar };
1671
+ export { OFF, ON, ROWS, COLS, avatars, findAvatar, Avatar, Button, Toaster, dist_default, AppBar };
@@ -1770,7 +1770,7 @@ import { randomUUID } from "crypto";
1770
1770
  // node_modules/mochi-framework/src/cache/cache-storage.ts
1771
1771
  import { mkdirSync, rmSync } from "fs";
1772
1772
  import { mkdir, open, readdir, rename, rm, stat, unlink } from "fs/promises";
1773
- import { join, relative } from "path";
1773
+ import { join, relative, resolve } from "path";
1774
1774
 
1775
1775
  // node_modules/mochi-framework/src/events.ts
1776
1776
  var mochiEvents = pinGlobal("__mochi_events__", () => {
@@ -1799,6 +1799,7 @@ var mochiEvents = pinGlobal("__mochi_events__", () => {
1799
1799
  });
1800
1800
 
1801
1801
  // node_modules/mochi-framework/src/cache/cache-storage.ts
1802
+ var sweeperOwners = pinGlobal("__mochi_cache_sweeper_owners__", () => new Map);
1802
1803
  function isBlobRef(value) {
1803
1804
  return typeof value === "object" && value !== null && value.__mochiBlobRef === true;
1804
1805
  }
@@ -1927,6 +1928,8 @@ class FileStorage {
1927
1928
  offloadBinary;
1928
1929
  initialTimer;
1929
1930
  intervalTimer;
1931
+ sweepKey;
1932
+ sweepOwner;
1930
1933
  constructor(options) {
1931
1934
  this.directory = options.directory;
1932
1935
  this.maxAge = options.maxAge ?? 600000;
@@ -2080,7 +2083,7 @@ class FileStorage {
2080
2083
  }));
2081
2084
  return options.reportKeys ? { removed, removedKeys } : { removed };
2082
2085
  }
2083
- dispose() {
2086
+ stopSweepTimers() {
2084
2087
  if (this.initialTimer) {
2085
2088
  clearTimeout(this.initialTimer);
2086
2089
  }
@@ -2090,6 +2093,14 @@ class FileStorage {
2090
2093
  this.initialTimer = undefined;
2091
2094
  this.intervalTimer = undefined;
2092
2095
  }
2096
+ dispose() {
2097
+ this.stopSweepTimers();
2098
+ if (this.sweepKey !== undefined && sweeperOwners.get(this.sweepKey) === this.sweepOwner) {
2099
+ sweeperOwners.delete(this.sweepKey);
2100
+ }
2101
+ this.sweepKey = undefined;
2102
+ this.sweepOwner = undefined;
2103
+ }
2093
2104
  async readKey(filePath) {
2094
2105
  try {
2095
2106
  const parsed = JSON.parse(await Bun.file(filePath).text());
@@ -2162,10 +2173,15 @@ class FileStorage {
2162
2173
  logger.warn(`Cache sweep failed: ${err instanceof Error ? err.message : String(err)}`);
2163
2174
  }
2164
2175
  };
2176
+ const key = resolve(this.directory);
2177
+ sweeperOwners.get(key)?.stop();
2178
+ this.sweepKey = key;
2179
+ this.sweepOwner = { stop: () => this.stopSweepTimers() };
2165
2180
  this.initialTimer = setTimeout(run2, 1000);
2166
2181
  this.intervalTimer = setInterval(run2, intervalMs);
2167
2182
  this.initialTimer.unref?.();
2168
2183
  this.intervalTimer.unref?.();
2184
+ sweeperOwners.set(key, this.sweepOwner);
2169
2185
  }
2170
2186
  }
2171
2187
 
@@ -3057,13 +3073,13 @@ async function fetchImageSource(src, opts) {
3057
3073
  } catch {
3058
3074
  throw new ImageError(502, "Failed to fetch source image");
3059
3075
  }
3060
- const location = res.headers.get("location");
3061
- if (res.status >= 300 && res.status < 400 && location) {
3076
+ const location2 = res.headers.get("location");
3077
+ if (res.status >= 300 && res.status < 400 && location2) {
3062
3078
  if (hop >= maxRedirects) {
3063
3079
  throw new ImageError(502, "Too many redirects");
3064
3080
  }
3065
3081
  await res.body?.cancel();
3066
- target = new URL(location, url).href;
3082
+ target = new URL(location2, url).href;
3067
3083
  continue;
3068
3084
  }
3069
3085
  break;
@@ -3214,6 +3230,12 @@ function HydratableBoundary($$renderer, $$props) {
3214
3230
  $$renderer2.push(`<!---->`);
3215
3231
  });
3216
3232
  }
3233
+ // src/lib/popup-nav.ts
3234
+ var POPUP_PARAM = "popup";
3235
+ function withPopupMarker(href) {
3236
+ return `${href}?${POPUP_PARAM}=1`;
3237
+ }
3238
+
3217
3239
  // node_modules/@lucide/svelte/dist/Icon.svelte
3218
3240
  import"svelte/internal/flags/async";
3219
3241
  import * as $2 from "svelte/internal/server";
@@ -3292,4 +3314,4 @@ function Icon($$renderer, $$props) {
3292
3314
  });
3293
3315
  }
3294
3316
 
3295
- export { emitIslandProps, isDev, cookies, HydratableBoundary, Icon };
3317
+ export { emitIslandProps, isDev, cookies, HydratableBoundary, Icon, withPopupMarker };
@@ -0,0 +1 @@
1
+ .brand.svelte-94cd8h{display:inline-flex;font-family:var(--font-display);text-transform:uppercase;letter-spacing:.04em;color:inherit;text-decoration:none;align-items: center;gap:11px;font-size:22px;font-weight:700}.logo.svelte-94cd8h{display:inline-flex;align-items: center}.words.svelte-94cd8h{display:inline-flex;align-items:baseline;gap:11px}.tagline.svelte-94cd8h{font-family:var(--font-sans);letter-spacing:.08em;color:var(--ink-soft);font-size:12px;font-weight:500}@media (max-width:640px){.tagline.svelte-94cd8h{display:none}}
@@ -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)}.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}.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}
package/README.md CHANGED
@@ -23,6 +23,10 @@ The UI opens at `http://localhost:6969`. State (SQLite DB + per-instance workspa
23
23
  - `CODEBAY_GITHUB_TOKEN` — GitHub token to inject instead of reading `gh auth token` from the host
24
24
  - `DISABLE_OPEN_BROWSER=1` — skip opening the browser on startup
25
25
 
26
+ ## Troubleshooting
27
+
28
+ - `warn: incorrect peer dependency "svelte@5.56.8"` on startup — harmless. It comes from `svelte-french-toast`, whose published peer range predates Svelte 5; the library works correctly on Svelte 5. Nothing to fix.
29
+
26
30
  ## Development
27
31
 
28
32
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebay",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
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",
@@ -60,12 +60,12 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@devcontainers/cli": "^0.88.0",
63
- "@fontsource-variable/doto": "^5.2.10",
64
- "@fontsource-variable/jetbrains-mono": "^5.2.8",
65
- "@lucide/svelte": "^1.27.0",
63
+ "@fontsource-variable/doto": "^5.3.0",
64
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
65
+ "@lucide/svelte": "^1.28.0",
66
66
  "@zihaolam/bun-sqlite-migrations": "^1.0.3",
67
67
  "dockerode": "^5.0.1",
68
- "mochi-framework": "^0.9.0",
68
+ "mochi-framework": "^0.9.1",
69
69
  "svelte": "^5.56.8",
70
70
  "svelte-french-toast": "^1.2.0"
71
71
  },
@@ -74,13 +74,13 @@
74
74
  "@types/bun": "1.3.14",
75
75
  "@types/dockerode": "^4.0.1",
76
76
  "eslint": "^10.8.0",
77
- "eslint-plugin-svelte": "^3.20.0",
77
+ "eslint-plugin-svelte": "^3.22.0",
78
78
  "globals": "^17.8.0",
79
- "prettier": "^3.9.4",
79
+ "prettier": "^3.9.6",
80
80
  "prettier-plugin-svelte": "^4.1.1",
81
81
  "svelte-check": "4.7.4",
82
82
  "typescript": "^6.0.3",
83
- "typescript-eslint": "^8.62.1"
83
+ "typescript-eslint": "^8.65.0"
84
84
  },
85
85
  "resolutions": {
86
86
  "msgpackr-extract": "npm:@mochi-framework/msgpackr-extract-stub@*",
@@ -124,5 +124,10 @@ export function pickAvatar(id: string): AvatarArt {
124
124
  return avatars[fnv1a(id) % avatars.length]!;
125
125
  }
126
126
 
127
+ /** Lookup by name, for sprites chosen by hand rather than hashed from an id. */
128
+ export function findAvatar(name: string | undefined): AvatarArt | undefined {
129
+ return name ? avatars.find((a) => a.name === name) : undefined;
130
+ }
131
+
127
132
  export { decode } from './types.ts';
128
133
  export type { AvatarArt } from './types.ts';
@@ -1,12 +1,19 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import House from '@lucide/svelte/icons/house';
4
+ import { onBackLinkClick } from '../lib/popup-nav.ts';
4
5
 
5
6
  let { children }: { children?: Snippet } = $props();
6
7
  </script>
7
8
 
8
9
  <header class="bar">
9
- <a class="home" href="/" title="All instances" aria-label="All instances"><House size={18} /></a>
10
+ <a
11
+ class="home"
12
+ href="/"
13
+ title="All instances"
14
+ aria-label="All instances"
15
+ onclick={onBackLinkClick}><House size={18} /></a
16
+ >
10
17
  {@render children?.()}
11
18
  </header>
12
19
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { ideUrl, type Instance, type Preflight } from '../types.ts';
3
+ import { findAvatar, type AvatarArt } from '../avatars/index.ts';
3
4
  import { SvelteSet } from 'svelte/reactivity';
4
5
  import DashboardView from './DashboardView.svelte';
5
6
  import IdeBar from './IdeBar.svelte';
@@ -13,8 +14,14 @@
13
14
  let {
14
15
  preflight,
15
16
  initialPath,
16
- snapshot
17
- }: { preflight: Preflight; initialPath: string; snapshot: Instance[] } = $props();
17
+ snapshot,
18
+ pet
19
+ }: {
20
+ preflight: Preflight;
21
+ initialPath: string;
22
+ snapshot: Instance[];
23
+ pet?: AvatarArt;
24
+ } = $props();
18
25
 
19
26
  // Seeding from the SSR snapshot is intentional — the live stream overwrites it.
20
27
  // svelte-ignore state_referenced_locally
@@ -22,6 +29,9 @@
22
29
  // Auth is preserved across stream updates because it's only ever probed at SSR.
23
30
  // svelte-ignore state_referenced_locally
24
31
  let livePreflight = $state<Preflight>(preflight);
32
+ // Seeded from SSR, then updated live so changing the pet in settings swaps the header without a reload.
33
+ // svelte-ignore state_referenced_locally
34
+ let livePet = $state<AvatarArt | undefined>(pet);
25
35
  // svelte-ignore state_referenced_locally
26
36
  let loaded = $state(snapshot.length > 0);
27
37
  const running = $derived(instances.filter((i) => i.status === 'running'));
@@ -147,6 +157,10 @@
147
157
  livePreflight = { ...livePreflight, docker: msg.data.docker, cli: msg.data.cli };
148
158
  return;
149
159
  }
160
+ if (msg.type === 'pet') {
161
+ livePet = findAvatar(msg.data.name ?? undefined);
162
+ return;
163
+ }
150
164
  if (msg.type === 'health') {
151
165
  // A tick in flight when a rebuild starts probes the *old* container and
152
166
  // reports it accessible, which would mount the iframe against the replacement too early.
@@ -223,7 +237,7 @@
223
237
  oncancelrename={cancelRename}
224
238
  />
225
239
  {:else}
226
- <DashboardView preflight={livePreflight} {instances} {loaded} />
240
+ <DashboardView preflight={livePreflight} {instances} {loaded} pet={livePet} />
227
241
  {/if}
228
242
 
229
243
  <!-- Always mounted, only hidden, so the iframes survive navigation. -->
@@ -1,17 +1,23 @@
1
1
  <script lang="ts">
2
2
  import Package from '@lucide/svelte/icons/package';
3
+ import Avatar from './Avatar.svelte';
4
+ import type { AvatarArt } from '../avatars/index.ts';
5
+
6
+ // Resolved server-side and threaded through props, so SSR and hydration agree and the logo can't flash.
7
+ let { pet }: { pet?: AvatarArt } = $props();
3
8
  </script>
4
9
 
5
10
  <a class="brand" href="/"
6
- ><span class="logo"><Package size={22} /></span><span>Codebay</span><span class="tagline"
7
- >devcontainer manager</span
11
+ ><span class="logo"
12
+ >{#if pet}<Avatar art={pet} name={pet.name} scale={4} />{:else}<Package size={22} />{/if}</span
13
+ ><span class="words"><span>Codebay</span><span class="tagline">devcontainer manager</span></span
8
14
  ></a
9
15
  >
10
16
 
11
17
  <style>
12
18
  .brand {
13
19
  display: inline-flex;
14
- align-items: baseline;
20
+ align-items: center;
15
21
  gap: 11px;
16
22
  font-family: var(--font-display);
17
23
  font-weight: 700;
@@ -24,7 +30,12 @@
24
30
  .logo {
25
31
  display: inline-flex;
26
32
  align-items: center;
27
- align-self: center;
33
+ }
34
+ /* Keep the wordmark and tagline on a shared baseline, but center the group against the logo. */
35
+ .words {
36
+ display: inline-flex;
37
+ align-items: baseline;
38
+ gap: 11px;
28
39
  }
29
40
  .tagline {
30
41
  font-family: var(--font-sans);
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { type Instance, type Preflight } from '../types.ts';
3
+ import type { AvatarArt } from '../avatars/index.ts';
3
4
  import FolderBrowser from './FolderBrowser.svelte';
4
5
  import InstanceCard from './InstanceCard.svelte';
5
6
  import TopBar from './TopBar.svelte';
@@ -11,8 +12,9 @@
11
12
  let {
12
13
  preflight,
13
14
  instances,
14
- loaded
15
- }: { preflight: Preflight; instances: Instance[]; loaded: boolean } = $props();
15
+ loaded,
16
+ pet
17
+ }: { preflight: Preflight; instances: Instance[]; loaded: boolean; pet?: AvatarArt } = $props();
16
18
 
17
19
  let browserOpen = $state(false);
18
20
  let creating = $state(false);
@@ -99,6 +101,7 @@
99
101
 
100
102
  <TopBar
101
103
  auth={preflight.auth}
104
+ {pet}
102
105
  canDelete={instances.length > 0}
103
106
  {ready}
104
107
  {creating}
@@ -3,6 +3,7 @@
3
3
  import Settings from '@lucide/svelte/icons/settings';
4
4
  import Avatar from './Avatar.svelte';
5
5
  import AppBar from './AppBar.svelte';
6
+ import { withPopupMarker } from '../lib/popup-nav.ts';
6
7
 
7
8
  let {
8
9
  running,
@@ -69,7 +70,14 @@
69
70
  {/each}
70
71
  </nav>
71
72
  {/if}
72
- <a class="cog" href="/settings" title="Settings" aria-label="Settings"><Settings size={18} /></a>
73
+ <a
74
+ class="cog"
75
+ href={withPopupMarker('/settings')}
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ title="Settings"
79
+ aria-label="Settings"><Settings size={18} /></a
80
+ >
73
81
  </AppBar>
74
82
 
75
83
  <style>
@@ -5,6 +5,7 @@
5
5
  import PortsBox from './PortsBox.svelte';
6
6
  import StatusBadge from './StatusBadge.svelte';
7
7
  import Button from './Button.svelte';
8
+ import { withPopupMarker } from '../lib/popup-nav.ts';
8
9
 
9
10
  let {
10
11
  instance,
@@ -81,8 +82,11 @@
81
82
  {:else if instance.status === 'stopped' || (instance.status === 'error' && instance.container_id)}
82
83
  <Button size="sm" onclick={() => onact('start')}>Start</Button>
83
84
  {:else if instance.status === 'creating'}
84
- <Button size="sm" href={`/instances/${instance.id}`} target="_blank" rel="noopener noreferrer"
85
- >View logs</Button
85
+ <Button
86
+ size="sm"
87
+ href={withPopupMarker(`/instances/${instance.id}`)}
88
+ target="_blank"
89
+ rel="noopener noreferrer">View logs</Button
86
90
  >
87
91
  {/if}
88
92
  {#if canRebuild}
@@ -95,7 +99,7 @@
95
99
  <Button
96
100
  size="sm"
97
101
  ghost
98
- href={`/instances/${instance.id}`}
102
+ href={withPopupMarker(`/instances/${instance.id}`)}
99
103
  target="_blank"
100
104
  rel="noopener noreferrer">Details</Button
101
105
  >
@@ -10,6 +10,7 @@
10
10
  import { apiPost, apiDelete } from '../api.ts';
11
11
  import { enhance } from 'mochi-framework';
12
12
  import type { MochiEnhanceOptions } from 'mochi-framework';
13
+ import { onBackLinkClick, installPopupBackTrap } from '../lib/popup-nav.ts';
13
14
 
14
15
  let { id, injectionChecks = 0 }: { id: string; injectionChecks?: number } = $props();
15
16
 
@@ -24,6 +25,8 @@
24
25
  node.scrollTop = node.scrollHeight;
25
26
  }
26
27
 
28
+ $effect(() => installPopupBackTrap());
29
+
27
30
  // Reset on every connect, because the server replays its whole buffer each time.
28
31
  $effect(() =>
29
32
  liveSocket(
@@ -120,7 +123,7 @@
120
123
  </script>
121
124
 
122
125
  <header class="topbar">
123
- <a class="back" href="/"><ArrowLeft size={15} /> All instances</a>
126
+ <a class="back" href="/" onclick={onBackLinkClick}><ArrowLeft size={15} /> All instances</a>
124
127
  <div class="title">
125
128
  <span class="name">{instance?.name ?? 'Instance'}</span>
126
129
  {#if instance}
@@ -1,8 +1,16 @@
1
1
  <script lang="ts">
2
2
  import Settings from '@lucide/svelte/icons/settings';
3
+ import { withPopupMarker } from '../lib/popup-nav.ts';
3
4
  </script>
4
5
 
5
- <a class="cog" href="/settings" aria-label="Settings" title="Settings"><Settings size={16} /></a>
6
+ <a
7
+ class="cog"
8
+ href={withPopupMarker('/settings')}
9
+ target="_blank"
10
+ rel="noopener noreferrer"
11
+ aria-label="Settings"
12
+ title="Settings"><Settings size={16} /></a
13
+ >
6
14
 
7
15
  <style>
8
16
  .cog {