codebay 0.14.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.
- package/.mochi/manifest.json +778 -646
- package/.mochi/svelte-client/HydratableIsland-6y9p903x.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-nv6yzk0x.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-ry83ck8a.js +14 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-vjt4jscf.js +3 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-1jdrk8x6.js +48 -0
- package/.mochi/svelte-client/{chunk-spk1b2r4.js → chunk-0vqvn81s.js} +1 -1
- package/.mochi/svelte-client/chunk-2dc6352a.js +1 -0
- package/.mochi/svelte-client/chunk-3cxaw5hr.js +2 -0
- package/.mochi/svelte-client/chunk-3jqc40sp.js +2 -0
- package/.mochi/svelte-client/chunk-3pgw6t5j.js +4 -0
- package/.mochi/svelte-client/chunk-7g3xt6ez.js +2 -0
- package/.mochi/svelte-client/chunk-b5s3mpg0.js +1 -0
- package/.mochi/svelte-client/chunk-bhseezf3.js +2 -0
- package/.mochi/svelte-client/chunk-bhwkq8er.js +1 -0
- package/.mochi/svelte-client/chunk-jm3ge348.js +1 -0
- package/.mochi/svelte-client/{chunk-0mc09cyb.js → chunk-sxcr9gzp.js} +1 -1
- package/.mochi/svelte-client/{chunk-ccfh8hxz.js → chunk-tss2473w.js} +1 -1
- package/.mochi/svelte-compile/{App-94xsdn0w.server.js → App-8mpgrxza.server.js} +470 -271
- package/.mochi/svelte-compile/{Avatars-fnccw47h.server.js → Avatars-y5nf6q25.server.js} +111 -110
- package/.mochi/svelte-compile/{DefaultError-je52k17d.server.js → DefaultError-rrjye0s2.server.js} +1 -2
- package/.mochi/svelte-compile/Instance-3sbzyp0g.server.js +695 -0
- package/.mochi/svelte-compile/{Settings-a7b11v90.server.js → Settings-ysvh5vhh.server.js} +838 -607
- package/.mochi/svelte-compile/chunk-7zaw9t5a.js +25 -0
- package/.mochi/svelte-compile/chunk-878xcmp4.js +65 -0
- package/.mochi/svelte-compile/chunk-d2z3qxxe.js +28 -0
- package/.mochi/svelte-compile/{chunk-60mpagr9.js → chunk-gnf5cnct.js} +661 -22
- package/.mochi/svelte-compile/{chunk-w0hca7va.js → chunk-jq7jj8jd.js} +18 -2
- package/.mochi/svelte-compile/chunk-vr0e32mv.js +24 -0
- package/.mochi/svelte-compile/{chunk-apdte4nh.js → chunk-xmygdsj4.js} +35 -612
- package/.mochi/svelte-compile/{chunk-pqb403fh.js → chunk-zk4a9zms.js} +28 -31
- package/.mochi/svelte-css/AgentLogBox-38adby5iygny7.css +1 -0
- package/.mochi/svelte-css/DropZone-1zeitd761uwyd.css +1 -0
- package/.mochi/svelte-css/IdeBar-d3un7kuigxc5.css +1 -0
- package/.mochi/svelte-css/{InstanceCard-25z7nas7y98zk.css → InstanceCard-3vxozrp9uoads.css} +1 -1
- package/.mochi/svelte-css/InstanceView-3ucotech5097w.css +1 -0
- package/.mochi/svelte-css/{SettingsView-bccw34agd2bj.css → SettingsView-2400myxb9ot56.css} +1 -1
- package/README.md +43 -3
- package/migrations/0009_agent_runs.sql +24 -0
- package/migrations/0010_agent_run_model.sql +2 -0
- package/migrations/0011_agent_run_rejected_structured_output.sql +2 -0
- package/package.json +13 -9
- package/src/__fixtures__/IdeBarStrip.svelte +41 -0
- package/src/avatars/dog.ts +1 -1
- package/src/avatars/lightning.ts +8 -9
- package/src/avatars/rocket.ts +4 -5
- package/src/components/AgentLogBox.svelte +458 -0
- package/src/components/AppShell.svelte +112 -1
- package/src/components/DropZone.svelte +77 -0
- package/src/components/FolderBrowser.svelte +13 -5
- package/src/components/HealthBox.svelte +6 -5
- package/src/components/IdeBar.svelte +222 -76
- package/src/components/InstanceCard.svelte +38 -0
- package/src/components/InstanceView.svelte +83 -2
- package/src/components/SettingsView.svelte +273 -0
- package/src/components/TerminalPane.svelte +54 -0
- package/src/components/UiShowcase.svelte +23 -3
- package/src/container-injections/claude-code-install.ts +1 -1
- package/src/container-injections/claude-history.ts +52 -0
- package/src/container-injections/claude-statusline.ts +4 -1
- package/src/container-injections/github-credentials.ts +12 -1
- package/src/container-injections/tmux.ts +6 -2
- package/src/index.ts +35 -7
- package/src/lib/agent-run-stream.ts +286 -0
- package/src/lib/agent-runs.server.ts +735 -0
- package/src/lib/auth.server.ts +11 -2
- package/src/lib/build-freshness.server.ts +71 -0
- package/src/lib/config.server.ts +36 -7
- package/src/lib/container-files.server.ts +1 -1
- package/src/lib/data-dir-lock.server.ts +78 -0
- package/src/lib/db.server.ts +124 -1
- package/src/lib/devcontainer.server.ts +93 -16
- package/src/lib/docker-client.server.ts +11 -5
- package/src/lib/drives.ts +11 -0
- package/src/lib/exec.server.ts +66 -5
- package/src/lib/injections.server.ts +5 -1
- package/src/lib/instances.server.ts +66 -7
- package/src/lib/log-capture.server.ts +63 -23
- package/src/lib/mcp-auth.server.ts +52 -0
- package/src/lib/osc52.ts +18 -0
- package/src/lib/picker.server.ts +23 -2
- package/src/lib/sandbox-ops.server.ts +263 -0
- package/src/lib/upload.ts +32 -0
- package/src/lib/uploads.server.ts +164 -0
- package/src/mcp/routes.server.ts +25 -0
- package/src/mcp/server.server.ts +55 -0
- package/src/mcp/tools.server.ts +660 -0
- package/src/pages/Instance.svelte +6 -2
- package/src/pages/Settings.svelte +15 -0
- package/src/routes.ts +79 -3
- package/src/types.ts +17 -0
- package/.mochi/svelte-client/HydratableIsland-zrdsrsmg.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-v7y7dh53.js +0 -2
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-tmwd6fpb.js +0 -14
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-m7t64dbh.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-4a7ann1f.js +0 -40
- package/.mochi/svelte-client/chunk-5jwfs4p9.js +0 -2
- package/.mochi/svelte-client/chunk-6qg4q98a.js +0 -2
- package/.mochi/svelte-client/chunk-phf7v5jj.js +0 -4
- package/.mochi/svelte-client/chunk-tmeh70wg.js +0 -2
- package/.mochi/svelte-client/chunk-vs1yjbqt.js +0 -2
- package/.mochi/svelte-client/chunk-x0m3k0rn.js +0 -1
- package/.mochi/svelte-client/chunk-y7f4jbdm.js +0 -2
- package/.mochi/svelte-compile/Instance-knnkcn2p.server.js +0 -377
- package/.mochi/svelte-compile/chunk-ed46m58e.js +0 -22
- package/.mochi/svelte-compile/chunk-jx6h1xzp.js +0 -20
- package/.mochi/svelte-css/IdeBar-1mtgemqfuvqzi.css +0 -1
- package/.mochi/svelte-css/InstanceView-32j12hyg8v9u0.css +0 -1
|
@@ -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>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { tick } from 'svelte';
|
|
3
3
|
import type { BrowseResult, FolderHistoryEntry, InstanceMode } from '../types.ts';
|
|
4
4
|
import { isRepoUrl } from '../lib/repo-url.ts';
|
|
5
|
+
import { DRIVES_ROOT, DRIVES_ROOT_LABEL } from '../lib/drives.ts';
|
|
5
6
|
import Terminal from '@lucide/svelte/icons/terminal';
|
|
6
7
|
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
|
|
7
8
|
import X from '@lucide/svelte/icons/x';
|
|
@@ -54,6 +55,9 @@
|
|
|
54
55
|
: []
|
|
55
56
|
);
|
|
56
57
|
|
|
58
|
+
// The virtual drive list is a chooser, not a folder — nothing in it can be handed to an instance.
|
|
59
|
+
const atDrivesRoot = $derived(result?.path === DRIVES_ROOT);
|
|
60
|
+
|
|
57
61
|
const shownHistory = $derived(showAll ? history : history.slice(0, 5));
|
|
58
62
|
|
|
59
63
|
async function loadHistory() {
|
|
@@ -180,7 +184,7 @@
|
|
|
180
184
|
</div>
|
|
181
185
|
|
|
182
186
|
<div class="crumbs">
|
|
183
|
-
<code>{result?.path ?? '…'}</code>
|
|
187
|
+
<code>{atDrivesRoot ? DRIVES_ROOT_LABEL : (result?.path ?? '…')}</code>
|
|
184
188
|
</div>
|
|
185
189
|
|
|
186
190
|
{#if history.length > 0}
|
|
@@ -268,7 +272,7 @@
|
|
|
268
272
|
{/if}
|
|
269
273
|
</div>
|
|
270
274
|
|
|
271
|
-
{#if result && !result.hasDevcontainer}
|
|
275
|
+
{#if result && !result.hasDevcontainer && !atDrivesRoot}
|
|
272
276
|
<div class="warn">
|
|
273
277
|
<TriangleAlert size={15} />
|
|
274
278
|
<span
|
|
@@ -279,11 +283,15 @@
|
|
|
279
283
|
{/if}
|
|
280
284
|
|
|
281
285
|
<div class="foot">
|
|
282
|
-
<span class="hint"
|
|
286
|
+
<span class="hint"
|
|
287
|
+
>{atDrivesRoot
|
|
288
|
+
? 'Pick a drive to browse.'
|
|
289
|
+
: 'Browse to a folder, then select this folder or any subfolder.'}</span
|
|
290
|
+
>
|
|
283
291
|
<button
|
|
284
292
|
class="primary"
|
|
285
|
-
disabled={!result}
|
|
286
|
-
onclick={() => result && onpick(result.path, { mode })}
|
|
293
|
+
disabled={!result || atDrivesRoot}
|
|
294
|
+
onclick={() => result && !atDrivesRoot && onpick(result.path, { mode })}
|
|
287
295
|
>
|
|
288
296
|
Select this folder
|
|
289
297
|
</button>
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
|
|
8
8
|
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
|
9
9
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
10
|
+
import CircleStop from '@lucide/svelte/icons/circle-stop';
|
|
10
11
|
import Avatar from './Avatar.svelte';
|
|
11
12
|
import AppBar from './AppBar.svelte';
|
|
12
13
|
import { withPopupMarker } from '../lib/popup-nav.ts';
|
|
13
14
|
import { nextTabIndex } from '../lib/tab-nav.ts';
|
|
15
|
+
import { filesFrom } from '../lib/upload.ts';
|
|
14
16
|
|
|
15
17
|
let {
|
|
16
18
|
running,
|
|
@@ -18,8 +20,11 @@
|
|
|
18
20
|
attention,
|
|
19
21
|
editingId,
|
|
20
22
|
editingName = $bindable(),
|
|
23
|
+
stopping = [],
|
|
21
24
|
onreload,
|
|
22
25
|
onselect,
|
|
26
|
+
onstop,
|
|
27
|
+
ondropfiles,
|
|
23
28
|
onstartrename,
|
|
24
29
|
oncommitrename,
|
|
25
30
|
oncancelrename
|
|
@@ -29,21 +34,59 @@
|
|
|
29
34
|
attention: Record<string, 'done' | 'waiting' | null>;
|
|
30
35
|
editingId: string | null;
|
|
31
36
|
editingName: string;
|
|
37
|
+
/** Ids with a stop already in flight — the tab survives until the stream drops it from `running`. */
|
|
38
|
+
stopping?: readonly string[];
|
|
32
39
|
/** Absent for terminal tabs, which carry their own reload, and until the iframe exists. */
|
|
33
40
|
onreload?: () => void;
|
|
34
41
|
onselect: (id: string) => void;
|
|
42
|
+
/** Absent on /debug, where there is no instance to stop. */
|
|
43
|
+
onstop?: (id: string) => void;
|
|
44
|
+
/** Absent while workspace uploads are switched off, so tabs stay plain. */
|
|
45
|
+
ondropfiles?: (id: string, files: File[]) => void;
|
|
35
46
|
onstartrename: (instance: Instance) => void;
|
|
36
47
|
oncommitrename: (id: string) => void;
|
|
37
48
|
oncancelrename: () => void;
|
|
38
49
|
} = $props();
|
|
39
50
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
51
|
+
const isStopping = (id: string) => stopping.includes(id);
|
|
52
|
+
|
|
53
|
+
// The tooltip is fixed-positioned from the slot's rect, since the scrolling strip clips anything
|
|
54
|
+
// hanging below the bar.
|
|
55
|
+
let dropTarget = $state<{ id: string; x: number; y: number; maxX: number } | null>(null);
|
|
56
|
+
let tipWidth = $state(0);
|
|
57
|
+
// Centred under the tab but kept on screen, since the first and last tabs sit near the edges.
|
|
58
|
+
const tipLeft = $derived(
|
|
59
|
+
dropTarget ? Math.max(8, Math.min(dropTarget.x - tipWidth / 2, dropTarget.maxX - tipWidth)) : 0
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
function onSlotDrag(e: DragEvent, id: string) {
|
|
63
|
+
if (!ondropfiles || !e.dataTransfer?.types.includes('Files')) return;
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
66
|
+
if (dropTarget?.id === id) return;
|
|
67
|
+
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
68
|
+
dropTarget = {
|
|
69
|
+
id,
|
|
70
|
+
x: r.left + r.width / 2,
|
|
71
|
+
y: r.bottom,
|
|
72
|
+
maxX: document.documentElement.clientWidth - 8
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onSlotLeave(e: DragEvent) {
|
|
77
|
+
if ((e.currentTarget as HTMLElement).contains(e.relatedTarget as Node | null)) return;
|
|
78
|
+
dropTarget = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onSlotDrop(e: DragEvent, id: string) {
|
|
82
|
+
if (!ondropfiles) return;
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
dropTarget = null;
|
|
85
|
+
ondropfiles(id, filesFrom(e.dataTransfer));
|
|
86
|
+
}
|
|
44
87
|
|
|
45
88
|
let viewport = $state<HTMLDivElement | null>(null);
|
|
46
|
-
let content = $state<
|
|
89
|
+
let content = $state<HTMLElement | null>(null);
|
|
47
90
|
let atStart = $state(true);
|
|
48
91
|
let atEnd = $state(true);
|
|
49
92
|
// `$state` rather than a plain object so `bind:this` into it is a tracked write —
|
|
@@ -67,8 +110,9 @@
|
|
|
67
110
|
tabEls[inst.id]?.focus();
|
|
68
111
|
}
|
|
69
112
|
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
// Relative to the focused tab, not the active one: every tab is in the Tab order now.
|
|
114
|
+
function onTabKeydown(e: KeyboardEvent, index: number) {
|
|
115
|
+
const next = nextTabIndex(index, e.key, running.length);
|
|
72
116
|
if (next === null) return;
|
|
73
117
|
e.preventDefault();
|
|
74
118
|
select(next);
|
|
@@ -115,74 +159,99 @@
|
|
|
115
159
|
>
|
|
116
160
|
{/if}
|
|
117
161
|
<div class="viewport" bind:this={viewport}>
|
|
118
|
-
|
|
162
|
+
<!-- A nav of buttons rather than a tablist: a tablist may only own tabs, so the
|
|
163
|
+
stop buttons beside them fail ARIA, and each tab switches the route anyway. -->
|
|
164
|
+
<nav class="tabs" aria-label="Instances" bind:this={content}>
|
|
119
165
|
{#each running as inst, i (inst.id)}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
? 'Terminal-only instance'
|
|
165
|
-
: 'Full IDE instance'}
|
|
166
|
+
<!-- The slot, not the tab button, carries the chrome: a stop button nested inside
|
|
167
|
+
a <button> would be invalid markup. -->
|
|
168
|
+
<div
|
|
169
|
+
class="tab-slot"
|
|
170
|
+
class:active={inst.id === active}
|
|
171
|
+
class:drop-target={dropTarget?.id === inst.id}
|
|
172
|
+
role="presentation"
|
|
173
|
+
ondragenter={(e) => onSlotDrag(e, inst.id)}
|
|
174
|
+
ondragover={(e) => onSlotDrag(e, inst.id)}
|
|
175
|
+
ondragleave={onSlotLeave}
|
|
176
|
+
ondrop={(e) => onSlotDrop(e, inst.id)}
|
|
177
|
+
>
|
|
178
|
+
{#if editingId === inst.id}
|
|
179
|
+
<div class="tab editing">
|
|
180
|
+
<Avatar
|
|
181
|
+
name={inst.name}
|
|
182
|
+
art={findAvatar(inst.avatar ?? undefined) ?? null}
|
|
183
|
+
scale={3}
|
|
184
|
+
/>
|
|
185
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
186
|
+
<input
|
|
187
|
+
class="tab-name-edit"
|
|
188
|
+
bind:value={editingName}
|
|
189
|
+
autofocus
|
|
190
|
+
onfocus={(e) => (e.currentTarget as HTMLInputElement).select()}
|
|
191
|
+
onblur={() => oncommitrename(inst.id)}
|
|
192
|
+
onkeydown={(e) => {
|
|
193
|
+
if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
|
|
194
|
+
else if (e.key === 'Escape') oncancelrename();
|
|
195
|
+
}}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
{:else}
|
|
199
|
+
<button
|
|
200
|
+
bind:this={tabEls[inst.id]}
|
|
201
|
+
type="button"
|
|
202
|
+
class="tab"
|
|
203
|
+
class:active={inst.id === active}
|
|
204
|
+
id="tab-{inst.id}"
|
|
205
|
+
aria-current={inst.id === active ? 'page' : undefined}
|
|
206
|
+
onclick={() => onselect(inst.id)}
|
|
207
|
+
ondblclick={() => onstartrename(inst)}
|
|
208
|
+
onkeydown={(e) => onTabKeydown(e, i)}
|
|
209
|
+
title={inst.name}
|
|
166
210
|
>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<!-- The focused tab never lights up, so a lit LED always means "needs your eyes". -->
|
|
174
|
-
{#if inst.id !== active && attention[inst.id]}
|
|
211
|
+
<Avatar
|
|
212
|
+
name={inst.name}
|
|
213
|
+
art={findAvatar(inst.avatar ?? undefined) ?? null}
|
|
214
|
+
scale={3}
|
|
215
|
+
/>
|
|
216
|
+
<span class="tab-name">{inst.name}</span>
|
|
175
217
|
<span
|
|
176
|
-
class="
|
|
177
|
-
aria-label={
|
|
178
|
-
? '
|
|
179
|
-
: '
|
|
180
|
-
|
|
218
|
+
class="tab-mode"
|
|
219
|
+
aria-label={inst.mode === 'terminal'
|
|
220
|
+
? 'Terminal-only instance'
|
|
221
|
+
: 'Full IDE instance'}
|
|
222
|
+
>
|
|
223
|
+
{#if inst.mode === 'terminal'}
|
|
224
|
+
<Terminal size={11} />
|
|
225
|
+
{:else}
|
|
226
|
+
<LayoutTemplate size={11} />
|
|
227
|
+
{/if}
|
|
228
|
+
</span>
|
|
229
|
+
<!-- The focused tab never lights up, so a lit LED always means "needs your eyes". -->
|
|
230
|
+
{#if inst.id !== active && attention[inst.id]}
|
|
231
|
+
<span
|
|
232
|
+
class="attn {attention[inst.id]}"
|
|
233
|
+
aria-label={attention[inst.id] === 'waiting'
|
|
234
|
+
? 'Claude is waiting for input'
|
|
235
|
+
: 'Claude finished'}
|
|
236
|
+
></span>
|
|
237
|
+
{/if}
|
|
238
|
+
</button>
|
|
239
|
+
{#if onstop}
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
class="tab-stop"
|
|
243
|
+
disabled={isStopping(inst.id)}
|
|
244
|
+
onclick={() => onstop(inst.id)}
|
|
245
|
+
title={isStopping(inst.id) ? 'Stopping…' : `Stop ${inst.name}`}
|
|
246
|
+
aria-label={isStopping(inst.id) ? 'Stopping…' : `Stop ${inst.name}`}
|
|
247
|
+
>
|
|
248
|
+
<CircleStop size={20} />
|
|
249
|
+
</button>
|
|
181
250
|
{/if}
|
|
182
|
-
|
|
183
|
-
|
|
251
|
+
{/if}
|
|
252
|
+
</div>
|
|
184
253
|
{/each}
|
|
185
|
-
</
|
|
254
|
+
</nav>
|
|
186
255
|
</div>
|
|
187
256
|
{#if overflowing}
|
|
188
257
|
<button
|
|
@@ -215,6 +284,16 @@
|
|
|
215
284
|
>
|
|
216
285
|
</div>
|
|
217
286
|
</AppBar>
|
|
287
|
+
{#if dropTarget}
|
|
288
|
+
<div
|
|
289
|
+
class="drop-tip panel"
|
|
290
|
+
bind:offsetWidth={tipWidth}
|
|
291
|
+
style:left="{tipLeft}px"
|
|
292
|
+
style:top="{dropTarget.y}px"
|
|
293
|
+
>
|
|
294
|
+
Drop to save into codebay-inbox/
|
|
295
|
+
</div>
|
|
296
|
+
{/if}
|
|
218
297
|
|
|
219
298
|
<style>
|
|
220
299
|
/* Grouped so the auto-margin doesn't have to move when the reload button comes and goes. */
|
|
@@ -307,6 +386,44 @@
|
|
|
307
386
|
outline-offset: -2px;
|
|
308
387
|
}
|
|
309
388
|
|
|
389
|
+
/* The slot owns the separator and the active plate so the stop button sits inside them. */
|
|
390
|
+
.tab-slot {
|
|
391
|
+
display: inline-flex;
|
|
392
|
+
align-items: stretch;
|
|
393
|
+
flex: none;
|
|
394
|
+
border-right: 1px solid var(--rule);
|
|
395
|
+
}
|
|
396
|
+
.tab-slot.active {
|
|
397
|
+
background: var(--fill);
|
|
398
|
+
color: var(--fill-ink);
|
|
399
|
+
/* The key has sunk into the chassis; this strip is the slot it dropped out of. */
|
|
400
|
+
box-shadow: inset 0 3px 0 var(--bg);
|
|
401
|
+
}
|
|
402
|
+
.tab-slot:not(.active):hover,
|
|
403
|
+
.tab-slot.drop-target:not(.active) {
|
|
404
|
+
background: color-mix(in srgb, var(--ink) 12%, transparent);
|
|
405
|
+
}
|
|
406
|
+
.tab-slot.drop-target {
|
|
407
|
+
outline: 2px dashed currentColor;
|
|
408
|
+
outline-offset: -5px;
|
|
409
|
+
}
|
|
410
|
+
/* pointer-events: none, or the tip would steal the drag from the tab it's pointing at. */
|
|
411
|
+
.drop-tip {
|
|
412
|
+
position: fixed;
|
|
413
|
+
z-index: 20;
|
|
414
|
+
transform: translateY(6px);
|
|
415
|
+
padding: 6px 10px;
|
|
416
|
+
background: var(--bg-card);
|
|
417
|
+
color: var(--ink);
|
|
418
|
+
font-family: var(--font-mono);
|
|
419
|
+
font-weight: 600;
|
|
420
|
+
font-size: 12px;
|
|
421
|
+
text-transform: uppercase;
|
|
422
|
+
letter-spacing: 0.06em;
|
|
423
|
+
white-space: nowrap;
|
|
424
|
+
pointer-events: none;
|
|
425
|
+
}
|
|
426
|
+
|
|
310
427
|
.tab {
|
|
311
428
|
display: inline-flex;
|
|
312
429
|
align-items: center;
|
|
@@ -317,7 +434,6 @@
|
|
|
317
434
|
margin: 0;
|
|
318
435
|
padding: 0 12px;
|
|
319
436
|
border: 0;
|
|
320
|
-
border-right: 1px solid var(--rule);
|
|
321
437
|
background: transparent;
|
|
322
438
|
color: var(--ink-soft);
|
|
323
439
|
font-family: var(--font-display);
|
|
@@ -331,16 +447,12 @@
|
|
|
331
447
|
}
|
|
332
448
|
/* Inversion is the house hover idiom, but it's also the active state here, so hovering
|
|
333
449
|
gets a tint instead — that keeps idle → hover → active legible as three steps. */
|
|
334
|
-
.tab:not(.active):hover {
|
|
335
|
-
background: color-mix(in srgb, var(--ink) 12%, transparent);
|
|
450
|
+
.tab-slot:not(.active):hover .tab {
|
|
336
451
|
color: var(--ink);
|
|
337
452
|
}
|
|
338
453
|
.tab.active {
|
|
339
|
-
background: var(--fill);
|
|
340
454
|
color: var(--fill-ink);
|
|
341
455
|
cursor: default;
|
|
342
|
-
/* The key has sunk into the chassis; this strip is the slot it dropped out of. */
|
|
343
|
-
box-shadow: inset 0 3px 0 var(--bg);
|
|
344
456
|
}
|
|
345
457
|
.tab:focus-visible {
|
|
346
458
|
outline: 2px solid var(--ink);
|
|
@@ -354,6 +466,40 @@
|
|
|
354
466
|
overflow: hidden;
|
|
355
467
|
text-overflow: ellipsis;
|
|
356
468
|
}
|
|
469
|
+
/* Reads as part of the tab, so it inherits the slot's colour rather than declaring its own. */
|
|
470
|
+
.tab-stop {
|
|
471
|
+
display: inline-flex;
|
|
472
|
+
align-items: center;
|
|
473
|
+
justify-content: center;
|
|
474
|
+
flex: none;
|
|
475
|
+
appearance: none;
|
|
476
|
+
margin: 0;
|
|
477
|
+
padding: 0 10px 0 0;
|
|
478
|
+
border: 0;
|
|
479
|
+
background: transparent;
|
|
480
|
+
color: inherit;
|
|
481
|
+
opacity: 0.55;
|
|
482
|
+
cursor: pointer;
|
|
483
|
+
}
|
|
484
|
+
.tab-slot:not(.active) .tab-stop {
|
|
485
|
+
color: var(--ink-soft);
|
|
486
|
+
}
|
|
487
|
+
.tab-stop:hover:not(:disabled) {
|
|
488
|
+
opacity: 1;
|
|
489
|
+
color: var(--danger);
|
|
490
|
+
}
|
|
491
|
+
.tab-stop:disabled {
|
|
492
|
+
opacity: var(--dim);
|
|
493
|
+
cursor: default;
|
|
494
|
+
}
|
|
495
|
+
.tab-stop:focus-visible {
|
|
496
|
+
outline: 2px solid var(--ink);
|
|
497
|
+
outline-offset: -2px;
|
|
498
|
+
}
|
|
499
|
+
.tab-slot.active .tab-stop:focus-visible {
|
|
500
|
+
outline-color: var(--fill-ink);
|
|
501
|
+
}
|
|
502
|
+
|
|
357
503
|
.tab-mode {
|
|
358
504
|
display: inline-flex;
|
|
359
505
|
align-items: center;
|
|
@@ -94,6 +94,16 @@
|
|
|
94
94
|
{#if instance.git_branch}
|
|
95
95
|
<BranchBox branch={instance.git_branch} />
|
|
96
96
|
{/if}
|
|
97
|
+
{#if instance.active_run}
|
|
98
|
+
<div class="agent-run" title="An MCP client is driving Claude in this sandbox">
|
|
99
|
+
<span class="agent-dot"></span>
|
|
100
|
+
<span class="agent-text">
|
|
101
|
+
{instance.active_run.status === 'queued'
|
|
102
|
+
? 'Agent run queued'
|
|
103
|
+
: (instance.active_run.last_activity ?? 'Agent running')}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
97
107
|
{#if instance.status === 'error' && instance.error}
|
|
98
108
|
<div class="card-error">{instance.error}</div>
|
|
99
109
|
{/if}
|
|
@@ -251,6 +261,34 @@
|
|
|
251
261
|
color: var(--ink);
|
|
252
262
|
outline: none;
|
|
253
263
|
}
|
|
264
|
+
.agent-run {
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
gap: 6px;
|
|
268
|
+
min-width: 0;
|
|
269
|
+
margin-top: 8px;
|
|
270
|
+
font-family: var(--font-mono);
|
|
271
|
+
font-size: 11px;
|
|
272
|
+
color: var(--ink-soft);
|
|
273
|
+
}
|
|
274
|
+
.agent-dot {
|
|
275
|
+
flex: none;
|
|
276
|
+
width: 6px;
|
|
277
|
+
height: 6px;
|
|
278
|
+
border-radius: 50%;
|
|
279
|
+
background: var(--fill);
|
|
280
|
+
animation: agent-pulse 1.6s ease-in-out infinite;
|
|
281
|
+
}
|
|
282
|
+
.agent-text {
|
|
283
|
+
overflow: hidden;
|
|
284
|
+
text-overflow: ellipsis;
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
}
|
|
287
|
+
@keyframes agent-pulse {
|
|
288
|
+
50% {
|
|
289
|
+
opacity: 0.25;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
254
292
|
.path {
|
|
255
293
|
margin-top: 10px;
|
|
256
294
|
font-family: var(--font-mono);
|