codebay 0.15.0 → 0.16.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 (90) hide show
  1. package/.mochi/manifest.json +493 -432
  2. package/.mochi/svelte-client/HydratableIsland-6y9p903x.js +1 -0
  3. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-nv6yzk0x.js +2 -0
  4. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-ry83ck8a.js +14 -0
  5. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-vjt4jscf.js +3 -0
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-1jdrk8x6.js +48 -0
  7. package/.mochi/svelte-client/{chunk-5pg0be5f.js → chunk-0vqvn81s.js} +1 -1
  8. package/.mochi/svelte-client/chunk-2dc6352a.js +1 -0
  9. package/.mochi/svelte-client/chunk-3cxaw5hr.js +2 -0
  10. package/.mochi/svelte-client/chunk-3jqc40sp.js +2 -0
  11. package/.mochi/svelte-client/chunk-3pgw6t5j.js +4 -0
  12. package/.mochi/svelte-client/chunk-7g3xt6ez.js +2 -0
  13. package/.mochi/svelte-client/chunk-b5s3mpg0.js +1 -0
  14. package/.mochi/svelte-client/chunk-bhseezf3.js +2 -0
  15. package/.mochi/svelte-client/chunk-bhwkq8er.js +1 -0
  16. package/.mochi/svelte-client/chunk-jm3ge348.js +1 -0
  17. package/.mochi/svelte-client/{chunk-ds9yn9cq.js → chunk-sxcr9gzp.js} +1 -1
  18. package/.mochi/svelte-client/{chunk-a2hty99b.js → chunk-tss2473w.js} +1 -1
  19. package/.mochi/svelte-compile/{App-68f9a9fq.server.js → App-8mpgrxza.server.js} +489 -295
  20. package/.mochi/svelte-compile/{Avatars-vyh3rwxx.server.js → Avatars-y5nf6q25.server.js} +102 -86
  21. package/.mochi/svelte-compile/{DefaultError-je52k17d.server.js → DefaultError-rrjye0s2.server.js} +1 -2
  22. package/.mochi/svelte-compile/{Instance-yfw7s5mp.server.js → Instance-3sbzyp0g.server.js} +186 -118
  23. package/.mochi/svelte-compile/{Settings-0b5t9kw9.server.js → Settings-ysvh5vhh.server.js} +739 -628
  24. package/.mochi/svelte-compile/chunk-7zaw9t5a.js +25 -0
  25. package/.mochi/svelte-compile/chunk-878xcmp4.js +65 -0
  26. package/.mochi/svelte-compile/chunk-d2z3qxxe.js +28 -0
  27. package/.mochi/svelte-compile/{chunk-60mpagr9.js → chunk-gnf5cnct.js} +661 -22
  28. package/.mochi/svelte-compile/{chunk-0y3wrae9.js → chunk-jq7jj8jd.js} +10 -6
  29. package/.mochi/svelte-compile/chunk-vr0e32mv.js +24 -0
  30. package/.mochi/svelte-compile/{chunk-apdte4nh.js → chunk-xmygdsj4.js} +35 -612
  31. package/.mochi/svelte-compile/{chunk-g0eravvj.js → chunk-zk4a9zms.js} +25 -17
  32. package/.mochi/svelte-css/DropZone-1zeitd761uwyd.css +1 -0
  33. package/.mochi/svelte-css/IdeBar-d3un7kuigxc5.css +1 -0
  34. package/.mochi/svelte-css/InstanceView-3ucotech5097w.css +1 -0
  35. package/README.md +13 -4
  36. package/migrations/0011_agent_run_rejected_structured_output.sql +2 -0
  37. package/package.json +9 -9
  38. package/src/__fixtures__/IdeBarStrip.svelte +41 -0
  39. package/src/avatars/dog.ts +1 -1
  40. package/src/avatars/lightning.ts +8 -9
  41. package/src/avatars/rocket.ts +4 -5
  42. package/src/components/AppShell.svelte +101 -1
  43. package/src/components/DropZone.svelte +77 -0
  44. package/src/components/HealthBox.svelte +6 -5
  45. package/src/components/IdeBar.svelte +222 -76
  46. package/src/components/InstanceView.svelte +75 -2
  47. package/src/components/SettingsView.svelte +87 -0
  48. package/src/components/TerminalPane.svelte +54 -0
  49. package/src/components/UiShowcase.svelte +19 -1
  50. package/src/container-injections/claude-code-install.ts +1 -1
  51. package/src/container-injections/claude-history.ts +52 -0
  52. package/src/container-injections/tmux.ts +6 -2
  53. package/src/index.ts +3 -0
  54. package/src/lib/agent-run-stream.ts +41 -1
  55. package/src/lib/agent-runs.server.ts +14 -3
  56. package/src/lib/config.server.ts +7 -1
  57. package/src/lib/db.server.ts +6 -2
  58. package/src/lib/devcontainer.server.ts +33 -2
  59. package/src/lib/injections.server.ts +5 -1
  60. package/src/lib/instances.server.ts +21 -4
  61. package/src/lib/log-capture.server.ts +46 -18
  62. package/src/lib/osc52.ts +18 -0
  63. package/src/lib/upload.ts +32 -0
  64. package/src/lib/uploads.server.ts +164 -0
  65. package/src/mcp/server.server.ts +4 -1
  66. package/src/mcp/tools.server.ts +178 -12
  67. package/src/pages/Instance.svelte +6 -2
  68. package/src/pages/Settings.svelte +3 -0
  69. package/src/routes.ts +16 -2
  70. package/src/types.ts +2 -0
  71. package/.mochi/svelte-client/HydratableIsland-smgsf0ma.js +0 -1
  72. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-9vc087ep.js +0 -2
  73. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-a48xq47a.js +0 -14
  74. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-d4zehrqz.js +0 -3
  75. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-t5175pf5.js +0 -44
  76. package/.mochi/svelte-client/chunk-0mstgbtr.js +0 -2
  77. package/.mochi/svelte-client/chunk-0q9g0kpg.js +0 -1
  78. package/.mochi/svelte-client/chunk-3fwjzw1r.js +0 -1
  79. package/.mochi/svelte-client/chunk-fn03gxer.js +0 -4
  80. package/.mochi/svelte-client/chunk-jhgq7f65.js +0 -2
  81. package/.mochi/svelte-client/chunk-p5a07kx1.js +0 -1
  82. package/.mochi/svelte-client/chunk-p5wpyhsf.js +0 -2
  83. package/.mochi/svelte-client/chunk-t2kfvtb6.js +0 -2
  84. package/.mochi/svelte-client/chunk-v3wg9mdy.js +0 -1
  85. package/.mochi/svelte-compile/chunk-2qzc07gt.js +0 -34
  86. package/.mochi/svelte-compile/chunk-ef3hvegw.js +0 -21
  87. package/.mochi/svelte-compile/chunk-f9td8ej2.js +0 -24
  88. package/.mochi/svelte-compile/chunk-jx6h1xzp.js +0 -20
  89. package/.mochi/svelte-css/IdeBar-1mtgemqfuvqzi.css +0 -1
  90. package/.mochi/svelte-css/InstanceView-32j12hyg8v9u0.css +0 -1
