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,458 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Bot from '@lucide/svelte/icons/bot';
|
|
3
|
+
import Terminal from '@lucide/svelte/icons/terminal';
|
|
4
|
+
import MessageSquare from '@lucide/svelte/icons/message-square';
|
|
5
|
+
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
|
6
|
+
import CircleCheck from '@lucide/svelte/icons/circle-check';
|
|
7
|
+
import CircleX from '@lucide/svelte/icons/circle-x';
|
|
8
|
+
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
9
|
+
import { untrack } from 'svelte';
|
|
10
|
+
import { apiFetch } from '../api.ts';
|
|
11
|
+
import type { RunTimelineEntry } from '../lib/agent-run-stream.ts';
|
|
12
|
+
import type { AgentRunStatus } from '../types.ts';
|
|
13
|
+
|
|
14
|
+
interface AgentRun {
|
|
15
|
+
id: string;
|
|
16
|
+
status: AgentRunStatus;
|
|
17
|
+
prompt: string;
|
|
18
|
+
/** The id Claude reported once it started; null until then. */
|
|
19
|
+
model: string | null;
|
|
20
|
+
/** What the caller asked for, an alias like `sonnet`; null when the sandbox default was used. */
|
|
21
|
+
requested_model: string | null;
|
|
22
|
+
/** Claude's own session id, reported once the run starts; null until then. */
|
|
23
|
+
session_id: string | null;
|
|
24
|
+
/** Set when the run was launched to continue an earlier session, which is what makes it a continuation. */
|
|
25
|
+
resume_session_id: string | null;
|
|
26
|
+
result: string | null;
|
|
27
|
+
error: string | null;
|
|
28
|
+
is_error: boolean;
|
|
29
|
+
last_activity: string | null;
|
|
30
|
+
num_turns: number | null;
|
|
31
|
+
cost_usd: number | null;
|
|
32
|
+
duration_ms: number | null;
|
|
33
|
+
created_at: number;
|
|
34
|
+
started_at: number | null;
|
|
35
|
+
finished_at: number | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
id,
|
|
40
|
+
/** Bumped by the parent on every `run` stream frame, which is what drives live refresh. */
|
|
41
|
+
bump = 0
|
|
42
|
+
}: { id: string; bump?: number } = $props();
|
|
43
|
+
|
|
44
|
+
let runs = $state<AgentRun[]>([]);
|
|
45
|
+
let timelines = $state<Record<string, RunTimelineEntry[]>>({});
|
|
46
|
+
/** Expanded run ids. An array rather than a Set so Svelte tracks the changes. */
|
|
47
|
+
let open = $state<string[]>([]);
|
|
48
|
+
/** The newest run already reacted to, so one starting later can take the spotlight exactly once. */
|
|
49
|
+
let newest: string | null = null;
|
|
50
|
+
let loaded = $state(false);
|
|
51
|
+
let error = $state<string | null>(null);
|
|
52
|
+
|
|
53
|
+
const isOpen = (runId: string) => open.includes(runId);
|
|
54
|
+
|
|
55
|
+
async function load() {
|
|
56
|
+
try {
|
|
57
|
+
const q = open.length ? `?run_ids=${encodeURIComponent(open.join(','))}` : '';
|
|
58
|
+
const data = await apiFetch<{
|
|
59
|
+
runs: AgentRun[];
|
|
60
|
+
timelines: Record<string, RunTimelineEntry[]>;
|
|
61
|
+
}>(`/api/instances/${id}/agent-log${q}`, undefined, 'Could not load the agent log');
|
|
62
|
+
|
|
63
|
+
// This island can be older than the route (production serves the prebuilt bundle; routes
|
|
64
|
+
// load from source), so a key it does not know must degrade, not throw and unmount the box.
|
|
65
|
+
runs = Array.isArray(data.runs) ? data.runs : [];
|
|
66
|
+
timelines = { ...timelines, ...(data.timelines ?? {}) };
|
|
67
|
+
|
|
68
|
+
// A run that starts while the panel is open takes over: collapse the rest and expand it,
|
|
69
|
+
// so the box you are looking at is always the one currently doing something.
|
|
70
|
+
const top = data.runs[0]?.id ?? null;
|
|
71
|
+
if (top && top !== newest) {
|
|
72
|
+
newest = top;
|
|
73
|
+
open = [top];
|
|
74
|
+
}
|
|
75
|
+
error = null;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
error = (err as Error).message;
|
|
78
|
+
} finally {
|
|
79
|
+
loaded = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
$effect(() => {
|
|
84
|
+
// `bump` must be read as a real value. A bare `void bump;` is dead code the compiler drops,
|
|
85
|
+
// which leaves this effect with no dependency at all — the panel then renders the state it
|
|
86
|
+
// mounted with and never updates again, which is exactly what shipped first time round.
|
|
87
|
+
if (bump < 0) return;
|
|
88
|
+
// load() reads and writes `open`; tracked, that re-runs this effect off its own fetch.
|
|
89
|
+
untrack(() => void load());
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function toggle(runId: string) {
|
|
93
|
+
const opening = !isOpen(runId);
|
|
94
|
+
open = opening ? [...open, runId] : open.filter((r) => r !== runId);
|
|
95
|
+
// Expanding a box whose timeline the server hasn't sent yet needs a fetch to fill it.
|
|
96
|
+
if (opening && !timelines[runId]) void load();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ICONS = { tool: Terminal, text: MessageSquare, retry: RefreshCw, result: CircleCheck };
|
|
100
|
+
|
|
101
|
+
const isLive = (run: AgentRun) => run.status === 'running' || run.status === 'queued';
|
|
102
|
+
|
|
103
|
+
/** Newest step on top, keyed by the original position so a new step never re-keys the rest. */
|
|
104
|
+
const newestFirst = (entries: RunTimelineEntry[]) =>
|
|
105
|
+
entries.map((entry, index) => ({ entry, index })).reverse();
|
|
106
|
+
|
|
107
|
+
/** The id Claude reported, or the caller's alias until the run has started. */
|
|
108
|
+
const modelOf = (run: AgentRun) => run.model ?? run.requested_model;
|
|
109
|
+
|
|
110
|
+
function modelTitle(run: AgentRun): string | undefined {
|
|
111
|
+
if (run.model && run.requested_model && run.model !== run.requested_model) {
|
|
112
|
+
return `Requested as ${run.requested_model}`;
|
|
113
|
+
}
|
|
114
|
+
return run.model ? undefined : 'Requested; Claude has not reported the model it is using yet';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A resumed run reports the session it continued, so the two ids match and one short form labels both. */
|
|
118
|
+
const sessionOf = (run: AgentRun) => run.session_id ?? run.resume_session_id;
|
|
119
|
+
|
|
120
|
+
const shortSession = (sid: string) => sid.slice(0, 8);
|
|
121
|
+
|
|
122
|
+
function sessionTitle(run: AgentRun): string {
|
|
123
|
+
const sid = sessionOf(run);
|
|
124
|
+
if (!sid) return '';
|
|
125
|
+
const lead = run.resume_session_id
|
|
126
|
+
? 'Continues an earlier run in this session'
|
|
127
|
+
: 'New session, started by this run';
|
|
128
|
+
return `${lead}\nSession ${sid}${run.session_id ? '' : ' (requested; not started yet)'}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function when(run: AgentRun): string {
|
|
132
|
+
return new Date(run.created_at).toLocaleTimeString([], {
|
|
133
|
+
hour: '2-digit',
|
|
134
|
+
minute: '2-digit',
|
|
135
|
+
second: '2-digit'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function meta(run: AgentRun): string {
|
|
140
|
+
const bits: string[] = [];
|
|
141
|
+
if (run.num_turns != null) bits.push(`${run.num_turns} turns`);
|
|
142
|
+
if (run.cost_usd != null) bits.push(`$${run.cost_usd.toFixed(4)}`);
|
|
143
|
+
if (run.duration_ms != null) bits.push(`${(run.duration_ms / 1000).toFixed(1)}s`);
|
|
144
|
+
return bits.join(' · ');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The one-line gist on a collapsed box, so the strip is worth scanning shut. */
|
|
148
|
+
function peek(run: AgentRun): string {
|
|
149
|
+
if (run.status === 'queued') return 'Queued';
|
|
150
|
+
if (isLive(run)) return (run.last_activity ?? 'Working…').replace(/\s+/g, ' ').trim();
|
|
151
|
+
return (run.error ?? run.result ?? run.prompt).replace(/\s+/g, ' ').trim();
|
|
152
|
+
}
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
{#if loaded && runs.length > 0}
|
|
156
|
+
<section class="agentwrap panel">
|
|
157
|
+
<div class="agent-bar panel-bar">
|
|
158
|
+
<span><Bot size={14} /> Agent log</span>
|
|
159
|
+
<span class="count">{runs.length} run{runs.length === 1 ? '' : 's'}</span>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{#each runs as run (run.id)}
|
|
163
|
+
{@const expanded = isOpen(run.id)}
|
|
164
|
+
<div class="run">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
class="run-head"
|
|
168
|
+
aria-expanded={expanded}
|
|
169
|
+
onclick={() => toggle(run.id)}
|
|
170
|
+
>
|
|
171
|
+
<span class="chev" class:down={expanded}><ChevronRight size={14} /></span>
|
|
172
|
+
<span class="when">{when(run)}</span>
|
|
173
|
+
<span class="status" class:bad={run.is_error} class:live={isLive(run)}>
|
|
174
|
+
{#if isLive(run)}<span class="pulse"></span>{/if}
|
|
175
|
+
{run.status}
|
|
176
|
+
</span>
|
|
177
|
+
{#if modelOf(run)}
|
|
178
|
+
<span class="model" title={modelTitle(run)}>{modelOf(run)}</span>
|
|
179
|
+
{/if}
|
|
180
|
+
{#if sessionOf(run)}
|
|
181
|
+
{@const sid = sessionOf(run)!}
|
|
182
|
+
<span class="session" class:resumed={run.resume_session_id} title={sessionTitle(run)}>
|
|
183
|
+
{run.resume_session_id ? '⤷' : '◆'}
|
|
184
|
+
{shortSession(sid)}
|
|
185
|
+
</span>
|
|
186
|
+
{/if}
|
|
187
|
+
{#if !expanded}
|
|
188
|
+
<span class="peek">{peek(run)}</span>
|
|
189
|
+
{/if}
|
|
190
|
+
<span class="run-meta">{meta(run)}</span>
|
|
191
|
+
</button>
|
|
192
|
+
|
|
193
|
+
{#if expanded}
|
|
194
|
+
<div class="prompt">
|
|
195
|
+
<div class="ptitle">Initial prompt</div>
|
|
196
|
+
<div class="ptext">{run.prompt}</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<ol class="steps">
|
|
200
|
+
{#if isLive(run)}
|
|
201
|
+
<li class="step">
|
|
202
|
+
<span class="ico"><span class="pulse"></span></span>
|
|
203
|
+
<span class="body">
|
|
204
|
+
<!-- Deliberately not `last_activity`: while a run is live that is the same step
|
|
205
|
+
the newest timeline row already shows, so echoing it prints the line twice. -->
|
|
206
|
+
<span class="text">
|
|
207
|
+
{run.status === 'queued' ? 'Queued — waiting for the sandbox' : 'Working…'}
|
|
208
|
+
</span>
|
|
209
|
+
</span>
|
|
210
|
+
</li>
|
|
211
|
+
{:else if (timelines[run.id] ?? []).length === 0}
|
|
212
|
+
<li class="step">
|
|
213
|
+
<span class="body"><span class="text">No steps recorded.</span></span>
|
|
214
|
+
</li>
|
|
215
|
+
{/if}
|
|
216
|
+
|
|
217
|
+
{#each newestFirst(timelines[run.id] ?? []) as { entry, index } (index)}
|
|
218
|
+
<li class="step" class:sub={entry.subagent}>
|
|
219
|
+
<span class="ico" class:bad={entry.kind === 'result' && entry.isError}>
|
|
220
|
+
{#if entry.kind === 'result' && entry.isError}
|
|
221
|
+
<CircleX size={14} />
|
|
222
|
+
{:else}
|
|
223
|
+
{@const Icon = ICONS[entry.kind]}
|
|
224
|
+
<Icon size={14} />
|
|
225
|
+
{/if}
|
|
226
|
+
</span>
|
|
227
|
+
<span class="body">
|
|
228
|
+
{#if entry.name}<span class="tool">{entry.name}</span>{/if}
|
|
229
|
+
<span class="text">{entry.text}</span>
|
|
230
|
+
</span>
|
|
231
|
+
</li>
|
|
232
|
+
{/each}
|
|
233
|
+
</ol>
|
|
234
|
+
|
|
235
|
+
{#if run.error && !isLive(run)}
|
|
236
|
+
<div class="err">{run.error}</div>
|
|
237
|
+
{/if}
|
|
238
|
+
{/if}
|
|
239
|
+
</div>
|
|
240
|
+
{/each}
|
|
241
|
+
|
|
242
|
+
{#if error}
|
|
243
|
+
<div class="err">{error}</div>
|
|
244
|
+
{/if}
|
|
245
|
+
</section>
|
|
246
|
+
{/if}
|
|
247
|
+
|
|
248
|
+
<style>
|
|
249
|
+
.agentwrap {
|
|
250
|
+
overflow: hidden;
|
|
251
|
+
/* Matches the 18px the page puts under every other panel; this one sits above the boot log. */
|
|
252
|
+
margin-bottom: 18px;
|
|
253
|
+
}
|
|
254
|
+
.agent-bar {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
justify-content: space-between;
|
|
258
|
+
gap: 12px;
|
|
259
|
+
}
|
|
260
|
+
.agent-bar span {
|
|
261
|
+
display: inline-flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 6px;
|
|
264
|
+
}
|
|
265
|
+
.count {
|
|
266
|
+
font-size: 11px;
|
|
267
|
+
opacity: 0.8;
|
|
268
|
+
}
|
|
269
|
+
.run {
|
|
270
|
+
background: var(--bg-card);
|
|
271
|
+
border-top: 1px solid var(--rule-soft);
|
|
272
|
+
}
|
|
273
|
+
.run:first-of-type {
|
|
274
|
+
border-top: none;
|
|
275
|
+
}
|
|
276
|
+
.run-head {
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
gap: 10px;
|
|
280
|
+
width: 100%;
|
|
281
|
+
padding: 9px 14px;
|
|
282
|
+
background: none;
|
|
283
|
+
border: none;
|
|
284
|
+
color: inherit;
|
|
285
|
+
font: inherit;
|
|
286
|
+
text-align: left;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
min-width: 0;
|
|
289
|
+
}
|
|
290
|
+
.run-head:hover {
|
|
291
|
+
background: color-mix(in srgb, var(--ink) 6%, transparent);
|
|
292
|
+
}
|
|
293
|
+
.chev {
|
|
294
|
+
flex: none;
|
|
295
|
+
display: inline-flex;
|
|
296
|
+
color: var(--ink-soft);
|
|
297
|
+
transition: transform 120ms ease;
|
|
298
|
+
}
|
|
299
|
+
.chev.down {
|
|
300
|
+
transform: rotate(90deg);
|
|
301
|
+
}
|
|
302
|
+
.when,
|
|
303
|
+
.status,
|
|
304
|
+
.model,
|
|
305
|
+
.session,
|
|
306
|
+
.run-meta,
|
|
307
|
+
.peek {
|
|
308
|
+
font-family: var(--font-mono);
|
|
309
|
+
font-size: 11px;
|
|
310
|
+
}
|
|
311
|
+
.session {
|
|
312
|
+
flex: none;
|
|
313
|
+
display: inline-flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
gap: 4px;
|
|
316
|
+
color: var(--ink-faint);
|
|
317
|
+
}
|
|
318
|
+
/* A continuation is the exception worth spotting in the strip, so only it gets full-strength ink. */
|
|
319
|
+
.session.resumed {
|
|
320
|
+
color: var(--ink-soft);
|
|
321
|
+
}
|
|
322
|
+
.model {
|
|
323
|
+
flex: none;
|
|
324
|
+
color: var(--ink-soft);
|
|
325
|
+
max-width: 14em;
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
text-overflow: ellipsis;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
}
|
|
330
|
+
.when {
|
|
331
|
+
flex: none;
|
|
332
|
+
color: var(--ink-soft);
|
|
333
|
+
}
|
|
334
|
+
.status {
|
|
335
|
+
flex: none;
|
|
336
|
+
display: inline-flex;
|
|
337
|
+
align-items: center;
|
|
338
|
+
gap: 5px;
|
|
339
|
+
border: 1px solid var(--ink-faint);
|
|
340
|
+
padding: 0 5px;
|
|
341
|
+
}
|
|
342
|
+
.status.live {
|
|
343
|
+
border-color: var(--fill);
|
|
344
|
+
}
|
|
345
|
+
.status.bad {
|
|
346
|
+
color: var(--attn-waiting);
|
|
347
|
+
border-color: var(--attn-waiting);
|
|
348
|
+
}
|
|
349
|
+
/* Collapsed rows carry the gist, so the strip is scannable without opening anything. */
|
|
350
|
+
.peek {
|
|
351
|
+
flex: 1;
|
|
352
|
+
min-width: 0;
|
|
353
|
+
color: var(--ink-soft);
|
|
354
|
+
overflow: hidden;
|
|
355
|
+
text-overflow: ellipsis;
|
|
356
|
+
white-space: nowrap;
|
|
357
|
+
}
|
|
358
|
+
.run-meta {
|
|
359
|
+
margin-left: auto;
|
|
360
|
+
flex: none;
|
|
361
|
+
color: var(--ink-faint);
|
|
362
|
+
}
|
|
363
|
+
.prompt {
|
|
364
|
+
padding: 10px 14px;
|
|
365
|
+
border-top: 1px solid var(--rule-soft);
|
|
366
|
+
}
|
|
367
|
+
.ptitle {
|
|
368
|
+
font-family: var(--font-mono);
|
|
369
|
+
font-size: 10px;
|
|
370
|
+
text-transform: uppercase;
|
|
371
|
+
letter-spacing: 0.08em;
|
|
372
|
+
color: var(--ink-soft);
|
|
373
|
+
margin-bottom: 4px;
|
|
374
|
+
}
|
|
375
|
+
.ptext {
|
|
376
|
+
font-size: 12px;
|
|
377
|
+
line-height: 1.5;
|
|
378
|
+
/* The prompt is arbitrary user text, often multi-line — keep its shape but cap the height. */
|
|
379
|
+
white-space: pre-wrap;
|
|
380
|
+
max-height: 7.5em;
|
|
381
|
+
overflow-y: auto;
|
|
382
|
+
}
|
|
383
|
+
/* Newest step first, so the cap never hides what the run is doing right now. */
|
|
384
|
+
.steps {
|
|
385
|
+
list-style: none;
|
|
386
|
+
margin: 0;
|
|
387
|
+
padding: 0;
|
|
388
|
+
max-height: 420px;
|
|
389
|
+
overflow-y: auto;
|
|
390
|
+
}
|
|
391
|
+
.step {
|
|
392
|
+
display: flex;
|
|
393
|
+
gap: 10px;
|
|
394
|
+
align-items: flex-start;
|
|
395
|
+
padding: 8px 14px;
|
|
396
|
+
border-top: 1px solid var(--rule-soft);
|
|
397
|
+
}
|
|
398
|
+
/* A subagent's steps are the nested ones; indent rather than hide them. */
|
|
399
|
+
.step.sub {
|
|
400
|
+
padding-left: 34px;
|
|
401
|
+
opacity: 0.75;
|
|
402
|
+
}
|
|
403
|
+
.ico {
|
|
404
|
+
flex: none;
|
|
405
|
+
display: inline-flex;
|
|
406
|
+
width: 16px;
|
|
407
|
+
height: 16px;
|
|
408
|
+
align-items: center;
|
|
409
|
+
justify-content: center;
|
|
410
|
+
color: var(--ink-soft);
|
|
411
|
+
margin-top: 1px;
|
|
412
|
+
}
|
|
413
|
+
.ico.bad {
|
|
414
|
+
color: var(--attn-waiting);
|
|
415
|
+
}
|
|
416
|
+
.body {
|
|
417
|
+
display: flex;
|
|
418
|
+
gap: 6px;
|
|
419
|
+
min-width: 0;
|
|
420
|
+
flex-wrap: wrap;
|
|
421
|
+
align-items: baseline;
|
|
422
|
+
}
|
|
423
|
+
.tool {
|
|
424
|
+
font-family: var(--font-mono);
|
|
425
|
+
font-size: 11px;
|
|
426
|
+
border: 1px solid var(--ink-faint);
|
|
427
|
+
padding: 0 4px;
|
|
428
|
+
flex: none;
|
|
429
|
+
}
|
|
430
|
+
.text {
|
|
431
|
+
font-family: var(--font-mono);
|
|
432
|
+
font-size: 12px;
|
|
433
|
+
line-height: 1.5;
|
|
434
|
+
word-break: break-word;
|
|
435
|
+
min-width: 0;
|
|
436
|
+
}
|
|
437
|
+
.pulse {
|
|
438
|
+
flex: none;
|
|
439
|
+
width: 7px;
|
|
440
|
+
height: 7px;
|
|
441
|
+
border-radius: 50%;
|
|
442
|
+
background: var(--fill);
|
|
443
|
+
animation: agent-pulse 1.6s ease-in-out infinite;
|
|
444
|
+
}
|
|
445
|
+
@keyframes agent-pulse {
|
|
446
|
+
50% {
|
|
447
|
+
opacity: 0.25;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
.err {
|
|
451
|
+
padding: 8px 14px;
|
|
452
|
+
font-family: var(--font-mono);
|
|
453
|
+
font-size: 11px;
|
|
454
|
+
border-top: 1px solid var(--rule-soft);
|
|
455
|
+
color: var(--attn-waiting);
|
|
456
|
+
white-space: pre-wrap;
|
|
457
|
+
}
|
|
458
|
+
</style>
|
|
@@ -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,10 +355,25 @@
|
|
|
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;
|
|
277
365
|
}
|
|
366
|
+
if (msg.type === 'run') {
|
|
367
|
+
// Its own frame because run progress ticks far faster than the instance list
|
|
368
|
+
// reconciles, so patch the one instance rather than waiting for a whole list.
|
|
369
|
+
const summary = msg.data;
|
|
370
|
+
instances = instances.map((i) =>
|
|
371
|
+
i.id === summary.instance_id
|
|
372
|
+
? { ...i, active_run: summary.finished_at ? null : summary }
|
|
373
|
+
: i
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
278
377
|
if (msg.type === 'health') {
|
|
279
378
|
// A tick in flight when a rebuild starts probes the *old* container and
|
|
280
379
|
// reports it accessible, which would mount the iframe against the replacement too early.
|
|
@@ -290,6 +389,7 @@
|
|
|
290
389
|
for (const id of [...everReady]) if (!live.has(id)) everReady.delete(id);
|
|
291
390
|
for (const id of [...forced]) if (!live.has(id)) forced.delete(id);
|
|
292
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);
|
|
293
393
|
const nextAttention: Record<string, 'done' | 'waiting' | null> = {};
|
|
294
394
|
for (const inst of next) nextAttention[inst.id] = inst.attention;
|
|
295
395
|
if (primed) {
|
|
@@ -345,10 +445,15 @@
|
|
|
345
445
|
{attention}
|
|
346
446
|
{editingId}
|
|
347
447
|
bind:editingName
|
|
448
|
+
stopping={[...stopping]}
|
|
348
449
|
onreload={activeInstance && activeInstance.mode !== 'terminal' && mountable(active)
|
|
349
450
|
? reloadActive
|
|
350
451
|
: undefined}
|
|
351
452
|
onselect={(id) => navigate(`/ide/${id}`)}
|
|
453
|
+
onstop={stopTab}
|
|
454
|
+
ondropfiles={livePreflight.uploadEnabled
|
|
455
|
+
? (id, files) => void handleUpload(id, files)
|
|
456
|
+
: undefined}
|
|
352
457
|
onstartrename={startRename}
|
|
353
458
|
oncommitrename={commitRename}
|
|
354
459
|
oncancelrename={cancelRename}
|
|
@@ -371,7 +476,7 @@
|
|
|
371
476
|
<div
|
|
372
477
|
class="pane"
|
|
373
478
|
class:active={inst.id === active}
|
|
374
|
-
role="
|
|
479
|
+
role="region"
|
|
375
480
|
aria-labelledby="tab-{inst.id}"
|
|
376
481
|
>
|
|
377
482
|
{#if mountable(inst.id)}
|
|
@@ -382,10 +487,13 @@
|
|
|
382
487
|
initialOpen={inst.terminal_split === 1}
|
|
383
488
|
/>
|
|
384
489
|
{:else}
|
|
490
|
+
<!-- code-server answers a terminal's OSC 52 through the Clipboard API, which
|
|
491
|
+
Permissions Policy gates per-frame. -->
|
|
385
492
|
<iframe
|
|
386
493
|
bind:this={frames[inst.id]}
|
|
387
494
|
src={ideUrl(inst)}
|
|
388
495
|
title={inst.name}
|
|
496
|
+
allow="clipboard-read; clipboard-write"
|
|
389
497
|
onload={() => loadedFrames.add(inst.id)}
|
|
390
498
|
></iframe>
|
|
391
499
|
{/if}
|
|
@@ -402,6 +510,9 @@
|
|
|
402
510
|
{:else if inst.mode !== 'terminal' && !loadedFrames.has(inst.id)}
|
|
403
511
|
<IdeLoader />
|
|
404
512
|
{/if}
|
|
513
|
+
{#if dropping && inst.id === active && inst.mode === 'terminal'}
|
|
514
|
+
<DropZone ondrop={onDrop} />
|
|
515
|
+
{/if}
|
|
405
516
|
</div>
|
|
406
517
|
{/if}
|
|
407
518
|
{/each}
|