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
|
@@ -3,8 +3,10 @@
|
|
|
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';
|
|
9
|
+
import DropZone from './DropZone.svelte';
|
|
8
10
|
import { forwardedPortUrl } from '../lib/links.ts';
|
|
9
11
|
import { liveSocket, liveStream } from '../live.ts';
|
|
10
12
|
import { apiPost, apiDelete } from '../api.ts';
|
|
@@ -13,13 +15,27 @@
|
|
|
13
15
|
import { onBackLinkClick, installPopupBackTrap } from '../lib/popup-nav.ts';
|
|
14
16
|
import { syncTheme } from '../theme.ts';
|
|
15
17
|
import { tick } from 'svelte';
|
|
18
|
+
import toast, { Toaster } from 'svelte-french-toast';
|
|
19
|
+
import { TOAST_OPTIONS } from '../toast.ts';
|
|
20
|
+
import { filesFrom, uploadFile } from '../lib/upload.ts';
|
|
16
21
|
|
|
17
|
-
let {
|
|
22
|
+
let {
|
|
23
|
+
id,
|
|
24
|
+
injectionChecks = 0,
|
|
25
|
+
uploadEnabled = false
|
|
26
|
+
}: { id: string; injectionChecks?: number; uploadEnabled?: boolean } = $props();
|
|
27
|
+
|
|
28
|
+
// DB-backed, so it initializes from the prop; the stream keeps it current across a Settings change.
|
|
29
|
+
// svelte-ignore state_referenced_locally
|
|
30
|
+
let uploadArmed = $state(uploadEnabled);
|
|
18
31
|
|
|
19
32
|
let instance = $state<Instance | null>(null);
|
|
20
33
|
let health = $state<InstanceHealth | null>(null);
|
|
21
34
|
let lastFetchedAt = $state<number | null>(null);
|
|
22
35
|
let logs = $state('');
|
|
36
|
+
// Every `run` frame bumps this; AgentLogBox refetches on the change, so a live run's steps
|
|
37
|
+
// stream in without a second WebSocket.
|
|
38
|
+
let runBump = $state(0);
|
|
23
39
|
let logEl = $state<HTMLDivElement | null>(null);
|
|
24
40
|
let following = $state(true);
|
|
25
41
|
|
|
@@ -62,8 +78,12 @@
|
|
|
62
78
|
} else if (msg.type === 'health' && msg.data.id === id) {
|
|
63
79
|
health = msg.data.health;
|
|
64
80
|
lastFetchedAt = Date.now();
|
|
81
|
+
} else if (msg.type === 'run' && msg.data.instance_id === id) {
|
|
82
|
+
runBump += 1;
|
|
65
83
|
} else if (msg.type === 'theme') {
|
|
66
84
|
syncTheme(msg.data.value);
|
|
85
|
+
} else if (msg.type === 'upload-enabled') {
|
|
86
|
+
uploadArmed = msg.data.enabled;
|
|
67
87
|
}
|
|
68
88
|
})
|
|
69
89
|
);
|
|
@@ -140,8 +160,57 @@
|
|
|
140
160
|
clearTimeout(copyTimer);
|
|
141
161
|
copyTimer = setTimeout(() => (copied = false), 2000);
|
|
142
162
|
}
|
|
163
|
+
|
|
164
|
+
// Sequential, so the toast order matches the drop order.
|
|
165
|
+
async function handleUpload(files: File[]) {
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
try {
|
|
168
|
+
const saved = await uploadFile(id, file);
|
|
169
|
+
toast.success(`Saved ${saved.containerPath}`);
|
|
170
|
+
void navigator.clipboard.writeText(saved.containerPath).catch(() => {});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
toast.error((err as Error).message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let dropping = $state(false);
|
|
178
|
+
|
|
179
|
+
function onDragEnter(e: DragEvent) {
|
|
180
|
+
if (!uploadArmed || !e.dataTransfer?.types.includes('Files')) return;
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
dropping = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function onDragOver(e: DragEvent) {
|
|
186
|
+
if (dropping) e.preventDefault();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function onDragLeave(e: DragEvent) {
|
|
190
|
+
if (e.relatedTarget === null) dropping = false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function onDrop(e: DragEvent) {
|
|
194
|
+
if (!dropping) return;
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
dropping = false;
|
|
197
|
+
if (uploadArmed) void handleUpload(filesFrom(e.dataTransfer));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Only claims pastes that carry files, so text pasted into the page's inputs is untouched.
|
|
201
|
+
function onPaste(e: ClipboardEvent) {
|
|
202
|
+
if (!uploadArmed) return;
|
|
203
|
+
const files = filesFrom(e.clipboardData);
|
|
204
|
+
if (!files.length) return;
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
void handleUpload(files);
|
|
207
|
+
}
|
|
143
208
|
</script>
|
|
144
209
|
|
|
210
|
+
<svelte:window onpaste={onPaste} />
|
|
211
|
+
<!-- Instance renders outside AppShell, which hosts the app-wide Toaster. -->
|
|
212
|
+
<Toaster toastOptions={TOAST_OPTIONS} />
|
|
213
|
+
|
|
145
214
|
<header class="topbar">
|
|
146
215
|
<a class="back" href="/" onclick={onBackLinkClick}><ArrowLeft size={15} /> All instances</a>
|
|
147
216
|
<div class="title">
|
|
@@ -165,7 +234,16 @@
|
|
|
165
234
|
</header>
|
|
166
235
|
{#if restartError}<p class="restart-err">{restartError}</p>{/if}
|
|
167
236
|
|
|
168
|
-
<main
|
|
237
|
+
<main
|
|
238
|
+
class="stage"
|
|
239
|
+
ondragenter={onDragEnter}
|
|
240
|
+
ondragover={onDragOver}
|
|
241
|
+
ondragleave={onDragLeave}
|
|
242
|
+
ondrop={onDrop}
|
|
243
|
+
>
|
|
244
|
+
{#if dropping}
|
|
245
|
+
<DropZone />
|
|
246
|
+
{/if}
|
|
169
247
|
<div class="meta">
|
|
170
248
|
<span class="k">Source</span>
|
|
171
249
|
{#if instance}<code>{instance.source_path}</code>{:else}<Skeleton variant="wide" />{/if}
|
|
@@ -252,6 +330,8 @@
|
|
|
252
330
|
</div>
|
|
253
331
|
</section>
|
|
254
332
|
|
|
333
|
+
<AgentLogBox {id} bump={runBump} />
|
|
334
|
+
|
|
255
335
|
<div class="logwrap panel">
|
|
256
336
|
<div class="log-bar panel-bar">
|
|
257
337
|
<span>Boot log</span>
|
|
@@ -366,6 +446,7 @@
|
|
|
366
446
|
color: var(--danger);
|
|
367
447
|
}
|
|
368
448
|
.stage {
|
|
449
|
+
position: relative;
|
|
369
450
|
max-width: 1200px;
|
|
370
451
|
margin: 0 auto;
|
|
371
452
|
padding: 20px 24px 40px;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import SunMoon from '@lucide/svelte/icons/sun-moon';
|
|
10
10
|
import ThemePicker from './ThemePicker.svelte';
|
|
11
11
|
import Layers from '@lucide/svelte/icons/layers';
|
|
12
|
+
import Upload from '@lucide/svelte/icons/upload';
|
|
12
13
|
import FolderMinus from '@lucide/svelte/icons/folder-minus';
|
|
13
14
|
import FolderCog from '@lucide/svelte/icons/folder-cog';
|
|
14
15
|
import Trash2 from '@lucide/svelte/icons/trash-2';
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
import ListOrdered from '@lucide/svelte/icons/list-ordered';
|
|
25
26
|
import Boxes from '@lucide/svelte/icons/boxes';
|
|
26
27
|
import Puzzle from '@lucide/svelte/icons/puzzle';
|
|
28
|
+
import Plug from '@lucide/svelte/icons/plug';
|
|
27
29
|
import { Toaster } from 'svelte-french-toast';
|
|
28
30
|
import { TOAST_OPTIONS } from '../toast.ts';
|
|
29
31
|
import { flushSync } from 'svelte';
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
import ShieldCheck from '@lucide/svelte/icons/shield-check';
|
|
55
57
|
import Gauge from '@lucide/svelte/icons/gauge';
|
|
56
58
|
import MessageSquare from '@lucide/svelte/icons/message-square';
|
|
59
|
+
import ClipboardX from '@lucide/svelte/icons/clipboard-x';
|
|
57
60
|
import { installPopupBackTrap } from '../lib/popup-nav.ts';
|
|
58
61
|
|
|
59
62
|
/** Every settings form action fails with the same `{ error }` shape. */
|
|
@@ -68,9 +71,14 @@
|
|
|
68
71
|
defaultImage,
|
|
69
72
|
builtinImage,
|
|
70
73
|
disableBuildCache,
|
|
74
|
+
workspaceUploadEnabled,
|
|
71
75
|
copyIgnorePatterns,
|
|
72
76
|
builtinCopyIgnore,
|
|
73
77
|
claudeConfigDir,
|
|
78
|
+
mcpEnabled,
|
|
79
|
+
mcpToken,
|
|
80
|
+
mcpUrl,
|
|
81
|
+
mcpPrAttribution,
|
|
74
82
|
gitIdentityEnabled,
|
|
75
83
|
gitIdentityName,
|
|
76
84
|
gitIdentityEmail,
|
|
@@ -110,9 +118,14 @@
|
|
|
110
118
|
defaultImage: string;
|
|
111
119
|
builtinImage: string;
|
|
112
120
|
disableBuildCache: boolean;
|
|
121
|
+
workspaceUploadEnabled: boolean;
|
|
113
122
|
copyIgnorePatterns: string;
|
|
114
123
|
builtinCopyIgnore: string;
|
|
115
124
|
claudeConfigDir: string;
|
|
125
|
+
mcpEnabled: boolean;
|
|
126
|
+
mcpToken: string;
|
|
127
|
+
mcpUrl: string;
|
|
128
|
+
mcpPrAttribution: boolean;
|
|
116
129
|
gitIdentityEnabled: boolean;
|
|
117
130
|
gitIdentityName: string;
|
|
118
131
|
gitIdentityEmail: string;
|
|
@@ -159,6 +172,17 @@
|
|
|
159
172
|
|
|
160
173
|
$effect(() => installPopupBackTrap());
|
|
161
174
|
|
|
175
|
+
/** Only settleable after hydration — SSR has no origin to judge. */
|
|
176
|
+
let insecure = $state<{ origin: string; tunnel: string } | null>(null);
|
|
177
|
+
$effect(() => {
|
|
178
|
+
if (window.isSecureContext) return;
|
|
179
|
+
const port = location.port || '80';
|
|
180
|
+
insecure = {
|
|
181
|
+
origin: location.origin,
|
|
182
|
+
tunnel: `ssh -L ${port}:localhost:${port} ${location.hostname}`
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
|
|
162
186
|
/** Reused by every plain save form below; only the per-control state setters differ. */
|
|
163
187
|
function saveOpts<Success extends Record<string, unknown> = Record<string, unknown>>(handlers: {
|
|
164
188
|
setSaving: (v: boolean) => void;
|
|
@@ -398,6 +422,57 @@
|
|
|
398
422
|
claudeDirFormEl?.requestSubmit();
|
|
399
423
|
}
|
|
400
424
|
|
|
425
|
+
// svelte-ignore state_referenced_locally
|
|
426
|
+
let mcp = $state(mcpEnabled);
|
|
427
|
+
// Unlike every other secret here this one round-trips to the client — a human has to copy it.
|
|
428
|
+
// svelte-ignore state_referenced_locally
|
|
429
|
+
let mcpTokenValue = $state(mcpToken);
|
|
430
|
+
let savingMcpToggle = $state(false);
|
|
431
|
+
let mcpToggleError = $state<string | null>(null);
|
|
432
|
+
let savingMcpToken = $state(false);
|
|
433
|
+
let mcpTokenError = $state<string | null>(null);
|
|
434
|
+
let mcpCopied = $state<string | null>(null);
|
|
435
|
+
|
|
436
|
+
const mcpToggleOpts = toggleOpts<{ enabled: boolean; token: string }>({
|
|
437
|
+
set: (v) => (mcp = v),
|
|
438
|
+
setSaving: (v) => (savingMcpToggle = v),
|
|
439
|
+
setError: (v) => (mcpToggleError = v),
|
|
440
|
+
onSuccess: (data) => (mcpTokenValue = data?.token ?? '')
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const mcpTokenOpts = saveOpts<{ token: string }>({
|
|
444
|
+
setSaving: (v) => (savingMcpToken = v),
|
|
445
|
+
setError: (v) => (mcpTokenError = v),
|
|
446
|
+
onSuccess: (data) => (mcpTokenValue = data?.token ?? ''),
|
|
447
|
+
confirmMessage:
|
|
448
|
+
'Regenerate the MCP token? Any agent already configured with the current token will stop working.'
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// svelte-ignore state_referenced_locally
|
|
452
|
+
let mcpAttribution = $state(mcpPrAttribution);
|
|
453
|
+
let savingMcpAttribution = $state(false);
|
|
454
|
+
let mcpAttributionError = $state<string | null>(null);
|
|
455
|
+
|
|
456
|
+
const mcpAttributionOpts = toggleOpts({
|
|
457
|
+
set: (v) => (mcpAttribution = v),
|
|
458
|
+
setSaving: (v) => (savingMcpAttribution = v),
|
|
459
|
+
setError: (v) => (mcpAttributionError = v)
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const mcpCommand = $derived(
|
|
463
|
+
`claude mcp add --transport http codebay ${mcpUrl} --header "Authorization: Bearer ${mcpTokenValue}"`
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
async function copyMcp(label: string, text: string) {
|
|
467
|
+
try {
|
|
468
|
+
await navigator.clipboard.writeText(text);
|
|
469
|
+
mcpCopied = label;
|
|
470
|
+
setTimeout(() => (mcpCopied = null), 1500);
|
|
471
|
+
} catch {
|
|
472
|
+
mcpTokenError = 'Could not copy — select the text and copy it manually.';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
401
476
|
// svelte-ignore state_referenced_locally
|
|
402
477
|
let gitIdentity = $state(gitIdentityEnabled);
|
|
403
478
|
let savingGitToggle = $state(false);
|
|
@@ -441,6 +516,18 @@
|
|
|
441
516
|
setError: (v) => (cacheError = v)
|
|
442
517
|
});
|
|
443
518
|
|
|
519
|
+
// DB-backed rather than localStorage, so it initializes from the prop.
|
|
520
|
+
// svelte-ignore state_referenced_locally
|
|
521
|
+
let workspaceUpload = $state(workspaceUploadEnabled);
|
|
522
|
+
let savingUpload = $state(false);
|
|
523
|
+
let uploadError = $state<string | null>(null);
|
|
524
|
+
|
|
525
|
+
const uploadToggleOpts = toggleOpts({
|
|
526
|
+
set: (v) => (workspaceUpload = v),
|
|
527
|
+
setSaving: (v) => (savingUpload = v),
|
|
528
|
+
setError: (v) => (uploadError = v)
|
|
529
|
+
});
|
|
530
|
+
|
|
444
531
|
let clearing = $state(false);
|
|
445
532
|
let clearMsg = $state<string | null>(null);
|
|
446
533
|
let clearError = $state<string | null>(null);
|
|
@@ -900,6 +987,28 @@
|
|
|
900
987
|
</AppBar>
|
|
901
988
|
|
|
902
989
|
<main class="content">
|
|
990
|
+
{#if insecure}
|
|
991
|
+
<section class="card">
|
|
992
|
+
<div class="row">
|
|
993
|
+
<div class="label">
|
|
994
|
+
<ClipboardX size={20} />
|
|
995
|
+
<div class="text">
|
|
996
|
+
<div class="name">Copying out of a terminal may not work on this address</div>
|
|
997
|
+
<div class="desc">
|
|
998
|
+
Browsers hand out clipboard access only on a secure origin, and
|
|
999
|
+
<code>{insecure.origin}</code> isn't one — so a copy out of an instance's VS Code
|
|
1000
|
+
terminal reaches nothing (codebay's own terminal falls back to a legacy copy that
|
|
1001
|
+
usually still works). Either open codebay through <code>localhost</code> (<code
|
|
1002
|
+
>{insecure.tunnel}</code
|
|
1003
|
+
>), or allow this exact origin under
|
|
1004
|
+
<code>chrome://flags/#unsafely-treat-insecure-origin-as-secure</code>.
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
</div>
|
|
1009
|
+
</section>
|
|
1010
|
+
{/if}
|
|
1011
|
+
|
|
903
1012
|
<section class="card">
|
|
904
1013
|
<form
|
|
905
1014
|
class="row"
|
|
@@ -1182,6 +1291,44 @@
|
|
|
1182
1291
|
</form>
|
|
1183
1292
|
</section>
|
|
1184
1293
|
|
|
1294
|
+
<section class="card">
|
|
1295
|
+
<form
|
|
1296
|
+
class="row"
|
|
1297
|
+
method="POST"
|
|
1298
|
+
action="?/workspaceUploadToggle"
|
|
1299
|
+
{@attach enhance(uploadToggleOpts)}
|
|
1300
|
+
>
|
|
1301
|
+
<div class="label">
|
|
1302
|
+
<Upload size={20} />
|
|
1303
|
+
<div class="text">
|
|
1304
|
+
<div class="name">Drop files into the workspace</div>
|
|
1305
|
+
<div class="desc">
|
|
1306
|
+
Drag-and-drop or paste files and images onto an instance to save them under
|
|
1307
|
+
<code>codebay-inbox/</code> in its workspace (git-excluded). The in-container path is shown
|
|
1308
|
+
so Claude can Read it. Turn off to drop straight into VS Code's explorer instead. Off by
|
|
1309
|
+
default.
|
|
1310
|
+
</div>
|
|
1311
|
+
</div>
|
|
1312
|
+
</div>
|
|
1313
|
+
<label class="switch">
|
|
1314
|
+
<input
|
|
1315
|
+
type="checkbox"
|
|
1316
|
+
name="enabled"
|
|
1317
|
+
checked={workspaceUpload}
|
|
1318
|
+
disabled={savingUpload}
|
|
1319
|
+
onchange={(e) => {
|
|
1320
|
+
workspaceUpload = e.currentTarget.checked;
|
|
1321
|
+
e.currentTarget.form?.requestSubmit();
|
|
1322
|
+
}}
|
|
1323
|
+
/>
|
|
1324
|
+
<span class="track"><span class="thumb"></span></span>
|
|
1325
|
+
</label>
|
|
1326
|
+
</form>
|
|
1327
|
+
{#if uploadError}
|
|
1328
|
+
<div class="sub"><div class="msg error">{uploadError}</div></div>
|
|
1329
|
+
{/if}
|
|
1330
|
+
</section>
|
|
1331
|
+
|
|
1185
1332
|
<section class="card">
|
|
1186
1333
|
<form
|
|
1187
1334
|
class="row image-row"
|
|
@@ -1230,6 +1377,116 @@
|
|
|
1230
1377
|
</form>
|
|
1231
1378
|
</section>
|
|
1232
1379
|
|
|
1380
|
+
<section class="card">
|
|
1381
|
+
<form class="row" method="POST" action="?/mcpToggle" {@attach enhance(mcpToggleOpts)}>
|
|
1382
|
+
<div class="label">
|
|
1383
|
+
<Plug size={20} />
|
|
1384
|
+
<div class="text">
|
|
1385
|
+
<div class="name">MCP server</div>
|
|
1386
|
+
<div class="desc">
|
|
1387
|
+
Expose Codebay to other AI agents over MCP, so they can create sandboxes, run Claude
|
|
1388
|
+
Code in them non-interactively and read back the result. Off by default; the endpoint
|
|
1389
|
+
404s until you turn it on, then authenticates with the bearer token below.
|
|
1390
|
+
</div>
|
|
1391
|
+
</div>
|
|
1392
|
+
</div>
|
|
1393
|
+
<label class="switch">
|
|
1394
|
+
<input
|
|
1395
|
+
type="checkbox"
|
|
1396
|
+
name="enabled"
|
|
1397
|
+
checked={mcp}
|
|
1398
|
+
disabled={savingMcpToggle}
|
|
1399
|
+
onchange={(e) => {
|
|
1400
|
+
mcp = e.currentTarget.checked;
|
|
1401
|
+
e.currentTarget.form?.requestSubmit();
|
|
1402
|
+
}}
|
|
1403
|
+
/>
|
|
1404
|
+
<span class="track"><span class="thumb"></span></span>
|
|
1405
|
+
</label>
|
|
1406
|
+
</form>
|
|
1407
|
+
{#if mcpToggleError}
|
|
1408
|
+
<div class="sub"><div class="msg error">{mcpToggleError}</div></div>
|
|
1409
|
+
{/if}
|
|
1410
|
+
|
|
1411
|
+
{#if mcp}
|
|
1412
|
+
<div class="row divided">
|
|
1413
|
+
<div class="label">
|
|
1414
|
+
<div class="text">
|
|
1415
|
+
<div class="name">Token</div>
|
|
1416
|
+
<div class="desc">
|
|
1417
|
+
Anything holding this token can create containers and run agents with your GitHub
|
|
1418
|
+
and Claude credentials. Treat it like a password.
|
|
1419
|
+
</div>
|
|
1420
|
+
</div>
|
|
1421
|
+
</div>
|
|
1422
|
+
<div class="model-fields">
|
|
1423
|
+
<label class="model-row">
|
|
1424
|
+
<span class="model-label">Bearer</span>
|
|
1425
|
+
<input
|
|
1426
|
+
type="text"
|
|
1427
|
+
class="image-input"
|
|
1428
|
+
value={mcpTokenValue}
|
|
1429
|
+
readonly
|
|
1430
|
+
spellcheck="false"
|
|
1431
|
+
onfocus={(e) => e.currentTarget.select()}
|
|
1432
|
+
/>
|
|
1433
|
+
</label>
|
|
1434
|
+
<div class="mcp-actions">
|
|
1435
|
+
<Button type="button" onclick={() => copyMcp('token', mcpTokenValue)}>
|
|
1436
|
+
{mcpCopied === 'token' ? 'Copied' : 'Copy token'}
|
|
1437
|
+
</Button>
|
|
1438
|
+
<Button type="button" onclick={() => copyMcp('command', mcpCommand)}>
|
|
1439
|
+
{mcpCopied === 'command' ? 'Copied' : 'Copy claude mcp add'}
|
|
1440
|
+
</Button>
|
|
1441
|
+
<form method="POST" action="?/mcpRegenerateToken" {@attach enhance(mcpTokenOpts)}>
|
|
1442
|
+
<Button type="submit" disabled={savingMcpToken}>
|
|
1443
|
+
{savingMcpToken ? 'Regenerating…' : 'Regenerate'}
|
|
1444
|
+
</Button>
|
|
1445
|
+
</form>
|
|
1446
|
+
</div>
|
|
1447
|
+
<code class="mcp-command">{mcpCommand}</code>
|
|
1448
|
+
{#if mcpTokenError}
|
|
1449
|
+
<div class="msg error">{mcpTokenError}</div>
|
|
1450
|
+
{/if}
|
|
1451
|
+
</div>
|
|
1452
|
+
</div>
|
|
1453
|
+
|
|
1454
|
+
<form
|
|
1455
|
+
class="row divided"
|
|
1456
|
+
method="POST"
|
|
1457
|
+
action="?/mcpPrAttributionToggle"
|
|
1458
|
+
{@attach enhance(mcpAttributionOpts)}
|
|
1459
|
+
>
|
|
1460
|
+
<div class="label">
|
|
1461
|
+
<div class="text">
|
|
1462
|
+
<div class="name">Credit Codebay on pull requests</div>
|
|
1463
|
+
<div class="desc">
|
|
1464
|
+
Append a one-line footer to the body of every pull request an agent opens through
|
|
1465
|
+
<code>create_pr</code>. Off by default — an attribution line on your PRs is yours to
|
|
1466
|
+
opt into.
|
|
1467
|
+
</div>
|
|
1468
|
+
</div>
|
|
1469
|
+
</div>
|
|
1470
|
+
<label class="switch">
|
|
1471
|
+
<input
|
|
1472
|
+
type="checkbox"
|
|
1473
|
+
name="enabled"
|
|
1474
|
+
checked={mcpAttribution}
|
|
1475
|
+
disabled={savingMcpAttribution}
|
|
1476
|
+
onchange={(e) => {
|
|
1477
|
+
mcpAttribution = e.currentTarget.checked;
|
|
1478
|
+
e.currentTarget.form?.requestSubmit();
|
|
1479
|
+
}}
|
|
1480
|
+
/>
|
|
1481
|
+
<span class="track"><span class="thumb"></span></span>
|
|
1482
|
+
</label>
|
|
1483
|
+
</form>
|
|
1484
|
+
{#if mcpAttributionError}
|
|
1485
|
+
<div class="sub"><div class="msg error">{mcpAttributionError}</div></div>
|
|
1486
|
+
{/if}
|
|
1487
|
+
{/if}
|
|
1488
|
+
</section>
|
|
1489
|
+
|
|
1233
1490
|
<section class="card">
|
|
1234
1491
|
<form
|
|
1235
1492
|
class="row"
|
|
@@ -2693,6 +2950,22 @@
|
|
|
2693
2950
|
flex: 1;
|
|
2694
2951
|
min-width: 220px;
|
|
2695
2952
|
}
|
|
2953
|
+
.mcp-actions {
|
|
2954
|
+
display: flex;
|
|
2955
|
+
flex-wrap: wrap;
|
|
2956
|
+
gap: 6px;
|
|
2957
|
+
}
|
|
2958
|
+
/* The registration line is long and unbreakable, so it scrolls rather than widening the card. */
|
|
2959
|
+
.mcp-command {
|
|
2960
|
+
display: block;
|
|
2961
|
+
overflow-x: auto;
|
|
2962
|
+
white-space: pre;
|
|
2963
|
+
padding: 6px 8px;
|
|
2964
|
+
border: 1px solid var(--edge);
|
|
2965
|
+
background: var(--panel);
|
|
2966
|
+
font-size: 11px;
|
|
2967
|
+
line-height: 1.5;
|
|
2968
|
+
}
|
|
2696
2969
|
.model-row {
|
|
2697
2970
|
display: flex;
|
|
2698
2971
|
align-items: center;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { Terminal } from '@xterm/xterm';
|
|
5
5
|
import type { FitAddon } from '@xterm/addon-fit';
|
|
6
6
|
import RotateCw from '@lucide/svelte/icons/rotate-cw';
|
|
7
|
+
import { decodeOsc52 } from '../lib/osc52.ts';
|
|
7
8
|
|
|
8
9
|
let {
|
|
9
10
|
id,
|
|
@@ -118,6 +119,40 @@
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The Clipboard API is the only path to the *host* clipboard, but it needs a secure context and
|
|
124
|
+
* (outside a user gesture) can still reject — hence the legacy textarea fallback behind it.
|
|
125
|
+
*/
|
|
126
|
+
async function toClipboard(text: string) {
|
|
127
|
+
if (!text) return;
|
|
128
|
+
try {
|
|
129
|
+
await navigator.clipboard.writeText(text);
|
|
130
|
+
return;
|
|
131
|
+
} catch {
|
|
132
|
+
/* no Clipboard API, or a write the browser refused — fall through */
|
|
133
|
+
}
|
|
134
|
+
const ta = document.createElement('textarea');
|
|
135
|
+
ta.value = text;
|
|
136
|
+
ta.setAttribute('aria-hidden', 'true');
|
|
137
|
+
ta.style.position = 'fixed';
|
|
138
|
+
ta.style.opacity = '0';
|
|
139
|
+
document.body.appendChild(ta);
|
|
140
|
+
ta.select();
|
|
141
|
+
try {
|
|
142
|
+
document.execCommand('copy');
|
|
143
|
+
} catch {
|
|
144
|
+
/* nothing left to try */
|
|
145
|
+
}
|
|
146
|
+
ta.remove();
|
|
147
|
+
if (active && focus) term?.focus();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A finished drag is both the moment the selection exists and the gesture the clipboard needs. */
|
|
151
|
+
function copySelection() {
|
|
152
|
+
const sel = term?.getSelection();
|
|
153
|
+
if (sel) void toClipboard(sel);
|
|
154
|
+
}
|
|
155
|
+
|
|
121
156
|
function fitSafe() {
|
|
122
157
|
// A hidden pane (display:none) has no size, so fitting there would throw or size to 0.
|
|
123
158
|
if (el?.clientWidth > 0 && el?.clientHeight > 0) {
|
|
@@ -165,6 +200,21 @@
|
|
|
165
200
|
term.open(el);
|
|
166
201
|
applyTheme();
|
|
167
202
|
fitSafe();
|
|
203
|
+
// Without this, tmux's `set-clipboard on` writes land in a handler xterm doesn't have.
|
|
204
|
+
term.parser.registerOscHandler(52, (data) => {
|
|
205
|
+
const text = decodeOsc52(data);
|
|
206
|
+
if (text !== null) void toClipboard(text);
|
|
207
|
+
return true;
|
|
208
|
+
});
|
|
209
|
+
// Chrome keeps Ctrl+Shift+C for devtools, so a browser terminal needs its own copy key.
|
|
210
|
+
term.attachCustomKeyEventHandler((e) => {
|
|
211
|
+
if (e.type !== 'keydown' || !term?.hasSelection()) return true;
|
|
212
|
+
const isCopy =
|
|
213
|
+
e.ctrlKey && (e.key === 'Insert' || (e.altKey && e.key.toLowerCase() === 'c'));
|
|
214
|
+
if (!isCopy) return true;
|
|
215
|
+
copySelection();
|
|
216
|
+
return false;
|
|
217
|
+
});
|
|
168
218
|
term.onData((d) => send(CMD_INPUT + d));
|
|
169
219
|
term.onResize(({ cols, rows }) => send(CMD_RESIZE + JSON.stringify({ columns: cols, rows })));
|
|
170
220
|
// The theme cookie flips data-theme on <html>; re-derive xterm's colors when it does.
|
|
@@ -182,8 +232,12 @@
|
|
|
182
232
|
const sizeObs = new ResizeObserver(() => fitSafe());
|
|
183
233
|
sizeObs.observe(el);
|
|
184
234
|
|
|
235
|
+
// Shift+drag selects in xterm even while tmux owns the mouse; copy it before a repaint clears it.
|
|
236
|
+
el.addEventListener('mouseup', copySelection);
|
|
237
|
+
|
|
185
238
|
return () => {
|
|
186
239
|
disposed = true;
|
|
240
|
+
el.removeEventListener('mouseup', copySelection);
|
|
187
241
|
sizeObs.disconnect();
|
|
188
242
|
themeObs?.disconnect();
|
|
189
243
|
scheme.removeEventListener('change', onScheme);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import SettingsCog from './SettingsCog.svelte';
|
|
15
15
|
import TopBar from './TopBar.svelte';
|
|
16
16
|
import IdeLoader from './IdeLoader.svelte';
|
|
17
|
+
import DropZone from './DropZone.svelte';
|
|
17
18
|
import HealthBox from './HealthBox.svelte';
|
|
18
19
|
import BranchBox from './BranchBox.svelte';
|
|
19
20
|
import PortsBox from './PortsBox.svelte';
|
|
@@ -183,7 +184,8 @@
|
|
|
183
184
|
terminal_split: 0,
|
|
184
185
|
git_branch: cardBranch ? branchName : null,
|
|
185
186
|
attention: cardAttention,
|
|
186
|
-
forwarded_ports: demoPorts
|
|
187
|
+
forwarded_ports: demoPorts,
|
|
188
|
+
active_run: null
|
|
187
189
|
});
|
|
188
190
|
|
|
189
191
|
const statuses: Instance['status'][] = ['creating', 'running', 'stopped', 'error'];
|
|
@@ -212,6 +214,8 @@
|
|
|
212
214
|
let tabEditingName = $state('');
|
|
213
215
|
// Renames are applied for real, so the inline editor is inspectable end to end.
|
|
214
216
|
let tabRenames = $state<Record<string, string>>({});
|
|
217
|
+
// No container behind the demo, so a stopped tab just stays disabled.
|
|
218
|
+
let tabStopping = $state<string[]>([]);
|
|
215
219
|
|
|
216
220
|
const demoTabs = $derived(
|
|
217
221
|
Array.from({ length: tabCount }, (_, i): Instance => {
|
|
@@ -234,7 +238,8 @@
|
|
|
234
238
|
terminal_split: 0,
|
|
235
239
|
git_branch: 'main',
|
|
236
240
|
attention: null,
|
|
237
|
-
forwarded_ports: []
|
|
241
|
+
forwarded_ports: [],
|
|
242
|
+
active_run: null
|
|
238
243
|
};
|
|
239
244
|
})
|
|
240
245
|
);
|
|
@@ -461,6 +466,15 @@
|
|
|
461
466
|
{/snippet}
|
|
462
467
|
</ComponentDemo>
|
|
463
468
|
|
|
469
|
+
<ComponentDemo
|
|
470
|
+
title="DropZone"
|
|
471
|
+
note="Shown while a file drag is over an instance; the page decides when."
|
|
472
|
+
>
|
|
473
|
+
<div class="loader-stage">
|
|
474
|
+
<DropZone />
|
|
475
|
+
</div>
|
|
476
|
+
</ComponentDemo>
|
|
477
|
+
|
|
464
478
|
<ComponentDemo title="HealthBox">
|
|
465
479
|
<HealthBox
|
|
466
480
|
health={demoHealth}
|
|
@@ -553,6 +567,12 @@
|
|
|
553
567
|
bind:editingName={tabEditingName}
|
|
554
568
|
onreload={() => toast('Reload editor (demo)')}
|
|
555
569
|
onselect={(id) => (tabActive = demoTabs.findIndex((t) => t.id === id))}
|
|
570
|
+
stopping={tabStopping}
|
|
571
|
+
onstop={(id) => {
|
|
572
|
+
tabStopping = [...tabStopping, id];
|
|
573
|
+
toast(`Stop ${id} (demo)`);
|
|
574
|
+
}}
|
|
575
|
+
ondropfiles={(id, files) => toast(`Drop ${files.length} file(s) on ${id} (demo)`)}
|
|
556
576
|
onstartrename={(inst) => {
|
|
557
577
|
tabEditingId = inst.id;
|
|
558
578
|
tabEditingName = inst.name;
|
|
@@ -675,7 +695,7 @@
|
|
|
675
695
|
.showcase :global(.fields input[type='checkbox']) {
|
|
676
696
|
accent-color: var(--ink);
|
|
677
697
|
}
|
|
678
|
-
/* IdeLoader
|
|
698
|
+
/* IdeLoader and DropZone are inset:0 overlays, so give them a sized, positioned stage. */
|
|
679
699
|
.loader-stage {
|
|
680
700
|
position: relative;
|
|
681
701
|
width: 100%;
|
|
@@ -49,7 +49,7 @@ const CHECK_SCRIPT = 'command -v claude >/dev/null 2>&1 && echo 1 || echo 0';
|
|
|
49
49
|
*/
|
|
50
50
|
export const claudeCodeInstall: Injection = {
|
|
51
51
|
id: 'claude-code-install',
|
|
52
|
-
label: 'Claude Code',
|
|
52
|
+
label: 'Claude Code installed',
|
|
53
53
|
modes: ['terminal'],
|
|
54
54
|
|
|
55
55
|
async apply(target, log) {
|