@@ -1,36 +1,44 @@
1
1
  // @bun
2
2
  import {
3
3
  Icon
4
- } from "./chunk-60mpagr9.js";
4
+ } from "./chunk-gnf5cnct.js";
5
5
 
6
6
  // node_modules/@lucide/svelte/dist/icons/layout-template.svelte
7
7
  import"svelte/internal/flags/async";
8
8
  import * as $ from "svelte/internal/server";
9
9
  function Layout_template($$renderer, $$props) {
10
10
  let { $$slots, $$events, ...props } = $$props;
11
- const iconNode = [
12
- [
13
- "rect",
14
- { width: "18", height: "7", x: "3", y: "3", rx: "1" }
15
- ],
16
- [
17
- "rect",
18
- { width: "9", height: "7", x: "3", y: "14", rx: "1" }
19
- ],
20
- [
21
- "rect",
22
- { width: "5", height: "7", x: "16", y: "14", rx: "1" }
11
+ const iconData = {
12
+ name: "layout-template",
13
+ size: 24,
14
+ node: [
15
+ [
16
+ "rect",
17
+ { width: "18", height: "7", x: "3", y: "3", rx: "1" }
18
+ ],
19
+ [
20
+ "rect",
21
+ { width: "9", height: "7", x: "3", y: "14", rx: "1" }
22
+ ],
23
+ [
24
+ "rect",
25
+ { width: "5", height: "7", x: "16", y: "14", rx: "1" }
26
+ ]
23
27
  ]
24
- ];
25
- Icon($$renderer, $.spread_props([{ name: "layout-template" }, props, { iconNode }]));
28
+ };
29
+ Icon($$renderer, $.spread_props([props, { icon: iconData }]));
26
30
  }
27
31
  // node_modules/@lucide/svelte/dist/icons/plus.svelte
28
32
  import"svelte/internal/flags/async";
29
33
  import * as $2 from "svelte/internal/server";
30
34
  function Plus($$renderer, $$props) {
31
35
  let { $$slots, $$events, ...props } = $$props;
32
- const iconNode = [["path", { d: "M5 12h14" }], ["path", { d: "M12 5v14" }]];
33
- Icon($$renderer, $2.spread_props([{ name: "plus" }, props, { iconNode }]));
36
+ const iconData = {
37
+ name: "plus",
38
+ size: 24,
39
+ node: [["path", { d: "M5 12h14" }], ["path", { d: "M12 5v14" }]]
40
+ };
41
+ Icon($$renderer, $2.spread_props([props, { icon: iconData }]));
34
42
  }
35
43
  // src/settings.ts
36
44
  var SOUND_KEY = "codebay.sound";
@@ -0,0 +1 @@
1
+ .dropzone.svelte-1b5cnjl{--gutter:16px;position:absolute;z-index:5;display:flex;background:color-mix(in srgb,var(--bg)80%,transparent);color:var(--ink);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;pointer-events:none;justify-content:center;align-items: center;font-size:13px;font-weight:600;inset:0}.dropzone.catches.svelte-1b5cnjl{pointer-events:auto}.frame.svelte-1b5cnjl{position:absolute;top:var(--gutter);left:var(--gutter);width:calc(100% - 2*var(--gutter));height:calc(100% - 2*var(--gutter));overflow:visible}.dots.svelte-1b5cnjl{fill:none;stroke:currentColor;stroke-width:4;stroke-dasharray:4 12;animation:svelte-1b5cnjl-segment 2s step-end infinite}.dots.odd.svelte-1b5cnjl{stroke-dashoffset:8;animation-delay:-1s}@keyframes svelte-1b5cnjl-segment{50%{opacity:0}}@media (prefers-reduced-motion:reduce){.dots.svelte-1b5cnjl{animation:none}}.label.svelte-1b5cnjl{position:relative}
@@ -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(--fill);color:var(--fill-ink)}.strip.svelte-r39acv{display:flex;align-items:stretch;min-width:0}.viewport.svelte-r39acv{display:flex;overflow-x:auto;scroll-behavior:smooth;scrollbar-width:none;min-width:0}.viewport.svelte-r39acv::-webkit-scrollbar{display:none}@media (prefers-reduced-motion:reduce){.viewport.svelte-r39acv{scroll-behavior:auto}}.tabs.svelte-r39acv{display:flex;flex:none;align-items:stretch}.page.svelte-r39acv{display:inline-flex;appearance:none;border:0;border-right:1px solid var(--rule);background:var(--bg-card);color:var(--ink);cursor:pointer;flex:none;justify-content:center;align-items: center;width:28px;margin:0;padding:0}.page.trailing.svelte-r39acv{border-right:0;border-left:1px solid var(--rule)}.page.svelte-r39acv:hover:not(:disabled){background:var(--fill);color:var(--fill-ink)}.page.svelte-r39acv:disabled{opacity:var(--dim);cursor:default}.page.svelte-r39acv:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}.tab-slot.svelte-r39acv{display:inline-flex;border-right:1px solid var(--rule);flex:none;align-items:stretch}.tab-slot.active.svelte-r39acv{background:var(--fill);color:var(--fill-ink);box-shadow:inset 0 3px 0 var(--bg)}.tab-slot.svelte-r39acv:not(.active):hover,.tab-slot.drop-target.svelte-r39acv:not(.active){background:color-mix(in srgb,var(--ink)12%,transparent)}.tab-slot.drop-target.svelte-r39acv{outline:2px dashed;outline-offset:-5px}.drop-tip.svelte-r39acv{position:fixed;z-index:20;background:var(--bg-card);color:var(--ink);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;pointer-events:none;padding:6px 10px;font-size:12px;font-weight:600;transform:translateY(6px)}.tab.svelte-r39acv{display:inline-flex;appearance:none;color:var(--ink-soft);font-family:var(--font-display);text-transform:uppercase;letter-spacing:.06em;cursor:pointer;background:0 0;border:0;flex:none;align-items: center;gap:10px;max-width:240px;margin:0;padding:0 12px;transition:background 80ms steps(2,end);font-size:13px;font-weight:700}.tab-slot.svelte-r39acv:not(.active):hover .tab:where(.svelte-r39acv){color:var(--ink)}.tab.active.svelte-r39acv{color:var(--fill-ink);cursor:default}.tab.svelte-r39acv:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}.tab.active.svelte-r39acv:focus-visible{outline-color:var(--fill-ink)}.tab-name.svelte-r39acv{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tab-stop.svelte-r39acv{display:inline-flex;appearance:none;color:inherit;opacity:.55;cursor:pointer;background:0 0;border:0;flex:none;justify-content:center;align-items: center;margin:0;padding:0 10px 0 0}.tab-slot.svelte-r39acv:not(.active) .tab-stop:where(.svelte-r39acv){color:var(--ink-soft)}.tab-stop.svelte-r39acv:hover:not(:disabled){opacity:1;color:var(--danger)}.tab-stop.svelte-r39acv:disabled{opacity:var(--dim);cursor:default}.tab-stop.svelte-r39acv:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}.tab-slot.active.svelte-r39acv .tab-stop:where(.svelte-r39acv):focus-visible{outline-color:var(--fill-ink)}.tab-mode.svelte-r39acv{display:inline-flex;opacity:.7;border:1px solid;flex:none;justify-content:center;align-items: center;width:18px;height:18px}.attn.svelte-r39acv{border:1px solid var(--edge);animation:svelte-r39acv-tab-attn-pulse 1.4s ease-in-out infinite;flex:none;width:10px;height:10px}.attn.done.svelte-r39acv{background:var(--attn-done);color:var(--attn-done)}.attn.waiting.svelte-r39acv{background:var(--attn-waiting);color:var(--attn-waiting)}@keyframes svelte-r39acv-tab-attn-pulse{0%,to{box-shadow:0 0 #0000;filter:brightness(.85)}50%{box-shadow:0 0 6px 2px color-mix(in srgb,currentColor 60%,transparent);filter:brightness(1.15)}}@media (prefers-reduced-motion:reduce){.attn.svelte-r39acv{animation:none;filter:none}}.tab.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);background:var(--bg);color:var(--ink);width:148px;margin:0;padding:3px 6px;font-size:12px}.tab-name-edit.svelte-r39acv:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}
@@ -0,0 +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(--edge);align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.open.svelte-15fxkvm:hover{background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.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(--edge);cursor:pointer;align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.restart.svelte-15fxkvm:hover:not(:disabled){background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.restart.svelte-15fxkvm:disabled{opacity:var(--dim-panel);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{position:relative;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(--fill-ink);color:var(--fill);border:1px solid var(--fill-ink);cursor:pointer;padding:4px 9px;font-size:10px;font-weight:600}.rebuild.svelte-15fxkvm:hover:not(:disabled){color:var(--fill-ink);background:0 0}.rebuild.svelte-15fxkvm:disabled{opacity:var(--dim-panel);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(--edge);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(--fill);border-color:var(--fill);color:var(--fill-ink)}.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(--fill-ink);color:var(--fill);border:1px solid var(--fill-ink);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(--fill-ink);background:0 0}.dot.svelte-15fxkvm{background:var(--live-dot);border-radius:50%;flex:none;width:6px;height:6px}.copy.svelte-15fxkvm:disabled{opacity:var(--dim-panel);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}
package/README.md CHANGED
@@ -19,7 +19,7 @@ The UI opens at `http://localhost:6969`. State (SQLite DB + per-instance workspa
19
19
  - `PORT` — server port (default `6969`)
20
20
  - `DATA_DIR` — where state lives (default `~/.codebay`)
21
21
  - `DOCKER_HOST` — Docker socket/URL (defaults to your active Docker context)
22
- - `HOST` — bind address (default `127.0.0.1`). Set `0.0.0.0` to reach codebay from other machines; your instances' forwarded app ports are then published on all interfaces too. Each container's code-server port stays loopback-only regardless — it runs without a password of its own and is reached through the Basic-Auth-gated `/p/:id/` proxy instead. Existing instances need a **Restart** to pick up the new binding.
22
+ - `HOST` — bind address (default `127.0.0.1`). Set `0.0.0.0` to reach codebay from other machines; your instances' forwarded app ports are then published on all interfaces too. Each container's code-server port stays loopback-only regardless — it runs without a password of its own and is reached through the Basic-Auth-gated `/p/:id/` proxy instead. Existing instances need a **Restart** to pick up the new binding. Note that a plain-`http` LAN address is not a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), so browsers refuse clipboard access there and copying out of an instance's VS Code terminal does nothing — reach codebay over `localhost` (e.g. `ssh -L 6969:localhost:6969 <host>`) or allow the origin under `chrome://flags/#unsafely-treat-insecure-origin-as-secure`.
23
23
  - `PUBLIC_ORIGIN` — the origin you actually load codebay from (default `http://localhost:<PORT>`). Every form POST — creating an instance, restarting one, saving settings — is checked against it, so reaching the UI at any other address (a LAN IP, a hostname, or a reverse proxy / tunnel that terminates TLS) makes those actions fail with a `403 Cross-site POST form submissions are forbidden` until you set this. Give the exact scheme + host + port your browser shows, with no trailing slash — `PUBLIC_ORIGIN=http://192.168.1.50:6969` or `PUBLIC_ORIGIN=https://codebay.example.com`. Behind a TLS-terminating proxy this is the public `https://` URL, not the loopback address the proxy forwards to.
24
24
  - `TRUSTED_ORIGINS` — comma-separated extra origins accepted alongside `PUBLIC_ORIGIN`, for when the UI is legitimately reachable at more than one address (e.g. `http://localhost:6969,http://192.168.1.50:6969`)
25
25
  - `BASIC_AUTH_PASSWORD` — enables HTTP Basic Auth over the whole UI (disabled when unset); required if you bind beyond loopback with `HOST=0.0.0.0`
@@ -45,8 +45,11 @@ it is the one place that does not use `BASIC_AUTH_PASSWORD` — MCP clients send
45
45
 
46
46
  The tools cover the whole loop: `create_sandbox`, `run_agent`, `get_run`, `list_runs`, `stop_run`,
47
47
  `get_diff`, `read_file`, `write_file`, `exec_command`, `git_push`, `create_pr`, `get_logs`,
48
- `list_sandboxes`, `get_sandbox` and `delete_sandbox`. Runs are asynchronous — `run_agent` hands back
49
- a run id and the work continues in the background, surviving a manager restart.
48
+ `list_sandboxes`, `get_sandbox`, `rename_sandbox`, `stop_sandbox`, `start_sandbox`,
49
+ `rebuild_sandbox`, `add_port_forward`, `remove_port_forward` and `delete_sandbox`. Runs are
50
+ asynchronous — `run_agent` hands back a run id and the work continues in the background, surviving a
51
+ manager restart. `stop_sandbox` / `start_sandbox` cycle the container without losing the workspace,
52
+ and `rebuild_sandbox` recreates it (which is what applies a port forward).
50
53
 
51
54
  Sandboxes created this way are ordinary instances: they show up on the dashboard with a live
52
55
  "agent running" line, and you can open the IDE to watch. They persist until an agent (or you)
@@ -56,6 +59,12 @@ deletes them.
56
59
  > credentials, with permission prompts bypassed inside the container.** Treat it like a password, and
57
60
  > regenerate it from Settings if it leaks.
58
61
 
62
+ ## Workspace uploads
63
+
64
+ Turn on Settings → **Drop files into the workspace** to drag-and-drop or paste files/images onto an
65
+ instance and save them under `codebay-inbox/` in its workspace (git-excluded), so Claude Code inside
66
+ can `Read` them. Off by default.
67
+
59
68
  ## Troubleshooting
60
69
 
61
70
  - `403 Cross-site POST form submissions are forbidden` when clicking anything that saves — codebay is being reached at an origin other than `http://localhost:<PORT>`. Set `PUBLIC_ORIGIN` to the URL in your browser's address bar and restart (see Configuration).
@@ -69,7 +78,7 @@ bun run dev # dev server, local ./.codebay DATA_DIR, no browser launch
69
78
  bun run checks # format + typecheck + tests
70
79
  ```
71
80
 
72
- The repo also ships a minimal Bun devcontainer (`.devcontainer/`) for a containerized setup. New to authoring devcontainers? See the general [Devcontainer guide](./DEVCONTAINER_GUIDE.md).
81
+ The repo also ships a Bun devcontainer (`.devcontainer/`) for a containerized setup; it runs a nested Docker daemon, so codebay itself can boot instances from inside it. New to authoring devcontainers? See the general [Devcontainer guide](./DEVCONTAINER_GUIDE.md).
73
82
 
74
83
  ## License
75
84
 
@@ -0,0 +1,2 @@
1
+ -- The last StructuredOutput payload the schema rejected, so a caller can salvage a run that exhausted its retries.
2
+ ALTER TABLE agent_runs ADD COLUMN rejected_structured_output TEXT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebay",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",
@@ -62,7 +62,7 @@
62
62
  "@devcontainers/cli": "^0.88.0",
63
63
  "@fontsource-variable/doto": "^5.3.0",
64
64
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
65
- "@lucide/svelte": "^1.31.0",
65
+ "@lucide/svelte": "^1.48.0",
66
66
  "@tmcp/adapter-valibot": "^0.1.6",
67
67
  "@tmcp/transport-http": "^0.9.0",
68
68
  "@xterm/addon-fit": "^0.11.0",
@@ -70,26 +70,26 @@
70
70
  "@zihaolam/bun-sqlite-migrations": "^1.0.3",
71
71
  "dockerode": "^5.0.1",
72
72
  "mochi-framework": "^0.9.1",
73
- "svelte": "^5.56.9",
73
+ "svelte": "^5.57.1",
74
74
  "svelte-french-toast": "^1.2.0",
75
75
  "tmcp": "^1.20.0",
76
- "valibot": "^1.1.0"
76
+ "valibot": "^1.5.0"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@eslint/js": "^10.0.1",
80
80
  "@types/bun": "1.3.14",
81
81
  "@types/dockerode": "^4.0.1",
82
- "eslint": "^10.8.1",
82
+ "eslint": "^10.11.0",
83
83
  "eslint-plugin-svelte": "^3.23.0",
84
- "globals": "^17.11.0",
85
- "prettier": "^3.9.6",
84
+ "globals": "^17.12.0",
85
+ "prettier": "^3.9.9",
86
86
  "prettier-plugin-svelte": "^4.1.1",
87
87
  "svelte-check": "4.7.4",
88
88
  "typescript": "^6.0.3",
89
- "typescript-eslint": "^8.67.0"
89
+ "typescript-eslint": "^8.70.1"
90
90
  },
91
91
  "resolutions": {
92
92
  "msgpackr-extract": "npm:@mochi-framework/msgpackr-extract-stub@*",
93
- "svelte": "5.56.9"
93
+ "svelte": "5.57.1"
94
94
  }
95
95
  }
@@ -0,0 +1,41 @@
1
+ <!-- Renders the IDE tab strip with a stop button per tab for src/index.isolated.test.ts. -->
2
+ <script lang="ts">
3
+ import IdeBar from '../components/IdeBar.svelte';
4
+ import type { Instance } from '../types.ts';
5
+
6
+ const tab = (id: string): Instance => ({
7
+ id,
8
+ name: id,
9
+ source_path: `/src/${id}`,
10
+ workspace_path: `/data/${id}`,
11
+ host_port: 8001,
12
+ container_id: null,
13
+ remote_workspace_folder: null,
14
+ status: 'running',
15
+ error: null,
16
+ created_at: 0,
17
+ image_source: null,
18
+ avatar: null,
19
+ mode: 'ide',
20
+ terminal_split: 0,
21
+ git_branch: null,
22
+ attention: null,
23
+ forwarded_ports: [],
24
+ active_run: null
25
+ });
26
+
27
+ let editingName = $state('');
28
+ </script>
29
+
30
+ <IdeBar
31
+ running={[tab('alpha'), tab('beta')]}
32
+ active="beta"
33
+ attention={{}}
34
+ editingId={null}
35
+ bind:editingName
36
+ onselect={() => {}}
37
+ onstop={() => {}}
38
+ onstartrename={() => {}}
39
+ oncommitrename={() => {}}
40
+ oncancelrename={() => {}}
41
+ />
@@ -6,7 +6,7 @@ const art: AvatarArt = {
6
6
  '..######',
7
7
  '.#######',
8
8
  '####.###',
9
- '##.###.#',
9
+ '##.###..',
10
10
  '#######.',
11
11
  '##.##..#',
12
12
  '##.##..#',
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'lightning',
5
- robot: true,
6
5
  pixels: [
7
- '...#####',
8
- '..####..',
9
- '.####...',
10
- '.#######',
11
- '..######',
12
- '...#####',
13
- '..####..',
14
- '.###....'
6
+ '....###.',
7
+ '...###..',
8
+ '..###...',
9
+ '.###....',
10
+ '#######.',
11
+ '...###..',
12
+ '..##....',
13
+ '.#......'
15
14
  ]
16
15
  };
17
16
 
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
2
2
 
3
3
  const art: AvatarArt = {
4
4
  name: 'rocket',
5
- robot: true,
6
5
  pixels: [
7
6
  '...##...',
8
7
  '..####..',
9
- '..#..#..',
8
+ '..#+.#..',
9
+ '..#+.#..',
10
10
  '..####..',
11
11
  '.######.',
12
- '##.##.##',
13
- '..#..#..',
14
- '...##...'
12
+ '.#.++.#.',
13
+ '..+.++..'
15
14
  ]
16
15
  };
17
16
 
@@ -5,6 +5,7 @@
5
5
  import { SvelteSet } from 'svelte/reactivity';
6
6
  import { nextTabIndex } from '../lib/tab-nav.ts';
7
7
  import DashboardView from './DashboardView.svelte';
8
+ import DropZone from './DropZone.svelte';
8
9
  import IdeBar from './IdeBar.svelte';
9
10
  import IdeLoader from './IdeLoader.svelte';
10
11
  import TerminalSplit from './TerminalSplit.svelte';
@@ -15,6 +16,7 @@
15
16
  import { syncTheme } from '../theme.ts';
16
17
  import toast, { Toaster } from 'svelte-french-toast';
17
18
  import { TOAST_OPTIONS } from '../toast.ts';
19
+ import { filesFrom, uploadFile } from '../lib/upload.ts';
18
20
 
19
21
  // `snapshot` seeds the live state so neither view renders a loading flash first.
20
22
  let {
@@ -56,6 +58,22 @@
56
58
  let editingId = $state<string | null>(null);
57
59
  let editingName = $state('');
58
60
 
61
+ // The tab only vanishes once the stream drops the instance from `running`, so the
62
+ // button stays disabled across the round-trip rather than flickering back to armed.
63
+ const stopping = new SvelteSet<string>();
64
+
65
+ async function stopTab(id: string) {
66
+ if (stopping.has(id)) return;
67
+ stopping.add(id);
68
+ try {
69
+ await apiPost(`/api/instances/${id}/stop`, undefined, 'Failed to stop instance');
70
+ // The live stream removes the tab once the container is down.
71
+ } catch (err) {
72
+ toast.error((err as Error).message);
73
+ stopping.delete(id);
74
+ }
75
+ }
76
+
59
77
  function startRename(instance: Instance) {
60
78
  editingId = instance.id;
61
79
  editingName = instance.name;
@@ -250,6 +268,72 @@
250
268
  };
251
269
  });
252
270
 
271
+ // Sequential, so the response order (and the toast order) matches the drop order.
272
+ async function handleUpload(id: string, files: File[]) {
273
+ for (const file of files) {
274
+ try {
275
+ const saved = await uploadFile(id, file);
276
+ toast.success(`Saved ${saved.containerPath}`);
277
+ void navigator.clipboard.writeText(saved.containerPath).catch(() => {});
278
+ } catch (err) {
279
+ toast.error((err as Error).message);
280
+ }
281
+ }
282
+ }
283
+
284
+ let dropping = $state(false);
285
+
286
+ function onDrop(e: DragEvent) {
287
+ e.preventDefault();
288
+ dropping = false;
289
+ void handleUpload(active, filesFrom(e.dataTransfer));
290
+ }
291
+
292
+ // Covering the code-server iframe hijacks VS Code's own drops, so there only the tab takes a
293
+ // file; a terminal pane lives in this document and gets the full overlay.
294
+ $effect(() => {
295
+ if (!livePreflight.uploadEnabled || !onIde) return;
296
+ const hasFiles = (e: DragEvent) => e.dataTransfer?.types.includes('Files') ?? false;
297
+ const enter = (e: DragEvent) => {
298
+ if (hasFiles(e)) dropping = true;
299
+ };
300
+ // A drop target already claimed the drag; anywhere else refuses it, since the browser's
301
+ // default is to open the file in a new tab.
302
+ const over = (e: DragEvent) => {
303
+ if (!hasFiles(e) || e.defaultPrevented) return;
304
+ e.preventDefault();
305
+ e.dataTransfer!.dropEffect = 'none';
306
+ };
307
+ const drop = (e: DragEvent) => {
308
+ if (hasFiles(e)) e.preventDefault();
309
+ dropping = false;
310
+ };
311
+ const leave = (e: DragEvent) => {
312
+ if (e.relatedTarget === null) dropping = false;
313
+ };
314
+ // Capture phase: only fires for events that originate in the parent document (xterm's
315
+ // textarea, the tab bar) — paste inside the iframe never reaches here.
316
+ const paste = (e: ClipboardEvent) => {
317
+ const files = filesFrom(e.clipboardData);
318
+ if (files.length && active) {
319
+ e.preventDefault();
320
+ void handleUpload(active, files);
321
+ }
322
+ };
323
+ window.addEventListener('dragenter', enter);
324
+ window.addEventListener('dragover', over);
325
+ window.addEventListener('dragleave', leave);
326
+ window.addEventListener('drop', drop);
327
+ window.addEventListener('paste', paste, true);
328
+ return () => {
329
+ window.removeEventListener('dragenter', enter);
330
+ window.removeEventListener('dragover', over);
331
+ window.removeEventListener('dragleave', leave);
332
+ window.removeEventListener('drop', drop);
333
+ window.removeEventListener('paste', paste, true);
334
+ };
335
+ });
336
+
253
337
  $effect(() => {
254
338
  // The re-seed after a reconnect is a baseline, not a change — otherwise it replays chimes.
255
339
  let primed = false;
@@ -271,6 +355,10 @@
271
355
  livePreflight = { ...livePreflight, defaultMode: msg.data.mode };
272
356
  return;
273
357
  }
358
+ if (msg.type === 'upload-enabled') {
359
+ livePreflight = { ...livePreflight, uploadEnabled: msg.data.enabled };
360
+ return;
361
+ }
274
362
  if (msg.type === 'theme') {
275
363
  syncTheme(msg.data.value);
276
364
  return;
@@ -301,6 +389,7 @@
301
389
  for (const id of [...everReady]) if (!live.has(id)) everReady.delete(id);
302
390
  for (const id of [...forced]) if (!live.has(id)) forced.delete(id);
303
391
  for (const id of [...loadedFrames]) if (!live.has(id)) loadedFrames.delete(id);
392
+ for (const id of [...stopping]) if (!live.has(id)) stopping.delete(id);
304
393
  const nextAttention: Record<string, 'done' | 'waiting' | null> = {};
305
394
  for (const inst of next) nextAttention[inst.id] = inst.attention;
306
395
  if (primed) {
@@ -356,10 +445,15 @@
356
445
  {attention}
357
446
  {editingId}
358
447
  bind:editingName
448
+ stopping={[...stopping]}
359
449
  onreload={activeInstance && activeInstance.mode !== 'terminal' && mountable(active)
360
450
  ? reloadActive
361
451
  : undefined}
362
452
  onselect={(id) => navigate(`/ide/${id}`)}
453
+ onstop={stopTab}
454
+ ondropfiles={livePreflight.uploadEnabled
455
+ ? (id, files) => void handleUpload(id, files)
456
+ : undefined}
363
457
  onstartrename={startRename}
364
458
  oncommitrename={commitRename}
365
459
  oncancelrename={cancelRename}
@@ -382,7 +476,7 @@
382
476
  <div
383
477
  class="pane"
384
478
  class:active={inst.id === active}
385
- role="tabpanel"
479
+ role="region"
386
480
  aria-labelledby="tab-{inst.id}"
387
481
  >
388
482
  {#if mountable(inst.id)}
@@ -393,10 +487,13 @@
393
487
  initialOpen={inst.terminal_split === 1}
394
488
  />
395
489
  {:else}
490
+ <!-- code-server answers a terminal's OSC 52 through the Clipboard API, which
491
+ Permissions Policy gates per-frame. -->
396
492
  <iframe
397
493
  bind:this={frames[inst.id]}
398
494
  src={ideUrl(inst)}
399
495
  title={inst.name}
496
+ allow="clipboard-read; clipboard-write"
400
497
  onload={() => loadedFrames.add(inst.id)}
401
498
  ></iframe>
402
499
  {/if}
@@ -413,6 +510,9 @@
413
510
  {:else if inst.mode !== 'terminal' && !loadedFrames.has(inst.id)}
414
511
  <IdeLoader />
415
512
  {/if}
513
+ {#if dropping && inst.id === active && inst.mode === 'terminal'}
514
+ <DropZone ondrop={onDrop} />
515
+ {/if}
416
516
  </div>
417
517
  {/if}
418
518
  {/each}
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ // With a drop handler the overlay takes the drop itself (an iframe beneath would swallow it);
3
+ // without one it stays inert and the drop lands on whatever is underneath.
4
+ let { ondrop }: { ondrop?: (e: DragEvent) => void } = $props();
5
+ </script>
6
+
7
+ <div
8
+ class="dropzone"
9
+ class:catches={ondrop}
10
+ role="presentation"
11
+ ondragover={ondrop ? (e) => e.preventDefault() : undefined}
12
+ {ondrop}
13
+ >
14
+ <!-- Two interleaved dot patterns taking turns, like alternating LCD segments. -->
15
+ <svg class="frame" aria-hidden="true">
16
+ <rect class="dots" width="100%" height="100%" />
17
+ <rect class="dots odd" width="100%" height="100%" />
18
+ </svg>
19
+ <span class="label">Drop to save into codebay-inbox/</span>
20
+ </div>
21
+
22
+ <style>
23
+ .dropzone {
24
+ --gutter: 16px;
25
+ position: absolute;
26
+ inset: 0;
27
+ z-index: 5;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background: color-mix(in srgb, var(--bg) 80%, transparent);
32
+ color: var(--ink);
33
+ font-family: var(--font-mono);
34
+ font-weight: 600;
35
+ text-transform: uppercase;
36
+ letter-spacing: 0.06em;
37
+ font-size: 13px;
38
+ pointer-events: none;
39
+ }
40
+ .dropzone.catches {
41
+ pointer-events: auto;
42
+ }
43
+ /* An inline svg sizes like a replaced element, so insets alone won't stretch it. */
44
+ .frame {
45
+ position: absolute;
46
+ top: var(--gutter);
47
+ left: var(--gutter);
48
+ width: calc(100% - 2 * var(--gutter));
49
+ height: calc(100% - 2 * var(--gutter));
50
+ overflow: visible;
51
+ }
52
+ .dots {
53
+ fill: none;
54
+ stroke: currentColor;
55
+ stroke-width: 4;
56
+ stroke-dasharray: 4 12;
57
+ animation: segment 2s step-end infinite;
58
+ }
59
+ .dots.odd {
60
+ stroke-dashoffset: 8;
61
+ animation-delay: -1s;
62
+ }
63
+ @keyframes segment {
64
+ 50% {
65
+ opacity: 0;
66
+ }
67
+ }
68
+ @media (prefers-reduced-motion: reduce) {
69
+ .dots {
70
+ animation: none;
71
+ }
72
+ }
73
+ /* Positioned so it paints above the frame, which comes first in source order. */
74
+ .label {
75
+ position: relative;
76
+ }
77
+ </style>
@@ -31,13 +31,14 @@
31
31
  ] as const);
32
32
 
33
33
  // The value word stays uniform because the tick/cross icon already conveys pass vs fail.
34
- const checks = $derived.by((): { label: string; ok: boolean; value: string }[] => {
34
+ // Keyed by id, not label: a repeated label would make the keyed each throw and freeze the panel.
35
+ const checks = $derived.by((): { key: string; label: string; ok: boolean; value: string }[] => {
35
36
  if (!health) return [];
36
37
  const v = (ok: boolean) => ({ ok, value: ok ? 'OK' : '—' });
37
38
  return [
38
- { label: FIXED_CHECKS[0], ...v(health.containerRunning) },
39
- { label: FIXED_CHECKS[1], ...v(health.codeServerAccessible) },
40
- ...health.injections.map((i) => ({ label: i.label, ...v(i.ok) }))
39
+ { key: 'container', label: FIXED_CHECKS[0], ...v(health.containerRunning) },
40
+ { key: 'surface', label: FIXED_CHECKS[1], ...v(health.codeServerAccessible) },
41
+ ...health.injections.map((i) => ({ key: i.id, label: i.label, ...v(i.ok) }))
41
42
  ];
42
43
  });
43
44
 
@@ -64,7 +65,7 @@
64
65
  <div class="panel-bar">Health</div>
65
66
  <div class="health">
66
67
  {#if health}
67
- {#each checks as check (check.label)}
68
+ {#each checks as check (check.key)}
68
69
  <div class="hrow">
69
70
  <span class="box {check.ok ? 'ok' : 'bad'}">
70
71
  {#if check.ok}<Check size={12} strokeWidth={3} />{:else}<X