codebay 0.13.0 → 0.15.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 +522 -443
- package/.mochi/svelte-client/HydratableIsland-smgsf0ma.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-9vc087ep.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-a48xq47a.js +14 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-d4zehrqz.js +3 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-t5175pf5.js +44 -0
- package/.mochi/svelte-client/chunk-0mstgbtr.js +2 -0
- package/.mochi/svelte-client/chunk-0q9g0kpg.js +1 -0
- package/.mochi/svelte-client/chunk-3fwjzw1r.js +1 -0
- package/.mochi/svelte-client/{chunk-spk1b2r4.js → chunk-5pg0be5f.js} +1 -1
- package/.mochi/svelte-client/{chunk-ccfh8hxz.js → chunk-a2hty99b.js} +1 -1
- package/.mochi/svelte-client/{chunk-0mc09cyb.js → chunk-ds9yn9cq.js} +1 -1
- package/.mochi/svelte-client/{chunk-phf7v5jj.js → chunk-fn03gxer.js} +1 -1
- package/.mochi/svelte-client/chunk-jhgq7f65.js +2 -0
- package/.mochi/svelte-client/chunk-p5a07kx1.js +1 -0
- package/.mochi/svelte-client/chunk-p5wpyhsf.js +2 -0
- package/.mochi/svelte-client/chunk-t2kfvtb6.js +2 -0
- package/.mochi/svelte-client/{chunk-x0m3k0rn.js → chunk-v3wg9mdy.js} +1 -1
- package/.mochi/svelte-compile/{App-94xsdn0w.server.js → App-68f9a9fq.server.js} +64 -59
- package/.mochi/svelte-compile/{Avatars-fnccw47h.server.js → Avatars-vyh3rwxx.server.js} +14 -29
- package/.mochi/svelte-compile/Instance-yfw7s5mp.server.js +627 -0
- package/.mochi/svelte-compile/{Settings-h7gsjyz3.server.js → Settings-0b5t9kw9.server.js} +348 -151
- package/.mochi/svelte-compile/{chunk-w0hca7va.js → chunk-0y3wrae9.js} +13 -1
- package/.mochi/svelte-compile/{chunk-ed46m58e.js → chunk-2qzc07gt.js} +13 -1
- package/.mochi/svelte-compile/chunk-ef3hvegw.js +21 -0
- package/.mochi/svelte-compile/chunk-f9td8ej2.js +24 -0
- package/.mochi/svelte-compile/{chunk-pqb403fh.js → chunk-g0eravvj.js} +5 -16
- package/.mochi/svelte-css/AgentLogBox-38adby5iygny7.css +1 -0
- package/.mochi/svelte-css/{InstanceCard-25z7nas7y98zk.css → InstanceCard-3vxozrp9uoads.css} +1 -1
- package/.mochi/svelte-css/{SettingsView-bccw34agd2bj.css → SettingsView-2400myxb9ot56.css} +1 -1
- package/README.md +32 -1
- package/migrations/0009_agent_runs.sql +24 -0
- package/migrations/0010_agent_run_model.sql +2 -0
- package/package.json +6 -2
- package/public/favicon.svg +9 -0
- package/src/components/AgentLogBox.svelte +458 -0
- package/src/components/AppShell.svelte +11 -0
- package/src/components/FolderBrowser.svelte +13 -5
- package/src/components/InstanceCard.svelte +38 -0
- package/src/components/InstanceView.svelte +8 -0
- package/src/components/SettingsView.svelte +260 -0
- package/src/components/UiShowcase.svelte +4 -2
- package/src/container-injections/claude-code-credentials.ts +2 -3
- package/src/container-injections/claude-code-skills.ts +125 -0
- package/src/container-injections/claude-output-style.ts +2 -3
- package/src/container-injections/claude-statusline.ts +9 -17
- package/src/container-injections/github-credentials.ts +12 -1
- package/src/index.ts +32 -7
- package/src/lib/agent-run-stream.ts +246 -0
- package/src/lib/agent-runs.server.ts +724 -0
- package/src/lib/auth.server.ts +11 -2
- package/src/lib/build-freshness.server.ts +71 -0
- package/src/lib/config.server.ts +29 -6
- package/src/lib/container-files.server.ts +23 -1
- package/src/lib/data-dir-lock.server.ts +78 -0
- package/src/lib/db.server.ts +120 -1
- package/src/lib/devcontainer.server.ts +60 -14
- 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/host-claude.server.ts +31 -0
- package/src/lib/injections.server.ts +2 -0
- package/src/lib/instances.server.ts +45 -3
- package/src/lib/log-capture.server.ts +17 -5
- package/src/lib/mcp-auth.server.ts +52 -0
- package/src/lib/picker.server.ts +23 -2
- package/src/lib/sandbox-ops.server.ts +263 -0
- package/src/mcp/routes.server.ts +25 -0
- package/src/mcp/server.server.ts +52 -0
- package/src/mcp/tools.server.ts +494 -0
- package/src/pages/Settings.svelte +15 -0
- package/src/routes.ts +72 -1
- package/src/shell.html +1 -0
- package/src/types.ts +15 -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-wedc9x9y.js +0 -39
- package/.mochi/svelte-client/chunk-5jwfs4p9.js +0 -2
- package/.mochi/svelte-client/chunk-6qg4q98a.js +0 -2
- package/.mochi/svelte-client/chunk-tmeh70wg.js +0 -2
- package/.mochi/svelte-client/chunk-vs1yjbqt.js +0 -2
- package/.mochi/svelte-client/chunk-y7f4jbdm.js +0 -2
- package/.mochi/svelte-compile/Instance-knnkcn2p.server.js +0 -377
|
@@ -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>
|
|
@@ -275,6 +275,17 @@
|
|
|
275
275
|
syncTheme(msg.data.value);
|
|
276
276
|
return;
|
|
277
277
|
}
|
|
278
|
+
if (msg.type === 'run') {
|
|
279
|
+
// Its own frame because run progress ticks far faster than the instance list
|
|
280
|
+
// reconciles, so patch the one instance rather than waiting for a whole list.
|
|
281
|
+
const summary = msg.data;
|
|
282
|
+
instances = instances.map((i) =>
|
|
283
|
+
i.id === summary.instance_id
|
|
284
|
+
? { ...i, active_run: summary.finished_at ? null : summary }
|
|
285
|
+
: i
|
|
286
|
+
);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
278
289
|
if (msg.type === 'health') {
|
|
279
290
|
// A tick in flight when a rebuild starts probes the *old* container and
|
|
280
291
|
// reports it accessible, which would mount the iframe against the replacement too early.
|
|
@@ -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>
|
|
@@ -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);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
|
4
4
|
import ArrowUpRight from '@lucide/svelte/icons/arrow-up-right';
|
|
5
5
|
import HealthBox from './HealthBox.svelte';
|
|
6
|
+
import AgentLogBox from './AgentLogBox.svelte';
|
|
6
7
|
import StatusBadge from './StatusBadge.svelte';
|
|
7
8
|
import Skeleton from './Skeleton.svelte';
|
|
8
9
|
import { forwardedPortUrl } from '../lib/links.ts';
|
|
@@ -20,6 +21,9 @@
|
|
|
20
21
|
let health = $state<InstanceHealth | null>(null);
|
|
21
22
|
let lastFetchedAt = $state<number | null>(null);
|
|
22
23
|
let logs = $state('');
|
|
24
|
+
// Every `run` frame bumps this; AgentLogBox refetches on the change, so a live run's steps
|
|
25
|
+
// stream in without a second WebSocket.
|
|
26
|
+
let runBump = $state(0);
|
|
23
27
|
let logEl = $state<HTMLDivElement | null>(null);
|
|
24
28
|
let following = $state(true);
|
|
25
29
|
|
|
@@ -62,6 +66,8 @@
|
|
|
62
66
|
} else if (msg.type === 'health' && msg.data.id === id) {
|
|
63
67
|
health = msg.data.health;
|
|
64
68
|
lastFetchedAt = Date.now();
|
|
69
|
+
} else if (msg.type === 'run' && msg.data.instance_id === id) {
|
|
70
|
+
runBump += 1;
|
|
65
71
|
} else if (msg.type === 'theme') {
|
|
66
72
|
syncTheme(msg.data.value);
|
|
67
73
|
}
|
|
@@ -252,6 +258,8 @@
|
|
|
252
258
|
</div>
|
|
253
259
|
</section>
|
|
254
260
|
|
|
261
|
+
<AgentLogBox {id} bump={runBump} />
|
|
262
|
+
|
|
255
263
|
<div class="logwrap panel">
|
|
256
264
|
<div class="log-bar panel-bar">
|
|
257
265
|
<span>Boot log</span>
|