codebay 0.8.0 → 0.9.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/import-css/xterm-1yy1hf1g.css +252 -0
- package/.mochi/manifest.json +330 -211
- package/.mochi/svelte-client/HydratableIsland-k6q8esj4.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-sg96rp17.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-92hepzby.js +14 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-p3f649gk.js +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-dzmgmv78.js +29 -0
- package/.mochi/svelte-client/chunk-2j95sv83.js +1 -0
- package/.mochi/svelte-client/chunk-6rbs0mhx.js +1 -0
- package/.mochi/svelte-client/chunk-7wkvn2vx.js +2 -0
- package/.mochi/svelte-client/chunk-a2psjmrm.js +2 -0
- package/.mochi/svelte-client/chunk-m497q88s.js +2 -0
- package/.mochi/svelte-client/{chunk-fqg2qzj8.js → chunk-m4fzsd5b.js} +1 -1
- package/.mochi/svelte-client/chunk-mpv3qx28.js +36 -0
- package/.mochi/svelte-client/chunk-rs41by6w.js +2 -0
- package/.mochi/svelte-client/{chunk-qj7zy2d9.js → chunk-v65n8th6.js} +4 -4
- package/.mochi/svelte-compile/{App-6rcf0d74.server.js → App-7vjcp38v.server.js} +368 -37
- package/.mochi/svelte-compile/{Avatars-pzyz0tv3.server.js → Avatars-e2cfc95t.server.js} +130 -25
- package/.mochi/svelte-compile/{ClientStats-z3tn9ad6.server.js → ClientStats-4cphmrnp.server.js} +1 -1
- package/.mochi/svelte-compile/{DefaultError-wh77vebf.server.js → DefaultError-je52k17d.server.js} +1 -1
- package/.mochi/svelte-compile/{Instance-49ksrp11.server.js → Instance-h78n2dsv.server.js} +16 -13
- package/.mochi/svelte-compile/{Settings-sz5d6qj7.server.js → Settings-tav2asn7.server.js} +153 -127
- package/.mochi/svelte-compile/{chunk-fpe16ywq.js → chunk-60mpagr9.js} +17 -7
- package/.mochi/svelte-compile/chunk-9nqc2r0y.js +9706 -0
- package/.mochi/svelte-compile/{chunk-6ghzacpg.js → chunk-bpqwf1nw.js} +36 -4
- package/.mochi/svelte-compile/chunk-jx6h1xzp.js +20 -0
- package/.mochi/svelte-compile/{chunk-q8rbesaw.js → chunk-m8ygpyhr.js} +5 -4
- package/.mochi/svelte-compile/chunk-rtdne1m8.js +31 -0
- package/.mochi/svelte-compile/{chunk-we0ggtws.js → chunk-xpt1a7jm.js} +2 -1
- package/.mochi/svelte-css/AvatarEditor-2mdq8ss346drw.css +1 -0
- package/.mochi/svelte-css/{AvatarsView-2ughbzc26luyb.css → AvatarsView-2llo48w0wt0xn.css} +1 -1
- package/.mochi/svelte-css/FolderBrowser-2pxp9z1o7zixk.css +1 -0
- package/.mochi/svelte-css/{IdeBar-2qoqi2ya9zdn3.css → IdeBar-10xodudcx57qr.css} +1 -1
- package/.mochi/svelte-css/{SettingsView-3uycxa8z548m4.css → SettingsView-2qfauz68axro1.css} +1 -1
- package/.mochi/svelte-css/TerminalPane-3bwgk6ulzq6b3.css +1 -0
- package/.mochi/svelte-css/TerminalSplit-2416xfuln89a3.css +1 -0
- package/migrations/0005_instance_mode.sql +1 -0
- package/migrations/0006_terminal_split.sql +1 -0
- package/package.json +3 -1
- package/src/avatars/serialize.ts +35 -6
- package/src/components/AppShell.svelte +46 -8
- package/src/components/Avatar.svelte +3 -2
- package/src/components/AvatarEditor.svelte +96 -19
- package/src/components/AvatarsView.svelte +0 -0
- package/src/components/DashboardView.svelte +22 -7
- package/src/components/FolderBrowser.svelte +88 -8
- package/src/components/HealthBox.svelte +12 -5
- package/src/components/IdeBar.svelte +10 -0
- package/src/components/InstanceCard.svelte +3 -1
- package/src/components/InstanceView.svelte +1 -0
- package/src/components/SettingsView.svelte +108 -0
- package/src/components/TerminalPane.svelte +274 -0
- package/src/components/TerminalSplit.svelte +154 -0
- package/src/container-injections/claude-code-ide-extension.ts +2 -0
- package/src/container-injections/ttyd.ts +49 -0
- package/src/lib/config.server.ts +3 -0
- package/src/lib/db.server.ts +14 -4
- package/src/lib/devcontainer.server.ts +136 -22
- package/src/lib/health.server.ts +1 -1
- package/src/lib/injections.server.ts +15 -5
- package/src/lib/instances.server.ts +64 -10
- package/src/lib/proxy.server.ts +87 -11
- package/src/pages/App.svelte +5 -3
- package/src/pages/Settings.svelte +4 -0
- package/src/routes.ts +55 -6
- package/src/types.ts +18 -0
- package/.mochi/svelte-client/HydratableIsland-qyjx1d98.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-fkna7bj2.js +0 -2
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-89wtkb86.js +0 -13
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-eamnbyqk.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-kdra4mcb.js +0 -28
- package/.mochi/svelte-client/chunk-330mrspw.js +0 -2
- package/.mochi/svelte-client/chunk-4jgkak13.js +0 -1
- package/.mochi/svelte-client/chunk-k5m4kt20.js +0 -2
- package/.mochi/svelte-client/chunk-rpdphch7.js +0 -2
- package/.mochi/svelte-compile/chunk-bacb4b0r.js +0 -14
- package/.mochi/svelte-css/AvatarEditor-2ewwtejaml4e2.css +0 -1
- package/.mochi/svelte-css/FolderBrowser-1x428zb3f2kti.css +0 -1
- /package/.mochi/svelte-compile/{chunk-btmcxhqy.js → chunk-brjv9pyd.js} +0 -0
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import Copy from '@lucide/svelte/icons/copy';
|
|
3
3
|
import Eraser from '@lucide/svelte/icons/eraser';
|
|
4
4
|
import Contrast from '@lucide/svelte/icons/contrast';
|
|
5
|
+
import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
|
|
5
6
|
import ExternalLink from '@lucide/svelte/icons/external-link';
|
|
7
|
+
import X from '@lucide/svelte/icons/x';
|
|
6
8
|
import toast from 'svelte-french-toast';
|
|
7
|
-
import { ROWS, COLS, ON, OFF, GRAY, type AvatarArt } from '../avatars/types.ts';
|
|
9
|
+
import { ROWS, COLS, ON, OFF, GRAY, decode, type AvatarArt } from '../avatars/types.ts';
|
|
8
10
|
import { MODES, nextValue, type Mode } from '../avatars/editor.ts';
|
|
9
11
|
import {
|
|
10
12
|
cellsToPixels,
|
|
@@ -16,13 +18,26 @@
|
|
|
16
18
|
import Button from './Button.svelte';
|
|
17
19
|
|
|
18
20
|
// Rolled by the route, not here, so hydration doesn't pick a different word.
|
|
19
|
-
|
|
21
|
+
// `editing` seeds the canvas with an existing sprite (a redraw); null is a fresh draw.
|
|
22
|
+
let {
|
|
23
|
+
namePlaceholder,
|
|
24
|
+
editing = null,
|
|
25
|
+
oncancelEdit
|
|
26
|
+
}: {
|
|
27
|
+
namePlaceholder: string;
|
|
28
|
+
editing?: AvatarArt | null;
|
|
29
|
+
oncancelEdit?: () => void;
|
|
30
|
+
} = $props();
|
|
20
31
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
let avatarName = $state('');
|
|
32
|
+
// A redraw keeps the sprite's name (it's the catalog key), so the name is fixed then.
|
|
33
|
+
const isEdit = $derived(editing != null);
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
// Same shape `decode()` produces, so the two round-trip. Seeded once — the parent
|
|
36
|
+
// remounts this component (keyed) when it switches which sprite is being edited.
|
|
37
|
+
let cells = $state<number[]>(editing ? decode(editing) : Array(ROWS * COLS).fill(OFF));
|
|
38
|
+
let avatarName = $state(editing?.name ?? '');
|
|
39
|
+
|
|
40
|
+
const slug = $derived(editing ? editing.name : normalizeName(avatarName));
|
|
26
41
|
const art = $derived<AvatarArt>({ name: slug || 'my-avatar', pixels: cellsToPixels(cells) });
|
|
27
42
|
// Contributing needs a name and at least one lit pixel — a blank sprite is no sprite.
|
|
28
43
|
const canContribute = $derived(slug.length > 0 && cells.some((c) => c !== OFF));
|
|
@@ -89,6 +104,11 @@
|
|
|
89
104
|
cells = Array(ROWS * COLS).fill(OFF);
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
// Only meaningful for a redraw: restore the sprite the edit started from.
|
|
108
|
+
function reset() {
|
|
109
|
+
if (editing) cells = decode(editing);
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
// Only swaps off↔on; gray sits in the middle and is left untouched.
|
|
93
113
|
function invert() {
|
|
94
114
|
cells = cells.map((c) => (c === ON ? OFF : c === OFF ? ON : c));
|
|
@@ -106,7 +126,13 @@
|
|
|
106
126
|
|
|
107
127
|
<section class="editor">
|
|
108
128
|
<div class="head">
|
|
109
|
-
<h2>Draw your own avatar</h2>
|
|
129
|
+
<h2>{isEdit ? `Redraw “${editing!.name}”` : 'Draw your own avatar'}</h2>
|
|
130
|
+
{#if isEdit}
|
|
131
|
+
<button type="button" class="head-cancel" onclick={() => oncancelEdit?.()}>
|
|
132
|
+
<X size={14} />
|
|
133
|
+
<span>New drawing</span>
|
|
134
|
+
</button>
|
|
135
|
+
{/if}
|
|
110
136
|
</div>
|
|
111
137
|
|
|
112
138
|
<div class="body">
|
|
@@ -141,18 +167,33 @@
|
|
|
141
167
|
</div>
|
|
142
168
|
</div>
|
|
143
169
|
|
|
170
|
+
{#if isEdit}
|
|
171
|
+
<!-- The AI-generated original, so the redraw can be compared against it at a glance. -->
|
|
172
|
+
<div class="preview before">
|
|
173
|
+
<div class="preview-label">Before</div>
|
|
174
|
+
<div class="preview-row">
|
|
175
|
+
<Avatar art={editing!} name={editing!.name} scale={3} />
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
144
180
|
<label class="name-field">
|
|
145
181
|
<span class="preview-label">Name</span>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
{#if isEdit}
|
|
183
|
+
<!-- The name is the catalog key; a redraw keeps it, so it's shown read-only. -->
|
|
184
|
+
<input type="text" value={editing!.name} readonly aria-readonly="true" />
|
|
185
|
+
{:else}
|
|
186
|
+
<input
|
|
187
|
+
type="text"
|
|
188
|
+
bind:value={avatarName}
|
|
189
|
+
placeholder={namePlaceholder}
|
|
190
|
+
spellcheck="false"
|
|
191
|
+
autocapitalize="off"
|
|
192
|
+
autocorrect="off"
|
|
193
|
+
autocomplete="off"
|
|
194
|
+
maxlength="24"
|
|
195
|
+
/>
|
|
196
|
+
{/if}
|
|
156
197
|
</label>
|
|
157
198
|
|
|
158
199
|
<div class="mode-field">
|
|
@@ -177,6 +218,9 @@
|
|
|
177
218
|
</div>
|
|
178
219
|
|
|
179
220
|
<div class="tools">
|
|
221
|
+
{#if isEdit}
|
|
222
|
+
<Button size="sm" icon={RotateCcw} onclick={reset}>Reset</Button>
|
|
223
|
+
{/if}
|
|
180
224
|
<Button size="sm" icon={Eraser} onclick={clear}>Clear</Button>
|
|
181
225
|
<Button size="sm" icon={Contrast} onclick={invert}>Invert</Button>
|
|
182
226
|
</div>
|
|
@@ -184,7 +228,11 @@
|
|
|
184
228
|
</div>
|
|
185
229
|
|
|
186
230
|
<div class="foot">
|
|
187
|
-
<span class="hint">
|
|
231
|
+
<span class="hint">
|
|
232
|
+
{isEdit
|
|
233
|
+
? 'Redraw it, then open an issue to replace the AI-generated original.'
|
|
234
|
+
: 'Draw something, then open an issue to get it into the official set.'}
|
|
235
|
+
</span>
|
|
188
236
|
<div class="actions">
|
|
189
237
|
<Button size="sm" icon={Copy} disabled={!canContribute} onclick={copyModule}>Copy</Button>
|
|
190
238
|
<Button
|
|
@@ -192,7 +240,7 @@
|
|
|
192
240
|
variant="primary"
|
|
193
241
|
icon={ExternalLink}
|
|
194
242
|
disabled={!canContribute}
|
|
195
|
-
href={toIssueUrl(art)}
|
|
243
|
+
href={toIssueUrl(art, isEdit ? 'edit' : 'create')}
|
|
196
244
|
target="_blank"
|
|
197
245
|
rel="noopener"
|
|
198
246
|
>
|
|
@@ -214,6 +262,8 @@
|
|
|
214
262
|
.head {
|
|
215
263
|
display: flex;
|
|
216
264
|
align-items: center;
|
|
265
|
+
justify-content: space-between;
|
|
266
|
+
gap: 12px;
|
|
217
267
|
padding: 14px 18px;
|
|
218
268
|
border-bottom: 1px solid var(--ink);
|
|
219
269
|
background: var(--ink);
|
|
@@ -227,6 +277,29 @@
|
|
|
227
277
|
letter-spacing: 0.06em;
|
|
228
278
|
color: var(--bg);
|
|
229
279
|
}
|
|
280
|
+
.head-cancel {
|
|
281
|
+
display: inline-flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 6px;
|
|
284
|
+
flex: none;
|
|
285
|
+
padding: 5px 10px;
|
|
286
|
+
border: 1px solid var(--bg);
|
|
287
|
+
background: transparent;
|
|
288
|
+
color: var(--bg);
|
|
289
|
+
font-family: var(--font-mono);
|
|
290
|
+
font-size: 11px;
|
|
291
|
+
text-transform: uppercase;
|
|
292
|
+
letter-spacing: 0.06em;
|
|
293
|
+
cursor: pointer;
|
|
294
|
+
}
|
|
295
|
+
.head-cancel:hover {
|
|
296
|
+
background: var(--bg);
|
|
297
|
+
color: var(--ink);
|
|
298
|
+
}
|
|
299
|
+
.head-cancel:focus-visible {
|
|
300
|
+
outline: none;
|
|
301
|
+
box-shadow: 0 0 0 2px var(--rule-soft);
|
|
302
|
+
}
|
|
230
303
|
.body {
|
|
231
304
|
display: flex;
|
|
232
305
|
flex-wrap: wrap;
|
|
@@ -308,6 +381,10 @@
|
|
|
308
381
|
.name-field input::placeholder {
|
|
309
382
|
color: var(--ink-faint);
|
|
310
383
|
}
|
|
384
|
+
.name-field input[readonly] {
|
|
385
|
+
color: var(--ink-soft);
|
|
386
|
+
cursor: default;
|
|
387
|
+
}
|
|
311
388
|
.name-field input:focus {
|
|
312
389
|
outline: none;
|
|
313
390
|
box-shadow: inset 3px 3px 0 var(--rule-soft);
|
|
Binary file
|
|
@@ -13,13 +13,21 @@
|
|
|
13
13
|
preflight,
|
|
14
14
|
instances,
|
|
15
15
|
loaded,
|
|
16
|
-
pet
|
|
17
|
-
|
|
16
|
+
pet,
|
|
17
|
+
filter,
|
|
18
|
+
onFilter
|
|
19
|
+
}: {
|
|
20
|
+
preflight: Preflight;
|
|
21
|
+
instances: Instance[];
|
|
22
|
+
loaded: boolean;
|
|
23
|
+
pet?: AvatarArt;
|
|
24
|
+
filter: InstanceFilter;
|
|
25
|
+
onFilter: (v: InstanceFilter) => void;
|
|
26
|
+
} = $props();
|
|
18
27
|
|
|
19
28
|
type Action = 'start' | 'stop' | 'delete' | 'rebuild';
|
|
20
29
|
|
|
21
30
|
let browserOpen = $state(false);
|
|
22
|
-
let filter = $state<InstanceFilter>('all');
|
|
23
31
|
let creating = $state(false);
|
|
24
32
|
let actionError = $state<string | null>(null);
|
|
25
33
|
let editingId = $state<string | null>(null);
|
|
@@ -51,14 +59,17 @@
|
|
|
51
59
|
pending = rest;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
async function createFrom(
|
|
62
|
+
async function createFrom(
|
|
63
|
+
sourcePath: string,
|
|
64
|
+
opts?: { branch?: string; mode?: Instance['mode'] }
|
|
65
|
+
) {
|
|
55
66
|
browserOpen = false;
|
|
56
67
|
creating = true;
|
|
57
68
|
actionError = null;
|
|
58
69
|
try {
|
|
59
70
|
await apiPost(
|
|
60
71
|
'/api/instances',
|
|
61
|
-
{ sourcePath, branch: opts?.branch },
|
|
72
|
+
{ sourcePath, branch: opts?.branch, mode: opts?.mode },
|
|
62
73
|
'Failed to create instance'
|
|
63
74
|
);
|
|
64
75
|
// The live stream delivers the new instance.
|
|
@@ -136,7 +147,7 @@
|
|
|
136
147
|
{ready}
|
|
137
148
|
{creating}
|
|
138
149
|
{filter}
|
|
139
|
-
onFilter
|
|
150
|
+
{onFilter}
|
|
140
151
|
onNew={() => (browserOpen = true)}
|
|
141
152
|
onDeleteAll={deleteAll}
|
|
142
153
|
/>
|
|
@@ -185,7 +196,11 @@
|
|
|
185
196
|
</main>
|
|
186
197
|
|
|
187
198
|
{#if browserOpen}
|
|
188
|
-
<FolderBrowser
|
|
199
|
+
<FolderBrowser
|
|
200
|
+
onpick={createFrom}
|
|
201
|
+
defaultMode={preflight.defaultMode}
|
|
202
|
+
onclose={() => (browserOpen = false)}
|
|
203
|
+
/>
|
|
189
204
|
{/if}
|
|
190
205
|
|
|
191
206
|
<style>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tick } from 'svelte';
|
|
3
|
-
import type { BrowseResult, FolderHistoryEntry } from '../types.ts';
|
|
3
|
+
import type { BrowseResult, FolderHistoryEntry, InstanceMode } from '../types.ts';
|
|
4
4
|
import { isRepoUrl } from '../lib/repo-url.ts';
|
|
5
|
+
import Terminal from '@lucide/svelte/icons/terminal';
|
|
6
|
+
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
|
|
5
7
|
import X from '@lucide/svelte/icons/x';
|
|
6
8
|
import FolderClock from '@lucide/svelte/icons/folder-clock';
|
|
7
9
|
import GitBranch from '@lucide/svelte/icons/git-branch';
|
|
@@ -12,9 +14,18 @@
|
|
|
12
14
|
|
|
13
15
|
let {
|
|
14
16
|
onpick,
|
|
15
|
-
onclose
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
onclose,
|
|
18
|
+
defaultMode = 'ide'
|
|
19
|
+
}: {
|
|
20
|
+
onpick: (source: string, opts?: { branch?: string; mode?: InstanceMode }) => void;
|
|
21
|
+
onclose: () => void;
|
|
22
|
+
defaultMode?: InstanceMode;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
// Follows the global default live — settings opens in its own popup, so the toggle there can
|
|
26
|
+
// move while this picker is open. An explicit choice here wins and is never yanked back.
|
|
27
|
+
let modeOverride = $state<InstanceMode | null>(null);
|
|
28
|
+
const mode = $derived(modeOverride ?? defaultMode);
|
|
18
29
|
|
|
19
30
|
let result = $state<BrowseResult | null>(null);
|
|
20
31
|
let loading = $state(true);
|
|
@@ -30,7 +41,7 @@
|
|
|
30
41
|
|
|
31
42
|
function cloneRepoUrl() {
|
|
32
43
|
if (!repoValid) return;
|
|
33
|
-
onpick(repoUrl.trim(), { branch: repoBranch.trim() || undefined });
|
|
44
|
+
onpick(repoUrl.trim(), { branch: repoBranch.trim() || undefined, mode });
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
const filtered = $derived(
|
|
@@ -103,6 +114,32 @@
|
|
|
103
114
|
<button class="x" onclick={onclose} aria-label="Close"><X size={16} /></button>
|
|
104
115
|
</div>
|
|
105
116
|
|
|
117
|
+
<div class="mode">
|
|
118
|
+
<div class="mode-label">Editor</div>
|
|
119
|
+
<div class="mode-toggle" role="group" aria-label="Editor mode">
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
class="mode-btn"
|
|
123
|
+
class:active={mode === 'ide'}
|
|
124
|
+
aria-pressed={mode === 'ide'}
|
|
125
|
+
onclick={() => (modeOverride = 'ide')}
|
|
126
|
+
>
|
|
127
|
+
<LayoutTemplate size={15} />
|
|
128
|
+
Full IDE
|
|
129
|
+
</button>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
class="mode-btn"
|
|
133
|
+
class:active={mode === 'terminal'}
|
|
134
|
+
aria-pressed={mode === 'terminal'}
|
|
135
|
+
onclick={() => (modeOverride = 'terminal')}
|
|
136
|
+
>
|
|
137
|
+
<Terminal size={15} />
|
|
138
|
+
Terminal
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
106
143
|
<div class="clone">
|
|
107
144
|
<div class="clone-label">Clone a Git repository</div>
|
|
108
145
|
<div class="clone-row">
|
|
@@ -147,7 +184,7 @@
|
|
|
147
184
|
<div class="recent-label">Recent</div>
|
|
148
185
|
{#each shownHistory as entry (entry.source_path)}
|
|
149
186
|
<div class="recent-row">
|
|
150
|
-
<button class="recent-pick" onclick={() => onpick(entry.source_path)}>
|
|
187
|
+
<button class="recent-pick" onclick={() => onpick(entry.source_path, { mode })}>
|
|
151
188
|
<span class="icon">
|
|
152
189
|
{#if isRepoUrl(entry.source_path)}<GitBranch size={16} />{:else}<FolderClock
|
|
153
190
|
size={16}
|
|
@@ -221,7 +258,7 @@
|
|
|
221
258
|
<span class="ename">{entry.name}</span>
|
|
222
259
|
{#if entry.hasDevcontainer}<span class="badge">devcontainer</span>{/if}
|
|
223
260
|
</button>
|
|
224
|
-
<button class="pick-inline" onclick={() => onpick(entry.path)}>Select</button>
|
|
261
|
+
<button class="pick-inline" onclick={() => onpick(entry.path, { mode })}>Select</button>
|
|
225
262
|
</div>
|
|
226
263
|
{/each}
|
|
227
264
|
{/if}
|
|
@@ -239,7 +276,11 @@
|
|
|
239
276
|
|
|
240
277
|
<div class="foot">
|
|
241
278
|
<span class="hint">Browse to a folder, then select this folder or any subfolder.</span>
|
|
242
|
-
<button
|
|
279
|
+
<button
|
|
280
|
+
class="primary"
|
|
281
|
+
disabled={!result}
|
|
282
|
+
onclick={() => result && onpick(result.path, { mode })}
|
|
283
|
+
>
|
|
243
284
|
Select this folder
|
|
244
285
|
</button>
|
|
245
286
|
</div>
|
|
@@ -293,6 +334,45 @@
|
|
|
293
334
|
.x:hover {
|
|
294
335
|
opacity: 0.7;
|
|
295
336
|
}
|
|
337
|
+
.mode {
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 12px;
|
|
341
|
+
padding: 12px 18px;
|
|
342
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
343
|
+
}
|
|
344
|
+
.mode-label {
|
|
345
|
+
font-size: 11px;
|
|
346
|
+
text-transform: uppercase;
|
|
347
|
+
letter-spacing: 0.06em;
|
|
348
|
+
color: var(--ink-faint);
|
|
349
|
+
}
|
|
350
|
+
.mode-toggle {
|
|
351
|
+
display: flex;
|
|
352
|
+
border: 1px solid var(--ink);
|
|
353
|
+
}
|
|
354
|
+
.mode-btn {
|
|
355
|
+
display: inline-flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
gap: 6px;
|
|
358
|
+
font-family: var(--font-mono);
|
|
359
|
+
font-weight: 700;
|
|
360
|
+
font-size: 12px;
|
|
361
|
+
text-transform: uppercase;
|
|
362
|
+
letter-spacing: 0.05em;
|
|
363
|
+
padding: 7px 14px;
|
|
364
|
+
border: none;
|
|
365
|
+
background: var(--bg);
|
|
366
|
+
color: var(--ink);
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
}
|
|
369
|
+
.mode-btn + .mode-btn {
|
|
370
|
+
border-left: 1px solid var(--ink);
|
|
371
|
+
}
|
|
372
|
+
.mode-btn.active {
|
|
373
|
+
background: var(--ink);
|
|
374
|
+
color: var(--bg);
|
|
375
|
+
}
|
|
296
376
|
.clone {
|
|
297
377
|
padding: 12px 18px;
|
|
298
378
|
border-bottom: 1px solid var(--rule-soft);
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Check from '@lucide/svelte/icons/check';
|
|
3
3
|
import X from '@lucide/svelte/icons/x';
|
|
4
|
-
import type { InstanceHealth } from '../types.ts';
|
|
4
|
+
import type { InstanceHealth, InstanceMode } from '../types.ts';
|
|
5
5
|
import Skeleton from './Skeleton.svelte';
|
|
6
6
|
|
|
7
|
-
// Intrinsic to every instance, independent of any injection.
|
|
8
|
-
const FIXED_CHECKS = ['Container running', 'Code-server reachable'] as const;
|
|
9
|
-
|
|
10
7
|
let {
|
|
11
8
|
health = null,
|
|
12
9
|
lastFetchedAt = null,
|
|
13
10
|
active = true,
|
|
14
|
-
injectionChecks = 0
|
|
11
|
+
injectionChecks = 0,
|
|
12
|
+
mode = 'ide'
|
|
15
13
|
}: {
|
|
16
14
|
/** Latest health snapshot, or null while the first check is pending. */
|
|
17
15
|
health?: InstanceHealth | null;
|
|
@@ -21,8 +19,17 @@
|
|
|
21
19
|
active?: boolean;
|
|
22
20
|
/** Registry-derived, so the skeleton renders one row per real check. */
|
|
23
21
|
injectionChecks?: number;
|
|
22
|
+
/** Terminal instances serve ttyd, not code-server, so the reachability label differs. */
|
|
23
|
+
mode?: InstanceMode;
|
|
24
24
|
} = $props();
|
|
25
25
|
|
|
26
|
+
// Intrinsic to every instance, independent of any injection. The second label tracks the
|
|
27
|
+
// served surface — the probe underneath is the same host-port reachability check either way.
|
|
28
|
+
const FIXED_CHECKS = $derived([
|
|
29
|
+
'Container running',
|
|
30
|
+
mode === 'terminal' ? 'Terminal reachable' : 'Code-server reachable'
|
|
31
|
+
] as const);
|
|
32
|
+
|
|
26
33
|
// The value word stays uniform because the tick/cross icon already conveys pass vs fail.
|
|
27
34
|
const checks = $derived.by((): { label: string; ok: boolean; value: string }[] => {
|
|
28
35
|
if (!health) return [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { type Instance } from '../types.ts';
|
|
3
3
|
import Settings from '@lucide/svelte/icons/settings';
|
|
4
|
+
import Terminal from '@lucide/svelte/icons/terminal';
|
|
4
5
|
import Avatar from './Avatar.svelte';
|
|
5
6
|
import AppBar from './AppBar.svelte';
|
|
6
7
|
import { withPopupMarker } from '../lib/popup-nav.ts';
|
|
@@ -64,6 +65,9 @@
|
|
|
64
65
|
>
|
|
65
66
|
<Avatar id={inst.id} name={inst.name} scale={4} />
|
|
66
67
|
<span class="tab-name">{inst.name}</span>
|
|
68
|
+
{#if inst.mode === 'terminal'}
|
|
69
|
+
<span class="tab-mode" title="Terminal-only instance"><Terminal size={13} /></span>
|
|
70
|
+
{/if}
|
|
67
71
|
</button>
|
|
68
72
|
{/if}
|
|
69
73
|
</div>
|
|
@@ -169,6 +173,12 @@
|
|
|
169
173
|
overflow: hidden;
|
|
170
174
|
text-overflow: ellipsis;
|
|
171
175
|
}
|
|
176
|
+
.tab-mode {
|
|
177
|
+
display: inline-flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
flex: none;
|
|
180
|
+
opacity: 0.75;
|
|
181
|
+
}
|
|
172
182
|
.tab:not(.active) .tab-label:hover {
|
|
173
183
|
color: var(--ink);
|
|
174
184
|
}
|
|
@@ -94,7 +94,9 @@
|
|
|
94
94
|
{/if}
|
|
95
95
|
<div class="actions">
|
|
96
96
|
{#if instance.status === 'running'}
|
|
97
|
-
<Button variant="primary" size="sm" href={`/ide/${instance.id}`}>
|
|
97
|
+
<Button variant="primary" size="sm" href={`/ide/${instance.id}`}>
|
|
98
|
+
{instance.mode === 'terminal' ? 'Open terminal' : 'Open IDE'}
|
|
99
|
+
</Button>
|
|
98
100
|
<Button size="sm" onclick={() => onact('stop')}>Stop</Button>
|
|
99
101
|
{:else if instance.status === 'stopped' || (instance.status === 'error' && instance.container_id)}
|
|
100
102
|
<Button size="sm" onclick={() => onact('start')}>Start</Button>
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
import CoinButton from './CoinButton.svelte';
|
|
28
28
|
import Avatar from './Avatar.svelte';
|
|
29
29
|
import { avatars, findAvatar, type AvatarArt } from '../avatars/index.ts';
|
|
30
|
+
import type { InstanceMode } from '../types.ts';
|
|
31
|
+
import Terminal from '@lucide/svelte/icons/terminal';
|
|
32
|
+
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
|
|
30
33
|
import { installPopupBackTrap } from '../lib/popup-nav.ts';
|
|
31
34
|
|
|
32
35
|
/** Every settings form action fails with the same `{ error }` shape. */
|
|
@@ -34,6 +37,7 @@
|
|
|
34
37
|
|
|
35
38
|
let {
|
|
36
39
|
pet,
|
|
40
|
+
defaultMode,
|
|
37
41
|
defaultImage,
|
|
38
42
|
builtinImage,
|
|
39
43
|
disableBuildCache,
|
|
@@ -66,6 +70,7 @@
|
|
|
66
70
|
version
|
|
67
71
|
}: {
|
|
68
72
|
pet?: AvatarArt;
|
|
73
|
+
defaultMode: InstanceMode;
|
|
69
74
|
defaultImage: string;
|
|
70
75
|
builtinImage: string;
|
|
71
76
|
disableBuildCache: boolean;
|
|
@@ -172,6 +177,29 @@
|
|
|
172
177
|
};
|
|
173
178
|
}
|
|
174
179
|
|
|
180
|
+
// DB-backed, so it initializes from the prop; a per-instance choice in the picker overrides it.
|
|
181
|
+
// svelte-ignore state_referenced_locally
|
|
182
|
+
let modeChoice = $state<InstanceMode>(defaultMode);
|
|
183
|
+
let savingMode = $state(false);
|
|
184
|
+
let modeError = $state<string | null>(null);
|
|
185
|
+
let modeFormEl: HTMLFormElement | undefined;
|
|
186
|
+
|
|
187
|
+
const defaultModeOpts = saveOpts<{ mode: InstanceMode }>({
|
|
188
|
+
setSaving: (v) => (savingMode = v),
|
|
189
|
+
setError: (v) => (modeError = v),
|
|
190
|
+
setMsg: () => {},
|
|
191
|
+
onSuccess: (data) => {
|
|
192
|
+
if (data?.mode) modeChoice = data.mode;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Set the bound hidden value then resubmit, mirroring the image reset (flushSync before requestSubmit).
|
|
197
|
+
function chooseMode(next: InstanceMode) {
|
|
198
|
+
if (next === modeChoice) return;
|
|
199
|
+
flushSync(() => (modeChoice = next));
|
|
200
|
+
modeFormEl?.requestSubmit();
|
|
201
|
+
}
|
|
202
|
+
|
|
175
203
|
// svelte-ignore state_referenced_locally
|
|
176
204
|
let image = $state(defaultImage);
|
|
177
205
|
let savingImage = $state(false);
|
|
@@ -605,6 +633,56 @@
|
|
|
605
633
|
</AppBar>
|
|
606
634
|
|
|
607
635
|
<main class="content">
|
|
636
|
+
<section class="card">
|
|
637
|
+
<form
|
|
638
|
+
class="row"
|
|
639
|
+
method="POST"
|
|
640
|
+
action="?/defaultMode"
|
|
641
|
+
bind:this={modeFormEl}
|
|
642
|
+
{@attach enhance(defaultModeOpts)}
|
|
643
|
+
>
|
|
644
|
+
<div class="label">
|
|
645
|
+
<Terminal size={20} />
|
|
646
|
+
<div class="text">
|
|
647
|
+
<div class="name">Default editor</div>
|
|
648
|
+
<div class="desc">
|
|
649
|
+
What new instances start in. <strong>Full IDE</strong> serves browser VS Code;
|
|
650
|
+
<strong>Terminal</strong> is lighter — just Claude Code in a terminal, no code-server. You
|
|
651
|
+
can override this per instance when creating one.
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
<input type="hidden" name="mode" value={modeChoice} />
|
|
656
|
+
<div class="mode-toggle" role="group" aria-label="Default editor mode">
|
|
657
|
+
<button
|
|
658
|
+
type="button"
|
|
659
|
+
class="mode-btn"
|
|
660
|
+
class:active={modeChoice === 'ide'}
|
|
661
|
+
aria-pressed={modeChoice === 'ide'}
|
|
662
|
+
disabled={savingMode}
|
|
663
|
+
onclick={() => chooseMode('ide')}
|
|
664
|
+
>
|
|
665
|
+
<LayoutTemplate size={15} />
|
|
666
|
+
Full IDE
|
|
667
|
+
</button>
|
|
668
|
+
<button
|
|
669
|
+
type="button"
|
|
670
|
+
class="mode-btn"
|
|
671
|
+
class:active={modeChoice === 'terminal'}
|
|
672
|
+
aria-pressed={modeChoice === 'terminal'}
|
|
673
|
+
disabled={savingMode}
|
|
674
|
+
onclick={() => chooseMode('terminal')}
|
|
675
|
+
>
|
|
676
|
+
<Terminal size={15} />
|
|
677
|
+
Terminal
|
|
678
|
+
</button>
|
|
679
|
+
</div>
|
|
680
|
+
</form>
|
|
681
|
+
{#if modeError}
|
|
682
|
+
<div class="sub"><div class="msg error">{modeError}</div></div>
|
|
683
|
+
{/if}
|
|
684
|
+
</section>
|
|
685
|
+
|
|
608
686
|
<section class="card">
|
|
609
687
|
<form
|
|
610
688
|
class="row image-row"
|
|
@@ -1809,6 +1887,36 @@
|
|
|
1809
1887
|
outline: 2px solid var(--ink);
|
|
1810
1888
|
outline-offset: 1px;
|
|
1811
1889
|
}
|
|
1890
|
+
.mode-toggle {
|
|
1891
|
+
display: flex;
|
|
1892
|
+
flex: none;
|
|
1893
|
+
border: 1px solid var(--ink);
|
|
1894
|
+
}
|
|
1895
|
+
.mode-btn {
|
|
1896
|
+
display: inline-flex;
|
|
1897
|
+
align-items: center;
|
|
1898
|
+
gap: 6px;
|
|
1899
|
+
font-family: var(--font-mono);
|
|
1900
|
+
font-weight: 700;
|
|
1901
|
+
font-size: 12px;
|
|
1902
|
+
text-transform: uppercase;
|
|
1903
|
+
letter-spacing: 0.05em;
|
|
1904
|
+
padding: 7px 14px;
|
|
1905
|
+
border: none;
|
|
1906
|
+
background: var(--bg);
|
|
1907
|
+
color: var(--ink);
|
|
1908
|
+
cursor: pointer;
|
|
1909
|
+
}
|
|
1910
|
+
.mode-btn + .mode-btn {
|
|
1911
|
+
border-left: 1px solid var(--ink);
|
|
1912
|
+
}
|
|
1913
|
+
.mode-btn.active {
|
|
1914
|
+
background: var(--ink);
|
|
1915
|
+
color: var(--bg);
|
|
1916
|
+
}
|
|
1917
|
+
.mode-btn:disabled {
|
|
1918
|
+
cursor: not-allowed;
|
|
1919
|
+
}
|
|
1812
1920
|
.switch {
|
|
1813
1921
|
position: relative;
|
|
1814
1922
|
flex: none;
|