codebay 0.0.1 → 0.2.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/index-0nwv3654.css +18 -0
- package/.mochi/import-css/index-xndx4980.css +54 -0
- package/.mochi/manifest.json +1608 -0
- package/.mochi/server-island.js +1 -0
- package/.mochi/svelte-client/HydratableIsland-ps09gjy2.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-62w5zwww.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-jmm501zf.js +13 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-ewk4n2c0.js +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-e7e89md0.js +23 -0
- package/.mochi/svelte-client/chunk-jx5cekhh.js +2 -0
- package/.mochi/svelte-client/chunk-k3my1snp.js +2 -0
- package/.mochi/svelte-client/chunk-kgn1b2t1.js +4 -0
- package/.mochi/svelte-client/chunk-ry7q5zk5.js +1 -0
- package/.mochi/svelte-client/chunk-x843txjc.js +2 -0
- package/.mochi/svelte-client/chunk-zjcg8kw5.js +2 -0
- package/.mochi/svelte-compile/App-8k1gyxdc.server.js +1167 -0
- package/.mochi/svelte-compile/Avatars-f7c6rp96.server.js +351 -0
- package/.mochi/svelte-compile/ClientStats-z3tn9ad6.server.js +99 -0
- package/.mochi/svelte-compile/DefaultError-wh77vebf.server.js +52 -0
- package/.mochi/svelte-compile/Instance-yvps12zg.server.js +355 -0
- package/.mochi/svelte-compile/Settings-jwg3httb.server.js +1169 -0
- package/.mochi/svelte-compile/chunk-4kwy539a.js +14 -0
- package/.mochi/svelte-compile/chunk-9bjk7yx6.js +81 -0
- package/.mochi/svelte-compile/chunk-dqgstte4.js +52 -0
- package/.mochi/svelte-compile/chunk-sxqys3nw.js +1668 -0
- package/.mochi/svelte-compile/chunk-v6796ga4.js +3295 -0
- package/.mochi/svelte-compile/chunk-we0ggtws.js +34 -0
- package/.mochi/svelte-css/AppBar-3bxinv4pabv8l.css +1 -0
- package/.mochi/svelte-css/AppShell-36mpg5smnbmuw.css +1 -0
- package/.mochi/svelte-css/Avatar-24th63awm9mna.css +1 -0
- package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +1 -0
- package/.mochi/svelte-css/AvatarsView-3hq37u4dgtljn.css +1 -0
- package/.mochi/svelte-css/BackLink-2wlu0eovz9j86.css +1 -0
- package/.mochi/svelte-css/BranchBox-1yqu1q2mgnjwz.css +1 -0
- package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +1 -0
- package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +1 -0
- package/.mochi/svelte-css/CheckmarkIcon-3qlazj2oc50jc.css +1 -0
- package/.mochi/svelte-css/ClientStats-19qzckq40p2q9.css +1 -0
- package/.mochi/svelte-css/CoinButton-1rej3sh3tkjud.css +1 -0
- package/.mochi/svelte-css/CredMenu-9hwgeptp7crb.css +1 -0
- package/.mochi/svelte-css/DashboardView-1cy4xpqhja9sx.css +1 -0
- package/.mochi/svelte-css/DefaultError-2b0daamydxgb2.css +1 -0
- package/.mochi/svelte-css/ErrorIcon-2x0tzsqxskw28.css +1 -0
- package/.mochi/svelte-css/FolderBrowser-1x428zb3f2kti.css +1 -0
- package/.mochi/svelte-css/HealthBox-er2her0jukwl.css +1 -0
- package/.mochi/svelte-css/IdeBar-2qoqi2ya9zdn3.css +1 -0
- package/.mochi/svelte-css/IdeLoader-26rlko8e00j3g.css +1 -0
- package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +1 -0
- package/.mochi/svelte-css/InstanceView-wq85bxtusont.css +1 -0
- package/.mochi/svelte-css/LoaderIcon-soq5wt3p05ye.css +1 -0
- package/.mochi/svelte-css/PortsBox-1t25b5yg7jva3.css +1 -0
- package/.mochi/svelte-css/SettingsCog-jftyadvo64qo.css +1 -0
- package/.mochi/svelte-css/SettingsView-3bbkzsvnrth3v.css +1 -0
- package/.mochi/svelte-css/Skeleton-8k69usiju43h.css +1 -0
- package/.mochi/svelte-css/StatusBadge-3mq0j7z6aq4j.css +1 -0
- package/.mochi/svelte-css/ThemePicker-1ir8jyocm8mvi.css +1 -0
- package/.mochi/svelte-css/ToastBar-3havgkmtofntq.css +1 -0
- package/.mochi/svelte-css/ToastIcon-2z5wrkc95khg0.css +1 -0
- package/.mochi/svelte-css/ToastMessage-2jfwb2og2lfa8.css +1 -0
- package/.mochi/svelte-css/ToastWrapper-1fbnym10rm2nv.css +1 -0
- package/.mochi/svelte-css/Toaster-kk60j193854n.css +1 -0
- package/.mochi/svelte-css/TopBar-2q87li6yt8ids.css +1 -0
- package/LICENSE +21 -0
- package/README.md +33 -3
- package/bin/codebay.ts +22 -0
- package/migrations/0001_init.sql +20 -0
- package/migrations/0002_port_forwards.sql +9 -0
- package/migrations/0003_options.sql +4 -0
- package/migrations/0004_instance_image.sql +1 -0
- package/package.json +87 -9
- package/public/sounds/done.wav +0 -0
- package/public/sounds/waiting.wav +0 -0
- package/src/__fixtures__/HelloWorld.svelte +4 -0
- package/src/additional-svelte-typings.d.ts +1 -0
- package/src/api.ts +61 -0
- package/src/avatars/anchor.ts +17 -0
- package/src/avatars/bear.ts +17 -0
- package/src/avatars/bee.ts +17 -0
- package/src/avatars/bob-marley.ts +17 -0
- package/src/avatars/car.ts +17 -0
- package/src/avatars/carpathians.ts +17 -0
- package/src/avatars/cat-body.ts +17 -0
- package/src/avatars/cat-sits.ts +17 -0
- package/src/avatars/cat.ts +17 -0
- package/src/avatars/celtic-cross.ts +17 -0
- package/src/avatars/cherry.ts +17 -0
- package/src/avatars/cotton.ts +17 -0
- package/src/avatars/crab.ts +17 -0
- package/src/avatars/crown.ts +17 -0
- package/src/avatars/diamond.ts +17 -0
- package/src/avatars/dog.ts +17 -0
- package/src/avatars/elephant.ts +17 -0
- package/src/avatars/fish.ts +17 -0
- package/src/avatars/fleur-de-lis.ts +17 -0
- package/src/avatars/flower.ts +17 -0
- package/src/avatars/fox.ts +17 -0
- package/src/avatars/frog.ts +17 -0
- package/src/avatars/ghost.ts +17 -0
- package/src/avatars/giraffe.ts +17 -0
- package/src/avatars/glasses.ts +17 -0
- package/src/avatars/heart.ts +17 -0
- package/src/avatars/index.ts +128 -0
- package/src/avatars/invader.ts +17 -0
- package/src/avatars/kangaroo.ts +17 -0
- package/src/avatars/key.ts +17 -0
- package/src/avatars/lightning.ts +17 -0
- package/src/avatars/mushroom.ts +17 -0
- package/src/avatars/octopus.ts +17 -0
- package/src/avatars/owl.ts +17 -0
- package/src/avatars/penguin.ts +17 -0
- package/src/avatars/planet.ts +17 -0
- package/src/avatars/rabbit.ts +17 -0
- package/src/avatars/rat.ts +17 -0
- package/src/avatars/robot.ts +17 -0
- package/src/avatars/rocket.ts +17 -0
- package/src/avatars/sea.ts +17 -0
- package/src/avatars/serialize.ts +65 -0
- package/src/avatars/skull.ts +17 -0
- package/src/avatars/snail.ts +17 -0
- package/src/avatars/snake.ts +17 -0
- package/src/avatars/snakegame.ts +17 -0
- package/src/avatars/star.ts +17 -0
- package/src/avatars/svelte.ts +17 -0
- package/src/avatars/target.ts +17 -0
- package/src/avatars/tree.ts +17 -0
- package/src/avatars/types.ts +24 -0
- package/src/avatars/ukraine.ts +17 -0
- package/src/avatars/whale.ts +17 -0
- package/src/avatars/wheat.ts +17 -0
- package/src/avatars/yinyan.ts +17 -0
- package/src/components/AppBar.svelte +35 -0
- package/src/components/AppShell.svelte +302 -0
- package/src/components/Avatar.svelte +235 -0
- package/src/components/AvatarEditor.svelte +321 -0
- package/src/components/AvatarsView.svelte +113 -0
- package/src/components/BranchBox.svelte +32 -0
- package/src/components/Brand.svelte +41 -0
- package/src/components/Button.svelte +161 -0
- package/src/components/CoinButton.svelte +121 -0
- package/src/components/CredMenu.svelte +171 -0
- package/src/components/DashboardView.svelte +198 -0
- package/src/components/FolderBrowser.svelte +635 -0
- package/src/components/HealthBox.svelte +161 -0
- package/src/components/IdeBar.svelte +186 -0
- package/src/components/IdeLoader.svelte +135 -0
- package/src/components/InstanceCard.svelte +220 -0
- package/src/components/InstanceView.svelte +565 -0
- package/src/components/PortsBox.svelte +73 -0
- package/src/components/SettingsCog.svelte +22 -0
- package/src/components/SettingsView.svelte +1484 -0
- package/src/components/Skeleton.svelte +43 -0
- package/src/components/StatusBadge.svelte +54 -0
- package/src/components/ThemePicker.svelte +77 -0
- package/src/components/TopBar.svelte +64 -0
- package/src/components/UiShowcase.svelte +578 -0
- package/src/components/ui-showcase/ComponentDemo.svelte +85 -0
- package/src/container-injections/attention-hooks.ts +117 -0
- package/src/container-injections/claude-aliases.ts +43 -0
- package/src/container-injections/claude-code-credentials.ts +145 -0
- package/src/container-injections/claude-code-custom.ts +125 -0
- package/src/container-injections/claude-no-coauthor.ts +35 -0
- package/src/container-injections/claude-skip-permissions.ts +38 -0
- package/src/container-injections/claude-statusline.ts +109 -0
- package/src/container-injections/git-identity.ts +63 -0
- package/src/container-injections/git-safe-directory.ts +18 -0
- package/src/container-injections/github-credentials.ts +162 -0
- package/src/container-injections/host-env-vars.ts +108 -0
- package/src/container-injections/tmux.ts +67 -0
- package/src/fontsource.d.ts +3 -0
- package/src/index.ts +81 -0
- package/src/lib/auth.server.ts +95 -0
- package/src/lib/bridge.server.ts +34 -0
- package/src/lib/config.server.ts +122 -0
- package/src/lib/crypto.server.ts +12 -0
- package/src/lib/db.server.ts +196 -0
- package/src/lib/devcontainer.server.ts +503 -0
- package/src/lib/docker-client.server.ts +61 -0
- package/src/lib/docker.server.ts +139 -0
- package/src/lib/exec.server.ts +92 -0
- package/src/lib/git.server.ts +68 -0
- package/src/lib/health.server.ts +105 -0
- package/src/lib/injections.server.ts +72 -0
- package/src/lib/instances.server.ts +552 -0
- package/src/lib/picker.server.ts +46 -0
- package/src/lib/ports.server.ts +9 -0
- package/src/lib/proxy.server.ts +170 -0
- package/src/lib/repo-url.ts +60 -0
- package/src/lib/spawn.server.ts +14 -0
- package/src/lib/theme.server.ts +22 -0
- package/src/live.ts +50 -0
- package/src/pages/App.svelte +14 -0
- package/src/pages/Avatars.svelte +7 -0
- package/src/pages/Instance.svelte +9 -0
- package/src/pages/Settings.svelte +73 -0
- package/src/pages/UI.svelte +10 -0
- package/src/routes.ts +440 -0
- package/src/settings.ts +14 -0
- package/src/shell.html +100 -0
- package/src/sound.ts +69 -0
- package/src/theme.ts +24 -0
- package/src/types.ts +79 -0
- package/svelte.config.js +5 -0
|
@@ -0,0 +1,1484 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Container from '@lucide/svelte/icons/container';
|
|
3
|
+
import AppBar from './AppBar.svelte';
|
|
4
|
+
import Power from '@lucide/svelte/icons/power';
|
|
5
|
+
import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
|
|
6
|
+
import Volume2 from '@lucide/svelte/icons/volume-2';
|
|
7
|
+
import SunMoon from '@lucide/svelte/icons/sun-moon';
|
|
8
|
+
import ThemePicker from './ThemePicker.svelte';
|
|
9
|
+
import Layers from '@lucide/svelte/icons/layers';
|
|
10
|
+
import FolderMinus from '@lucide/svelte/icons/folder-minus';
|
|
11
|
+
import Trash2 from '@lucide/svelte/icons/trash-2';
|
|
12
|
+
import Hammer from '@lucide/svelte/icons/hammer';
|
|
13
|
+
import KeyRound from '@lucide/svelte/icons/key-round';
|
|
14
|
+
import Variable from '@lucide/svelte/icons/variable';
|
|
15
|
+
import Plus from '@lucide/svelte/icons/plus';
|
|
16
|
+
import X from '@lucide/svelte/icons/x';
|
|
17
|
+
import { Toaster } from 'svelte-french-toast';
|
|
18
|
+
import { flushSync } from 'svelte';
|
|
19
|
+
import { enhance } from 'mochi-framework';
|
|
20
|
+
import type { MochiEnhanceOptions } from 'mochi-framework';
|
|
21
|
+
import { soundEnabled, setSoundEnabled } from '../settings.ts';
|
|
22
|
+
import { playChime, unlockAudio } from '../sound.ts';
|
|
23
|
+
import Button from './Button.svelte';
|
|
24
|
+
import CoinButton from './CoinButton.svelte';
|
|
25
|
+
|
|
26
|
+
/** Every settings form action fails with the same `{ error }` shape. */
|
|
27
|
+
type ActionFailure = { error: string };
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
defaultImage,
|
|
31
|
+
builtinImage,
|
|
32
|
+
disableBuildCache,
|
|
33
|
+
copyIgnorePatterns,
|
|
34
|
+
builtinCopyIgnore,
|
|
35
|
+
dockerArch,
|
|
36
|
+
manualTokensEnabled,
|
|
37
|
+
githubTokenSet,
|
|
38
|
+
claudeTokenSet,
|
|
39
|
+
customEndpointEnabled,
|
|
40
|
+
customEndpointBaseUrl,
|
|
41
|
+
customEndpointTokenSet,
|
|
42
|
+
customEndpointOpusModel,
|
|
43
|
+
customEndpointSonnetModel,
|
|
44
|
+
customEndpointHaikuModel,
|
|
45
|
+
customEndpointSmallFastModel,
|
|
46
|
+
customEndpointModel,
|
|
47
|
+
hostEnvVarsEnabled,
|
|
48
|
+
hostEnvVarNames,
|
|
49
|
+
hostEnvVarPresence
|
|
50
|
+
}: {
|
|
51
|
+
defaultImage: string;
|
|
52
|
+
builtinImage: string;
|
|
53
|
+
disableBuildCache: boolean;
|
|
54
|
+
copyIgnorePatterns: string;
|
|
55
|
+
builtinCopyIgnore: string;
|
|
56
|
+
dockerArch: string | null;
|
|
57
|
+
manualTokensEnabled: boolean;
|
|
58
|
+
githubTokenSet: boolean;
|
|
59
|
+
claudeTokenSet: boolean;
|
|
60
|
+
customEndpointEnabled: boolean;
|
|
61
|
+
customEndpointBaseUrl: string;
|
|
62
|
+
customEndpointTokenSet: boolean;
|
|
63
|
+
customEndpointOpusModel: string;
|
|
64
|
+
customEndpointSonnetModel: string;
|
|
65
|
+
customEndpointHaikuModel: string;
|
|
66
|
+
customEndpointSmallFastModel: string;
|
|
67
|
+
customEndpointModel: string;
|
|
68
|
+
hostEnvVarsEnabled: boolean;
|
|
69
|
+
hostEnvVarNames: string[];
|
|
70
|
+
hostEnvVarPresence: Record<string, boolean>;
|
|
71
|
+
} = $props();
|
|
72
|
+
|
|
73
|
+
// Defaults to on during SSR, where localStorage doesn't exist.
|
|
74
|
+
let sound = $state(soundEnabled());
|
|
75
|
+
|
|
76
|
+
let shuttingDown = $state(false);
|
|
77
|
+
|
|
78
|
+
/** Reused by every plain save form below; only the per-control state setters differ. */
|
|
79
|
+
function saveOpts<Success extends Record<string, unknown> = Record<string, unknown>>(handlers: {
|
|
80
|
+
setSaving: (v: boolean) => void;
|
|
81
|
+
setError: (v: string | null) => void;
|
|
82
|
+
setMsg?: (v: string | null) => void;
|
|
83
|
+
onSuccess: (data: Success | undefined) => void;
|
|
84
|
+
confirmMessage?: string;
|
|
85
|
+
}): MochiEnhanceOptions<Success, ActionFailure> {
|
|
86
|
+
return {
|
|
87
|
+
onPending: handlers.setSaving,
|
|
88
|
+
submit: ({ cancel }) => {
|
|
89
|
+
if (handlers.confirmMessage && !confirm(handlers.confirmMessage)) {
|
|
90
|
+
cancel();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
handlers.setError(null);
|
|
94
|
+
handlers.setMsg?.(null);
|
|
95
|
+
return ({ result }) => {
|
|
96
|
+
if (result.type === 'success') {
|
|
97
|
+
handlers.onSuccess(result.data);
|
|
98
|
+
} else if (result.type === 'failure') {
|
|
99
|
+
handlers.setError(result.data?.error ?? 'Request failed');
|
|
100
|
+
} else if (result.type === 'error') {
|
|
101
|
+
handlers.setError('Network error. Try again.');
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The optimistic flip is free: `onchange` fires before submit, so `formData`
|
|
110
|
+
* already carries the new value. Failure reverts to the opposite of what was sent.
|
|
111
|
+
*/
|
|
112
|
+
function toggleOpts<Success extends Record<string, unknown> = Record<string, unknown>>(handlers: {
|
|
113
|
+
set: (v: boolean) => void;
|
|
114
|
+
setSaving: (v: boolean) => void;
|
|
115
|
+
setError: (v: string | null) => void;
|
|
116
|
+
onSuccess?: (data: Success | undefined) => void;
|
|
117
|
+
}): MochiEnhanceOptions<Success, ActionFailure> {
|
|
118
|
+
return {
|
|
119
|
+
onPending: handlers.setSaving,
|
|
120
|
+
submit: ({ formData }) => {
|
|
121
|
+
const intended = formData.get('enabled') === 'on';
|
|
122
|
+
handlers.setError(null);
|
|
123
|
+
return ({ result }) => {
|
|
124
|
+
if (result.type === 'success') {
|
|
125
|
+
handlers.onSuccess?.(result.data);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
handlers.set(!intended);
|
|
129
|
+
handlers.setError(
|
|
130
|
+
result.type === 'failure'
|
|
131
|
+
? (result.data?.error ?? 'Request failed')
|
|
132
|
+
: 'Network error. Try again.'
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// svelte-ignore state_referenced_locally
|
|
140
|
+
let image = $state(defaultImage);
|
|
141
|
+
let savingImage = $state(false);
|
|
142
|
+
let imageError = $state<string | null>(null);
|
|
143
|
+
let imageSaved = $state(false);
|
|
144
|
+
let imageFormEl: HTMLFormElement | undefined;
|
|
145
|
+
|
|
146
|
+
const imageOpts = saveOpts<{ image: string }>({
|
|
147
|
+
setSaving: (v) => (savingImage = v),
|
|
148
|
+
setError: (v) => (imageError = v),
|
|
149
|
+
setMsg: (v) => (imageSaved = !!v),
|
|
150
|
+
onSuccess: (data) => {
|
|
151
|
+
image = data?.image ?? image;
|
|
152
|
+
imageSaved = true;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Setting the bound value then resubmitting keeps this a one-form control.
|
|
157
|
+
function resetImage() {
|
|
158
|
+
// flushSync forces the bind:value DOM update to happen before requestSubmit
|
|
159
|
+
// reads the input's value via FormData — otherwise it would submit the stale value.
|
|
160
|
+
flushSync(() => (image = builtinImage));
|
|
161
|
+
imageFormEl?.requestSubmit();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Unlike the image field, an empty value is valid here — it means "copy everything".
|
|
165
|
+
// svelte-ignore state_referenced_locally
|
|
166
|
+
let copyIgnore = $state(copyIgnorePatterns);
|
|
167
|
+
let savingCopyIgnore = $state(false);
|
|
168
|
+
let copyIgnoreError = $state<string | null>(null);
|
|
169
|
+
let copyIgnoreSaved = $state(false);
|
|
170
|
+
let copyIgnoreFormEl: HTMLFormElement | undefined;
|
|
171
|
+
|
|
172
|
+
const copyIgnoreOpts = saveOpts<{ patterns: string }>({
|
|
173
|
+
setSaving: (v) => (savingCopyIgnore = v),
|
|
174
|
+
setError: (v) => (copyIgnoreError = v),
|
|
175
|
+
setMsg: (v) => (copyIgnoreSaved = !!v),
|
|
176
|
+
onSuccess: (data) => {
|
|
177
|
+
copyIgnore = data?.patterns ?? copyIgnore;
|
|
178
|
+
copyIgnoreSaved = true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
function resetCopyIgnore() {
|
|
183
|
+
flushSync(() => (copyIgnore = builtinCopyIgnore));
|
|
184
|
+
copyIgnoreFormEl?.requestSubmit();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// DB-backed rather than localStorage, so it initializes from the prop.
|
|
188
|
+
// svelte-ignore state_referenced_locally
|
|
189
|
+
let noCache = $state(disableBuildCache);
|
|
190
|
+
let savingCache = $state(false);
|
|
191
|
+
let cacheError = $state<string | null>(null);
|
|
192
|
+
|
|
193
|
+
const buildCacheToggleOpts = toggleOpts({
|
|
194
|
+
set: (v) => (noCache = v),
|
|
195
|
+
setSaving: (v) => (savingCache = v),
|
|
196
|
+
setError: (v) => (cacheError = v)
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
let clearing = $state(false);
|
|
200
|
+
let clearMsg = $state<string | null>(null);
|
|
201
|
+
let clearError = $state<string | null>(null);
|
|
202
|
+
|
|
203
|
+
function formatBytes(n: number): string {
|
|
204
|
+
if (n <= 0) return '0 B';
|
|
205
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
206
|
+
const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
|
207
|
+
return `${(n / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const clearCacheOpts = saveOpts<{ spaceReclaimed: number }>({
|
|
211
|
+
setSaving: (v) => (clearing = v),
|
|
212
|
+
setError: (v) => (clearError = v),
|
|
213
|
+
setMsg: (v) => (clearMsg = v),
|
|
214
|
+
onSuccess: (data) => {
|
|
215
|
+
clearMsg = `Cleared — freed ${formatBytes(data?.spaceReclaimed ?? 0)}.`;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
let rebuilding = $state(false);
|
|
220
|
+
let rebuildMsg = $state<string | null>(null);
|
|
221
|
+
let rebuildError = $state<string | null>(null);
|
|
222
|
+
|
|
223
|
+
const rebuildAllOpts = saveOpts<{ count: number }>({
|
|
224
|
+
setSaving: (v) => (rebuilding = v),
|
|
225
|
+
setError: (v) => (rebuildError = v),
|
|
226
|
+
setMsg: (v) => (rebuildMsg = v),
|
|
227
|
+
confirmMessage:
|
|
228
|
+
'Rebuild every running container from scratch (no build cache)? Each will restart and may take a while.',
|
|
229
|
+
onSuccess: (data) => {
|
|
230
|
+
const n = data?.count ?? 0;
|
|
231
|
+
rebuildMsg = n === 0 ? 'No running containers to rebuild.' : `Rebuilding ${n} container(s)…`;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Token values never reach the client; only whether each is set, for the placeholder.
|
|
236
|
+
// svelte-ignore state_referenced_locally
|
|
237
|
+
let manualTokens = $state(manualTokensEnabled);
|
|
238
|
+
let savingManualToggle = $state(false);
|
|
239
|
+
let manualToggleError = $state<string | null>(null);
|
|
240
|
+
|
|
241
|
+
const manualTokensToggleOpts = toggleOpts({
|
|
242
|
+
set: (v) => (manualTokens = v),
|
|
243
|
+
setSaving: (v) => (savingManualToggle = v),
|
|
244
|
+
setError: (v) => (manualToggleError = v)
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// svelte-ignore state_referenced_locally
|
|
248
|
+
let ghSaved = $state(githubTokenSet);
|
|
249
|
+
let githubToken = $state('');
|
|
250
|
+
let savingGithub = $state(false);
|
|
251
|
+
let githubMsg = $state<string | null>(null);
|
|
252
|
+
let githubError = $state<string | null>(null);
|
|
253
|
+
|
|
254
|
+
const githubTokenOpts = saveOpts<{ set: boolean }>({
|
|
255
|
+
setSaving: (v) => (savingGithub = v),
|
|
256
|
+
setError: (v) => (githubError = v),
|
|
257
|
+
setMsg: (v) => (githubMsg = v),
|
|
258
|
+
onSuccess: (data) => {
|
|
259
|
+
ghSaved = data?.set ?? false;
|
|
260
|
+
githubToken = '';
|
|
261
|
+
githubMsg = ghSaved ? 'Saved.' : 'Cleared.';
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// svelte-ignore state_referenced_locally
|
|
266
|
+
let claudeSaved = $state(claudeTokenSet);
|
|
267
|
+
let claudeToken = $state('');
|
|
268
|
+
let savingClaude = $state(false);
|
|
269
|
+
let claudeMsg = $state<string | null>(null);
|
|
270
|
+
let claudeError = $state<string | null>(null);
|
|
271
|
+
|
|
272
|
+
const claudeTokenOpts = saveOpts<{ set: boolean }>({
|
|
273
|
+
setSaving: (v) => (savingClaude = v),
|
|
274
|
+
setError: (v) => (claudeError = v),
|
|
275
|
+
setMsg: (v) => (claudeMsg = v),
|
|
276
|
+
onSuccess: (data) => {
|
|
277
|
+
claudeSaved = data?.set ?? false;
|
|
278
|
+
claudeToken = '';
|
|
279
|
+
claudeMsg = claudeSaved ? 'Saved.' : 'Cleared.';
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// As with manual tokens, the token value never comes back from the server.
|
|
284
|
+
// svelte-ignore state_referenced_locally
|
|
285
|
+
let customEndpoint = $state(customEndpointEnabled);
|
|
286
|
+
let savingCustomToggle = $state(false);
|
|
287
|
+
let customToggleError = $state<string | null>(null);
|
|
288
|
+
|
|
289
|
+
const customEndpointToggleOpts = toggleOpts({
|
|
290
|
+
set: (v) => (customEndpoint = v),
|
|
291
|
+
setSaving: (v) => (savingCustomToggle = v),
|
|
292
|
+
setError: (v) => (customToggleError = v)
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// svelte-ignore state_referenced_locally
|
|
296
|
+
let customBaseUrl = $state(customEndpointBaseUrl);
|
|
297
|
+
let savingCustomBaseUrl = $state(false);
|
|
298
|
+
let customBaseUrlMsg = $state<string | null>(null);
|
|
299
|
+
let customBaseUrlError = $state<string | null>(null);
|
|
300
|
+
|
|
301
|
+
const customBaseUrlOpts = saveOpts<{ set: boolean }>({
|
|
302
|
+
setSaving: (v) => (savingCustomBaseUrl = v),
|
|
303
|
+
setError: (v) => (customBaseUrlError = v),
|
|
304
|
+
setMsg: (v) => (customBaseUrlMsg = v),
|
|
305
|
+
onSuccess: (data) => {
|
|
306
|
+
customBaseUrlMsg = data?.set ? 'Saved.' : 'Cleared.';
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
let customToken = $state('');
|
|
311
|
+
// svelte-ignore state_referenced_locally
|
|
312
|
+
let customTokenSaved = $state(customEndpointTokenSet);
|
|
313
|
+
let savingCustomToken = $state(false);
|
|
314
|
+
let customTokenMsg = $state<string | null>(null);
|
|
315
|
+
let customTokenError = $state<string | null>(null);
|
|
316
|
+
|
|
317
|
+
const customTokenOpts = saveOpts<{ set: boolean }>({
|
|
318
|
+
setSaving: (v) => (savingCustomToken = v),
|
|
319
|
+
setError: (v) => (customTokenError = v),
|
|
320
|
+
setMsg: (v) => (customTokenMsg = v),
|
|
321
|
+
onSuccess: (data) => {
|
|
322
|
+
customTokenSaved = data?.set ?? false;
|
|
323
|
+
customToken = '';
|
|
324
|
+
customTokenMsg = customTokenSaved ? 'Saved.' : 'Cleared.';
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// svelte-ignore state_referenced_locally
|
|
329
|
+
let customOpusModel = $state(customEndpointOpusModel);
|
|
330
|
+
// svelte-ignore state_referenced_locally
|
|
331
|
+
let customSonnetModel = $state(customEndpointSonnetModel);
|
|
332
|
+
// svelte-ignore state_referenced_locally
|
|
333
|
+
let customHaikuModel = $state(customEndpointHaikuModel);
|
|
334
|
+
// svelte-ignore state_referenced_locally
|
|
335
|
+
let customSmallFastModel = $state(customEndpointSmallFastModel);
|
|
336
|
+
// svelte-ignore state_referenced_locally
|
|
337
|
+
let customDefaultModel = $state(customEndpointModel);
|
|
338
|
+
let savingCustomModels = $state(false);
|
|
339
|
+
let customModelsMsg = $state<string | null>(null);
|
|
340
|
+
let customModelsError = $state<string | null>(null);
|
|
341
|
+
|
|
342
|
+
const customModelsOpts = saveOpts({
|
|
343
|
+
setSaving: (v) => (savingCustomModels = v),
|
|
344
|
+
setError: (v) => (customModelsError = v),
|
|
345
|
+
setMsg: (v) => (customModelsMsg = v),
|
|
346
|
+
onSuccess: () => {
|
|
347
|
+
customModelsMsg = 'Saved.';
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Only names round-trip; `hostEnvPresence` is refreshed from each save response so a
|
|
352
|
+
// newly-added name doesn't read as "missing" until the next full page load.
|
|
353
|
+
// svelte-ignore state_referenced_locally
|
|
354
|
+
let hostEnvVars = $state(hostEnvVarsEnabled);
|
|
355
|
+
let savingHostEnvToggle = $state(false);
|
|
356
|
+
let hostEnvToggleError = $state<string | null>(null);
|
|
357
|
+
|
|
358
|
+
const hostEnvVarsToggleOpts = toggleOpts<{ presence: Record<string, boolean> }>({
|
|
359
|
+
set: (v) => (hostEnvVars = v),
|
|
360
|
+
setSaving: (v) => (savingHostEnvToggle = v),
|
|
361
|
+
setError: (v) => (hostEnvToggleError = v),
|
|
362
|
+
onSuccess: (data) => {
|
|
363
|
+
hostEnvPresence = data?.presence ?? {};
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// svelte-ignore state_referenced_locally
|
|
368
|
+
let hostEnvNames = $state([...hostEnvVarNames]);
|
|
369
|
+
// svelte-ignore state_referenced_locally
|
|
370
|
+
let hostEnvPresence = $state({ ...hostEnvVarPresence });
|
|
371
|
+
let newHostEnvName = $state('');
|
|
372
|
+
let savingHostEnvNames = $state(false);
|
|
373
|
+
let hostEnvNamesMsg = $state<string | null>(null);
|
|
374
|
+
let hostEnvNamesError = $state<string | null>(null);
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Add form: submits every existing name (as hidden inputs) plus the free-text
|
|
378
|
+
* input, all under the repeated `names` field — the last entry is always the
|
|
379
|
+
* typed value. Validated and normalized (uppercased, deduped) here, in the
|
|
380
|
+
* submit callback, by mutating `formData` directly before the fetch fires —
|
|
381
|
+
* mirrors the old client-side pre-check without a second code path.
|
|
382
|
+
*/
|
|
383
|
+
const addHostEnvOpts: MochiEnhanceOptions<{ presence: Record<string, boolean> }, ActionFailure> =
|
|
384
|
+
{
|
|
385
|
+
onPending: (v) => (savingHostEnvNames = v),
|
|
386
|
+
submit: ({ formData, cancel }) => {
|
|
387
|
+
hostEnvNamesError = null;
|
|
388
|
+
hostEnvNamesMsg = null;
|
|
389
|
+
const existing = formData.getAll('names').map(String);
|
|
390
|
+
const typed = existing.pop() ?? '';
|
|
391
|
+
const name = typed.trim().toUpperCase();
|
|
392
|
+
if (!name) {
|
|
393
|
+
cancel();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
397
|
+
hostEnvNamesError = `Invalid variable name: ${name}`;
|
|
398
|
+
cancel();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
newHostEnvName = '';
|
|
402
|
+
if (existing.includes(name)) {
|
|
403
|
+
cancel();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const names = [...existing, name];
|
|
407
|
+
formData.delete('names');
|
|
408
|
+
for (const n of names) formData.append('names', n);
|
|
409
|
+
return ({ result }) => {
|
|
410
|
+
if (result.type === 'success') {
|
|
411
|
+
hostEnvNames = names;
|
|
412
|
+
hostEnvPresence = result.data?.presence ?? {};
|
|
413
|
+
hostEnvNamesMsg = 'Saved.';
|
|
414
|
+
} else if (result.type === 'failure') {
|
|
415
|
+
hostEnvNamesError = result.data?.error ?? 'Request failed';
|
|
416
|
+
} else if (result.type === 'error') {
|
|
417
|
+
hostEnvNamesError = 'Network error. Try again.';
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/** Remove form (one per row): submits every name except this one. */
|
|
424
|
+
function removeHostEnvOpts(name: string) {
|
|
425
|
+
return saveOpts<{ presence: Record<string, boolean> }>({
|
|
426
|
+
setSaving: (v) => (savingHostEnvNames = v),
|
|
427
|
+
setError: (v) => (hostEnvNamesError = v),
|
|
428
|
+
setMsg: (v) => (hostEnvNamesMsg = v),
|
|
429
|
+
onSuccess: (data) => {
|
|
430
|
+
hostEnvNames = hostEnvNames.filter((n) => n !== name);
|
|
431
|
+
hostEnvPresence = data?.presence ?? {};
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function toggleSound(on: boolean) {
|
|
437
|
+
sound = on;
|
|
438
|
+
setSoundEnabled(on);
|
|
439
|
+
// A toggle is a user gesture — unlock audio and preview when enabling.
|
|
440
|
+
unlockAudio();
|
|
441
|
+
if (on) playChime('done');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The server exits mid-flight, so a dropped connection means success here too.
|
|
445
|
+
const shutdownOpts: MochiEnhanceOptions<Record<string, never>, ActionFailure> = {
|
|
446
|
+
submit: ({ cancel }) => {
|
|
447
|
+
if (
|
|
448
|
+
!confirm(
|
|
449
|
+
'Delete the database, remove all instances and their containers, and shut down the server? This cannot be undone.'
|
|
450
|
+
)
|
|
451
|
+
) {
|
|
452
|
+
cancel();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
shuttingDown = true;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
</script>
|
|
459
|
+
|
|
460
|
+
<div class="page">
|
|
461
|
+
<AppBar>
|
|
462
|
+
<span class="title">Settings</span>
|
|
463
|
+
</AppBar>
|
|
464
|
+
|
|
465
|
+
<main class="content">
|
|
466
|
+
<section class="card">
|
|
467
|
+
<form
|
|
468
|
+
class="row image-row"
|
|
469
|
+
method="POST"
|
|
470
|
+
action="?/defaultImage"
|
|
471
|
+
bind:this={imageFormEl}
|
|
472
|
+
{@attach enhance(imageOpts)}
|
|
473
|
+
>
|
|
474
|
+
<div class="label">
|
|
475
|
+
<Container size={18} />
|
|
476
|
+
<div class="text">
|
|
477
|
+
<div class="name">
|
|
478
|
+
Default container image
|
|
479
|
+
{#if dockerArch}
|
|
480
|
+
<span class="arch" title="Docker daemon architecture">{dockerArch}</span>
|
|
481
|
+
{/if}
|
|
482
|
+
</div>
|
|
483
|
+
<div class="desc">
|
|
484
|
+
Used only when a project folder ships no devcontainer.json. Takes effect for instances
|
|
485
|
+
created from now on.
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
<div class="image-controls">
|
|
490
|
+
<input
|
|
491
|
+
type="text"
|
|
492
|
+
name="image"
|
|
493
|
+
class="image-input"
|
|
494
|
+
bind:value={image}
|
|
495
|
+
spellcheck="false"
|
|
496
|
+
autocapitalize="off"
|
|
497
|
+
autocorrect="off"
|
|
498
|
+
placeholder="mcr.microsoft.com/devcontainers/base:ubuntu"
|
|
499
|
+
/>
|
|
500
|
+
<Button type="submit" disabled={savingImage}>Save</Button>
|
|
501
|
+
<Button
|
|
502
|
+
type="button"
|
|
503
|
+
icon={RotateCcw}
|
|
504
|
+
disabled={savingImage}
|
|
505
|
+
onclick={resetImage}
|
|
506
|
+
title="Reset to default ({builtinImage})"
|
|
507
|
+
aria-label="Reset to default image"
|
|
508
|
+
/>
|
|
509
|
+
</div>
|
|
510
|
+
<div class="desc tip">
|
|
511
|
+
{#if dockerArch}
|
|
512
|
+
Your Docker daemon runs on <strong>{dockerArch}</strong> — pick an image that publishes
|
|
513
|
+
an <strong>{dockerArch}</strong> manifest, or the pull will fail.
|
|
514
|
+
{:else}
|
|
515
|
+
Pick an image whose manifest covers your Docker daemon's architecture, or the pull will
|
|
516
|
+
fail.
|
|
517
|
+
{/if}
|
|
518
|
+
</div>
|
|
519
|
+
{#if imageError}
|
|
520
|
+
<div class="msg error">{imageError}</div>
|
|
521
|
+
{:else if imageSaved}
|
|
522
|
+
<div class="msg ok">Saved.</div>
|
|
523
|
+
{/if}
|
|
524
|
+
</form>
|
|
525
|
+
</section>
|
|
526
|
+
|
|
527
|
+
<section class="card">
|
|
528
|
+
<form
|
|
529
|
+
class="row image-row"
|
|
530
|
+
method="POST"
|
|
531
|
+
action="?/copyIgnorePatterns"
|
|
532
|
+
bind:this={copyIgnoreFormEl}
|
|
533
|
+
{@attach enhance(copyIgnoreOpts)}
|
|
534
|
+
>
|
|
535
|
+
<div class="label">
|
|
536
|
+
<FolderMinus size={18} />
|
|
537
|
+
<div class="text">
|
|
538
|
+
<div class="name">Skip when copying a local folder</div>
|
|
539
|
+
<div class="desc">
|
|
540
|
+
Comma-separated folder/file names excluded when copying a local source folder into a
|
|
541
|
+
new instance's workspace. Leave blank to copy everything. Only applies to local
|
|
542
|
+
folders — cloning a Git repo URL is unaffected.
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
<div class="image-controls">
|
|
547
|
+
<input
|
|
548
|
+
type="text"
|
|
549
|
+
name="patterns"
|
|
550
|
+
class="image-input"
|
|
551
|
+
bind:value={copyIgnore}
|
|
552
|
+
spellcheck="false"
|
|
553
|
+
autocapitalize="off"
|
|
554
|
+
autocorrect="off"
|
|
555
|
+
placeholder="node_modules, .venv, dist"
|
|
556
|
+
/>
|
|
557
|
+
<Button type="submit" disabled={savingCopyIgnore}>Save</Button>
|
|
558
|
+
<Button
|
|
559
|
+
type="button"
|
|
560
|
+
icon={RotateCcw}
|
|
561
|
+
disabled={savingCopyIgnore}
|
|
562
|
+
onclick={resetCopyIgnore}
|
|
563
|
+
title="Reset to default ({builtinCopyIgnore})"
|
|
564
|
+
aria-label="Reset to default copy-ignore patterns"
|
|
565
|
+
/>
|
|
566
|
+
</div>
|
|
567
|
+
{#if copyIgnoreError}
|
|
568
|
+
<div class="msg error">{copyIgnoreError}</div>
|
|
569
|
+
{:else if copyIgnoreSaved}
|
|
570
|
+
<div class="msg ok">Saved.</div>
|
|
571
|
+
{/if}
|
|
572
|
+
</form>
|
|
573
|
+
</section>
|
|
574
|
+
|
|
575
|
+
<section class="card">
|
|
576
|
+
<form
|
|
577
|
+
class="row"
|
|
578
|
+
method="POST"
|
|
579
|
+
action="?/disableBuildCache"
|
|
580
|
+
{@attach enhance(buildCacheToggleOpts)}
|
|
581
|
+
>
|
|
582
|
+
<div class="label">
|
|
583
|
+
<Layers size={18} />
|
|
584
|
+
<div class="text">
|
|
585
|
+
<div class="name">Disable build cache</div>
|
|
586
|
+
<div class="desc">
|
|
587
|
+
Build every new container with <code>--build-no-cache</code>. Applies to first boot
|
|
588
|
+
and rebuilds — slower, but always picks up upstream image/layer changes.
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
<label class="switch">
|
|
593
|
+
<input
|
|
594
|
+
type="checkbox"
|
|
595
|
+
name="enabled"
|
|
596
|
+
checked={noCache}
|
|
597
|
+
disabled={savingCache}
|
|
598
|
+
onchange={(e) => {
|
|
599
|
+
noCache = e.currentTarget.checked;
|
|
600
|
+
e.currentTarget.form?.requestSubmit();
|
|
601
|
+
}}
|
|
602
|
+
/>
|
|
603
|
+
<span class="track"><span class="thumb"></span></span>
|
|
604
|
+
</label>
|
|
605
|
+
</form>
|
|
606
|
+
{#if cacheError}
|
|
607
|
+
<div class="sub"><div class="msg error">{cacheError}</div></div>
|
|
608
|
+
{/if}
|
|
609
|
+
|
|
610
|
+
<form
|
|
611
|
+
class="row divided"
|
|
612
|
+
method="POST"
|
|
613
|
+
action="?/clearBuildCache"
|
|
614
|
+
{@attach enhance(clearCacheOpts)}
|
|
615
|
+
>
|
|
616
|
+
<div class="label">
|
|
617
|
+
<Trash2 size={18} />
|
|
618
|
+
<div class="text">
|
|
619
|
+
<div class="name">Clear build cache</div>
|
|
620
|
+
<div class="desc">
|
|
621
|
+
Purge Docker's BuildKit layer cache now, so the next build runs uncached. Doesn't
|
|
622
|
+
remove pulled images.
|
|
623
|
+
</div>
|
|
624
|
+
{#if clearError}
|
|
625
|
+
<div class="msg error">{clearError}</div>
|
|
626
|
+
{:else if clearMsg}
|
|
627
|
+
<div class="msg ok">{clearMsg}</div>
|
|
628
|
+
{/if}
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
<Button type="submit" icon={Trash2} disabled={clearing}>
|
|
632
|
+
{clearing ? 'Clearing…' : 'Clear cache'}
|
|
633
|
+
</Button>
|
|
634
|
+
</form>
|
|
635
|
+
|
|
636
|
+
<form
|
|
637
|
+
class="row divided"
|
|
638
|
+
method="POST"
|
|
639
|
+
action="?/rebuildAllNoCache"
|
|
640
|
+
{@attach enhance(rebuildAllOpts)}
|
|
641
|
+
>
|
|
642
|
+
<div class="label">
|
|
643
|
+
<Hammer size={18} />
|
|
644
|
+
<div class="text">
|
|
645
|
+
<div class="name">Rebuild running containers (no cache)</div>
|
|
646
|
+
<div class="desc">
|
|
647
|
+
Re-run <code>devcontainer up --build-no-cache</code> for every currently-running instance.
|
|
648
|
+
In-container edits are kept; stopped instances are left alone.
|
|
649
|
+
</div>
|
|
650
|
+
{#if rebuildError}
|
|
651
|
+
<div class="msg error">{rebuildError}</div>
|
|
652
|
+
{:else if rebuildMsg}
|
|
653
|
+
<div class="msg ok">{rebuildMsg}</div>
|
|
654
|
+
{/if}
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
<Button type="submit" icon={Hammer} disabled={rebuilding}>
|
|
658
|
+
{rebuilding ? 'Starting…' : 'Rebuild all'}
|
|
659
|
+
</Button>
|
|
660
|
+
</form>
|
|
661
|
+
</section>
|
|
662
|
+
|
|
663
|
+
<section class="card" class:disabled-card={customEndpoint}>
|
|
664
|
+
<form
|
|
665
|
+
class="row"
|
|
666
|
+
method="POST"
|
|
667
|
+
action="?/manualTokensToggle"
|
|
668
|
+
{@attach enhance(manualTokensToggleOpts)}
|
|
669
|
+
>
|
|
670
|
+
<div class="label">
|
|
671
|
+
<KeyRound size={18} />
|
|
672
|
+
<div class="text">
|
|
673
|
+
<div class="name">
|
|
674
|
+
Set tokens manually
|
|
675
|
+
{#if customEndpoint}
|
|
676
|
+
<span class="arch" title="Disabled while LiteLLM + Bedrock mode is on"
|
|
677
|
+
>disabled</span
|
|
678
|
+
>
|
|
679
|
+
{/if}
|
|
680
|
+
</div>
|
|
681
|
+
<div class="desc">
|
|
682
|
+
{#if customEndpoint}
|
|
683
|
+
Not available while LiteLLM + Bedrock mode is enabled — Claude credentials are
|
|
684
|
+
provided by the LiteLLM endpoint instead.
|
|
685
|
+
{:else}
|
|
686
|
+
Provide GitHub and Claude Code tokens yourself instead of discovering them from this
|
|
687
|
+
machine. Useful on a headless server or when signed in as a different identity. A
|
|
688
|
+
token set here is injected into every new container and overrides host credential
|
|
689
|
+
discovery.
|
|
690
|
+
{/if}
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
<label class="switch">
|
|
695
|
+
<input
|
|
696
|
+
type="checkbox"
|
|
697
|
+
name="enabled"
|
|
698
|
+
checked={manualTokens}
|
|
699
|
+
disabled={savingManualToggle || customEndpoint}
|
|
700
|
+
onchange={(e) => {
|
|
701
|
+
manualTokens = e.currentTarget.checked;
|
|
702
|
+
e.currentTarget.form?.requestSubmit();
|
|
703
|
+
}}
|
|
704
|
+
/>
|
|
705
|
+
<span class="track"><span class="thumb"></span></span>
|
|
706
|
+
</label>
|
|
707
|
+
</form>
|
|
708
|
+
{#if manualToggleError}
|
|
709
|
+
<div class="sub"><div class="msg error">{manualToggleError}</div></div>
|
|
710
|
+
{/if}
|
|
711
|
+
|
|
712
|
+
{#if manualTokens && !customEndpoint}
|
|
713
|
+
<form
|
|
714
|
+
class="row divided token-row"
|
|
715
|
+
method="POST"
|
|
716
|
+
action="?/githubToken"
|
|
717
|
+
{@attach enhance(githubTokenOpts)}
|
|
718
|
+
>
|
|
719
|
+
<div class="label">
|
|
720
|
+
<div class="text">
|
|
721
|
+
<div class="name">GitHub token</div>
|
|
722
|
+
<div class="desc">
|
|
723
|
+
macOS / Linux: run <code>gh auth token</code> to print your GitHub CLI token, or
|
|
724
|
+
create a Personal Access Token at
|
|
725
|
+
<code>github.com/settings/tokens</code> (scopes: <code>repo</code>,
|
|
726
|
+
<code>read:org</code>). Leave blank and Save to clear.
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
<div class="image-controls">
|
|
731
|
+
<input
|
|
732
|
+
type="password"
|
|
733
|
+
name="githubToken"
|
|
734
|
+
class="image-input"
|
|
735
|
+
bind:value={githubToken}
|
|
736
|
+
spellcheck="false"
|
|
737
|
+
autocapitalize="off"
|
|
738
|
+
autocorrect="off"
|
|
739
|
+
autocomplete="off"
|
|
740
|
+
placeholder={ghSaved ? '•••••••• (saved)' : 'ghp_… / gho_…'}
|
|
741
|
+
/>
|
|
742
|
+
<Button type="submit" disabled={savingGithub}>Save</Button>
|
|
743
|
+
</div>
|
|
744
|
+
{#if githubError}
|
|
745
|
+
<div class="msg error">{githubError}</div>
|
|
746
|
+
{:else if githubMsg}
|
|
747
|
+
<div class="msg ok">{githubMsg}</div>
|
|
748
|
+
{/if}
|
|
749
|
+
</form>
|
|
750
|
+
|
|
751
|
+
<form
|
|
752
|
+
class="row divided token-row"
|
|
753
|
+
method="POST"
|
|
754
|
+
action="?/claudeToken"
|
|
755
|
+
{@attach enhance(claudeTokenOpts)}
|
|
756
|
+
>
|
|
757
|
+
<div class="label">
|
|
758
|
+
<div class="text">
|
|
759
|
+
<div class="name">Claude Code token</div>
|
|
760
|
+
<div class="desc">
|
|
761
|
+
macOS / Linux: run <code>claude setup-token</code> to mint a long-lived token and
|
|
762
|
+
paste it here. Copying a live login out of the keychain or
|
|
763
|
+
<code>~/.claude/.credentials.json</code> works too, but that credential rotates —
|
|
764
|
+
the first <code>claude</code> to refresh it signs the other one out. Leave blank and Save
|
|
765
|
+
to clear.
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
</div>
|
|
769
|
+
<div class="image-controls">
|
|
770
|
+
<input
|
|
771
|
+
type="password"
|
|
772
|
+
name="claudeToken"
|
|
773
|
+
class="image-input"
|
|
774
|
+
bind:value={claudeToken}
|
|
775
|
+
spellcheck="false"
|
|
776
|
+
autocapitalize="off"
|
|
777
|
+
autocorrect="off"
|
|
778
|
+
autocomplete="off"
|
|
779
|
+
placeholder={claudeSaved ? '•••••••• (saved)' : 'sk-ant-oat…'}
|
|
780
|
+
/>
|
|
781
|
+
<Button type="submit" disabled={savingClaude}>Save</Button>
|
|
782
|
+
</div>
|
|
783
|
+
{#if claudeError}
|
|
784
|
+
<div class="msg error">{claudeError}</div>
|
|
785
|
+
{:else if claudeMsg}
|
|
786
|
+
<div class="msg ok">{claudeMsg}</div>
|
|
787
|
+
{/if}
|
|
788
|
+
</form>
|
|
789
|
+
{/if}
|
|
790
|
+
</section>
|
|
791
|
+
|
|
792
|
+
<section class="card" class:disabled-card={manualTokens}>
|
|
793
|
+
<form
|
|
794
|
+
class="row"
|
|
795
|
+
method="POST"
|
|
796
|
+
action="?/customEndpointToggle"
|
|
797
|
+
{@attach enhance(customEndpointToggleOpts)}
|
|
798
|
+
>
|
|
799
|
+
<div class="label">
|
|
800
|
+
<KeyRound size={18} />
|
|
801
|
+
<div class="text">
|
|
802
|
+
<div class="name">
|
|
803
|
+
LiteLLM + Bedrock
|
|
804
|
+
{#if manualTokens}
|
|
805
|
+
<span class="arch" title="Disabled while Set tokens manually is on">disabled</span>
|
|
806
|
+
{/if}
|
|
807
|
+
</div>
|
|
808
|
+
<div class="desc">
|
|
809
|
+
{#if manualTokens}
|
|
810
|
+
Not available while "Set tokens manually" is enabled — these modes are mutually
|
|
811
|
+
incompatible.
|
|
812
|
+
{:else}
|
|
813
|
+
Route <code>claude</code> through a LiteLLM proxy fronting AWS Bedrock instead of Anthropic's
|
|
814
|
+
default API. When enabled, the Bedrock endpoint variables are injected into every new
|
|
815
|
+
container, host OAuth credentials are not used, and "Set tokens manually" is disabled.
|
|
816
|
+
{/if}
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
</div>
|
|
820
|
+
<label class="switch">
|
|
821
|
+
<input
|
|
822
|
+
type="checkbox"
|
|
823
|
+
name="enabled"
|
|
824
|
+
checked={customEndpoint}
|
|
825
|
+
disabled={savingCustomToggle || manualTokens}
|
|
826
|
+
onchange={(e) => {
|
|
827
|
+
customEndpoint = e.currentTarget.checked;
|
|
828
|
+
e.currentTarget.form?.requestSubmit();
|
|
829
|
+
}}
|
|
830
|
+
/>
|
|
831
|
+
<span class="track"><span class="thumb"></span></span>
|
|
832
|
+
</label>
|
|
833
|
+
</form>
|
|
834
|
+
{#if customToggleError}
|
|
835
|
+
<div class="sub"><div class="msg error">{customToggleError}</div></div>
|
|
836
|
+
{/if}
|
|
837
|
+
|
|
838
|
+
{#if customEndpoint}
|
|
839
|
+
<form
|
|
840
|
+
class="row divided token-row"
|
|
841
|
+
method="POST"
|
|
842
|
+
action="?/customBaseUrl"
|
|
843
|
+
{@attach enhance(customBaseUrlOpts)}
|
|
844
|
+
>
|
|
845
|
+
<div class="label">
|
|
846
|
+
<div class="text">
|
|
847
|
+
<div class="name">Base URL</div>
|
|
848
|
+
<div class="desc">
|
|
849
|
+
The LiteLLM proxy endpoint, e.g.
|
|
850
|
+
<code>https://litellm.example.com/bedrock</code>. Passed as
|
|
851
|
+
<code>ANTHROPIC_BEDROCK_BASE_URL</code>.
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
<div class="image-controls">
|
|
856
|
+
<input
|
|
857
|
+
type="text"
|
|
858
|
+
name="baseUrl"
|
|
859
|
+
class="image-input"
|
|
860
|
+
bind:value={customBaseUrl}
|
|
861
|
+
spellcheck="false"
|
|
862
|
+
autocapitalize="off"
|
|
863
|
+
autocorrect="off"
|
|
864
|
+
autocomplete="off"
|
|
865
|
+
placeholder="https://litellm.example.com/bedrock"
|
|
866
|
+
/>
|
|
867
|
+
<Button type="submit" disabled={savingCustomBaseUrl}>Save</Button>
|
|
868
|
+
</div>
|
|
869
|
+
{#if customBaseUrlError}
|
|
870
|
+
<div class="msg error">{customBaseUrlError}</div>
|
|
871
|
+
{:else if customBaseUrlMsg}
|
|
872
|
+
<div class="msg ok">{customBaseUrlMsg}</div>
|
|
873
|
+
{/if}
|
|
874
|
+
</form>
|
|
875
|
+
|
|
876
|
+
<form
|
|
877
|
+
class="row divided token-row"
|
|
878
|
+
method="POST"
|
|
879
|
+
action="?/customToken"
|
|
880
|
+
{@attach enhance(customTokenOpts)}
|
|
881
|
+
>
|
|
882
|
+
<div class="label">
|
|
883
|
+
<div class="text">
|
|
884
|
+
<div class="name">Auth token</div>
|
|
885
|
+
<div class="desc">
|
|
886
|
+
Your LiteLLM API key. Passed as <code>ANTHROPIC_AUTH_TOKEN</code>. Leave blank and
|
|
887
|
+
Save to clear.
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
<div class="image-controls">
|
|
892
|
+
<input
|
|
893
|
+
type="password"
|
|
894
|
+
name="token"
|
|
895
|
+
class="image-input"
|
|
896
|
+
bind:value={customToken}
|
|
897
|
+
spellcheck="false"
|
|
898
|
+
autocapitalize="off"
|
|
899
|
+
autocorrect="off"
|
|
900
|
+
autocomplete="off"
|
|
901
|
+
placeholder={customTokenSaved ? '•••••••• (saved)' : 'sk-…'}
|
|
902
|
+
/>
|
|
903
|
+
<Button type="submit" disabled={savingCustomToken}>Save</Button>
|
|
904
|
+
</div>
|
|
905
|
+
{#if customTokenError}
|
|
906
|
+
<div class="msg error">{customTokenError}</div>
|
|
907
|
+
{:else if customTokenMsg}
|
|
908
|
+
<div class="msg ok">{customTokenMsg}</div>
|
|
909
|
+
{/if}
|
|
910
|
+
</form>
|
|
911
|
+
|
|
912
|
+
<form
|
|
913
|
+
class="row divided token-row"
|
|
914
|
+
method="POST"
|
|
915
|
+
action="?/customModels"
|
|
916
|
+
{@attach enhance(customModelsOpts)}
|
|
917
|
+
>
|
|
918
|
+
<div class="label">
|
|
919
|
+
<div class="text">
|
|
920
|
+
<div class="name">Model IDs</div>
|
|
921
|
+
<div class="desc">
|
|
922
|
+
Model aliases to use for each tier. Prefilled with defaults from the reference
|
|
923
|
+
launcher script. Leave a field as-is to keep its current value.
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="model-fields">
|
|
928
|
+
<label class="model-row">
|
|
929
|
+
<span class="model-label">Opus</span>
|
|
930
|
+
<input
|
|
931
|
+
type="text"
|
|
932
|
+
name="opusModel"
|
|
933
|
+
class="image-input"
|
|
934
|
+
bind:value={customOpusModel}
|
|
935
|
+
spellcheck="false"
|
|
936
|
+
autocapitalize="off"
|
|
937
|
+
autocorrect="off"
|
|
938
|
+
autocomplete="off"
|
|
939
|
+
/>
|
|
940
|
+
</label>
|
|
941
|
+
<label class="model-row">
|
|
942
|
+
<span class="model-label">Sonnet</span>
|
|
943
|
+
<input
|
|
944
|
+
type="text"
|
|
945
|
+
name="sonnetModel"
|
|
946
|
+
class="image-input"
|
|
947
|
+
bind:value={customSonnetModel}
|
|
948
|
+
spellcheck="false"
|
|
949
|
+
autocapitalize="off"
|
|
950
|
+
autocorrect="off"
|
|
951
|
+
autocomplete="off"
|
|
952
|
+
/>
|
|
953
|
+
</label>
|
|
954
|
+
<label class="model-row">
|
|
955
|
+
<span class="model-label">Haiku</span>
|
|
956
|
+
<input
|
|
957
|
+
type="text"
|
|
958
|
+
name="haikuModel"
|
|
959
|
+
class="image-input"
|
|
960
|
+
bind:value={customHaikuModel}
|
|
961
|
+
spellcheck="false"
|
|
962
|
+
autocapitalize="off"
|
|
963
|
+
autocorrect="off"
|
|
964
|
+
autocomplete="off"
|
|
965
|
+
/>
|
|
966
|
+
</label>
|
|
967
|
+
<label class="model-row">
|
|
968
|
+
<span class="model-label">Small/fast</span>
|
|
969
|
+
<input
|
|
970
|
+
type="text"
|
|
971
|
+
name="smallFastModel"
|
|
972
|
+
class="image-input"
|
|
973
|
+
bind:value={customSmallFastModel}
|
|
974
|
+
spellcheck="false"
|
|
975
|
+
autocapitalize="off"
|
|
976
|
+
autocorrect="off"
|
|
977
|
+
autocomplete="off"
|
|
978
|
+
/>
|
|
979
|
+
</label>
|
|
980
|
+
<label class="model-row">
|
|
981
|
+
<span class="model-label">Default</span>
|
|
982
|
+
<input
|
|
983
|
+
type="text"
|
|
984
|
+
name="defaultModel"
|
|
985
|
+
class="image-input"
|
|
986
|
+
bind:value={customDefaultModel}
|
|
987
|
+
spellcheck="false"
|
|
988
|
+
autocapitalize="off"
|
|
989
|
+
autocorrect="off"
|
|
990
|
+
autocomplete="off"
|
|
991
|
+
/>
|
|
992
|
+
</label>
|
|
993
|
+
<div class="model-save-row">
|
|
994
|
+
<Button type="submit" disabled={savingCustomModels}>Save models</Button>
|
|
995
|
+
</div>
|
|
996
|
+
</div>
|
|
997
|
+
{#if customModelsError}
|
|
998
|
+
<div class="msg error">{customModelsError}</div>
|
|
999
|
+
{:else if customModelsMsg}
|
|
1000
|
+
<div class="msg ok">{customModelsMsg}</div>
|
|
1001
|
+
{/if}
|
|
1002
|
+
</form>
|
|
1003
|
+
{/if}
|
|
1004
|
+
</section>
|
|
1005
|
+
|
|
1006
|
+
<section class="card">
|
|
1007
|
+
<form
|
|
1008
|
+
class="row"
|
|
1009
|
+
method="POST"
|
|
1010
|
+
action="?/hostEnvVarsToggle"
|
|
1011
|
+
{@attach enhance(hostEnvVarsToggleOpts)}
|
|
1012
|
+
>
|
|
1013
|
+
<div class="label">
|
|
1014
|
+
<Variable size={18} />
|
|
1015
|
+
<div class="text">
|
|
1016
|
+
<div class="name">Host environment variables</div>
|
|
1017
|
+
<div class="desc">
|
|
1018
|
+
Forward selected environment variables from this machine into every new container's
|
|
1019
|
+
interactive shells. Values are read from this process's own environment at container
|
|
1020
|
+
start and are never shown here or stored — only the variable names are saved.
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
<label class="switch">
|
|
1025
|
+
<input
|
|
1026
|
+
type="checkbox"
|
|
1027
|
+
name="enabled"
|
|
1028
|
+
checked={hostEnvVars}
|
|
1029
|
+
disabled={savingHostEnvToggle}
|
|
1030
|
+
onchange={(e) => {
|
|
1031
|
+
hostEnvVars = e.currentTarget.checked;
|
|
1032
|
+
e.currentTarget.form?.requestSubmit();
|
|
1033
|
+
}}
|
|
1034
|
+
/>
|
|
1035
|
+
<span class="track"><span class="thumb"></span></span>
|
|
1036
|
+
</label>
|
|
1037
|
+
</form>
|
|
1038
|
+
{#if hostEnvToggleError}
|
|
1039
|
+
<div class="sub"><div class="msg error">{hostEnvToggleError}</div></div>
|
|
1040
|
+
{/if}
|
|
1041
|
+
|
|
1042
|
+
{#if hostEnvVars}
|
|
1043
|
+
<div class="row divided var-row">
|
|
1044
|
+
<div class="var-list">
|
|
1045
|
+
{#each hostEnvNames as name (name)}
|
|
1046
|
+
<form
|
|
1047
|
+
class="var-item"
|
|
1048
|
+
method="POST"
|
|
1049
|
+
action="?/hostEnvVarNames"
|
|
1050
|
+
{@attach enhance(removeHostEnvOpts(name))}
|
|
1051
|
+
>
|
|
1052
|
+
{#each hostEnvNames.filter((n) => n !== name) as other (other)}
|
|
1053
|
+
<input type="hidden" name="names" value={other} />
|
|
1054
|
+
{/each}
|
|
1055
|
+
<span class="var-name">{name}</span>
|
|
1056
|
+
<span
|
|
1057
|
+
class="var-status"
|
|
1058
|
+
class:present={hostEnvPresence[name]}
|
|
1059
|
+
title={hostEnvPresence[name]
|
|
1060
|
+
? 'Set on this host — will be injected'
|
|
1061
|
+
: 'Not set on this host — will be skipped'}
|
|
1062
|
+
>
|
|
1063
|
+
{hostEnvPresence[name] ? 'set' : 'missing'}
|
|
1064
|
+
</span>
|
|
1065
|
+
<button
|
|
1066
|
+
type="submit"
|
|
1067
|
+
class="var-remove"
|
|
1068
|
+
disabled={savingHostEnvNames}
|
|
1069
|
+
aria-label={`Remove ${name}`}
|
|
1070
|
+
>
|
|
1071
|
+
<X size={13} />
|
|
1072
|
+
</button>
|
|
1073
|
+
</form>
|
|
1074
|
+
{:else}
|
|
1075
|
+
<div class="var-empty">No variables added yet.</div>
|
|
1076
|
+
{/each}
|
|
1077
|
+
</div>
|
|
1078
|
+
<form
|
|
1079
|
+
class="var-add"
|
|
1080
|
+
method="POST"
|
|
1081
|
+
action="?/hostEnvVarNames"
|
|
1082
|
+
{@attach enhance(addHostEnvOpts)}
|
|
1083
|
+
>
|
|
1084
|
+
{#each hostEnvNames as name (name)}
|
|
1085
|
+
<input type="hidden" name="names" value={name} />
|
|
1086
|
+
{/each}
|
|
1087
|
+
<input
|
|
1088
|
+
type="text"
|
|
1089
|
+
name="names"
|
|
1090
|
+
class="image-input"
|
|
1091
|
+
bind:value={newHostEnvName}
|
|
1092
|
+
spellcheck="false"
|
|
1093
|
+
autocapitalize="off"
|
|
1094
|
+
autocorrect="off"
|
|
1095
|
+
autocomplete="off"
|
|
1096
|
+
placeholder="AWS_ACCESS_KEY_ID"
|
|
1097
|
+
/>
|
|
1098
|
+
<Button type="submit" icon={Plus} disabled={savingHostEnvNames}>Add</Button>
|
|
1099
|
+
</form>
|
|
1100
|
+
{#if hostEnvNamesError}
|
|
1101
|
+
<div class="msg error">{hostEnvNamesError}</div>
|
|
1102
|
+
{:else if hostEnvNamesMsg}
|
|
1103
|
+
<div class="msg ok">{hostEnvNamesMsg}</div>
|
|
1104
|
+
{/if}
|
|
1105
|
+
</div>
|
|
1106
|
+
{/if}
|
|
1107
|
+
</section>
|
|
1108
|
+
|
|
1109
|
+
<section class="card">
|
|
1110
|
+
<div class="row">
|
|
1111
|
+
<div class="label">
|
|
1112
|
+
<SunMoon size={18} />
|
|
1113
|
+
<div class="text">
|
|
1114
|
+
<div class="name">Theme</div>
|
|
1115
|
+
<div class="desc">
|
|
1116
|
+
Light, dark, or follow the browser preference automatically (auto).
|
|
1117
|
+
</div>
|
|
1118
|
+
</div>
|
|
1119
|
+
</div>
|
|
1120
|
+
<ThemePicker />
|
|
1121
|
+
</div>
|
|
1122
|
+
</section>
|
|
1123
|
+
|
|
1124
|
+
<section class="card">
|
|
1125
|
+
<div class="row">
|
|
1126
|
+
<div class="label">
|
|
1127
|
+
<Volume2 size={18} />
|
|
1128
|
+
<div class="text">
|
|
1129
|
+
<div class="name">Attention sound</div>
|
|
1130
|
+
<div class="desc">
|
|
1131
|
+
Play a chime when an instance finishes a task or needs your input.
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
</div>
|
|
1135
|
+
<label class="switch">
|
|
1136
|
+
<input
|
|
1137
|
+
type="checkbox"
|
|
1138
|
+
checked={sound}
|
|
1139
|
+
onchange={(e) => toggleSound(e.currentTarget.checked)}
|
|
1140
|
+
/>
|
|
1141
|
+
<span class="track"><span class="thumb"></span></span>
|
|
1142
|
+
</label>
|
|
1143
|
+
</div>
|
|
1144
|
+
</section>
|
|
1145
|
+
|
|
1146
|
+
<section class="card danger-card">
|
|
1147
|
+
<form class="row" method="POST" action="?/shutdown" {@attach enhance(shutdownOpts)}>
|
|
1148
|
+
<div class="label">
|
|
1149
|
+
<Power size={18} />
|
|
1150
|
+
<div class="text">
|
|
1151
|
+
<div class="name">Delete database, containers, and shut down</div>
|
|
1152
|
+
<div class="desc">
|
|
1153
|
+
Stop and remove every instance and its container, delete all copied workspaces and the
|
|
1154
|
+
database, then shut down the server. This cannot be undone.
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
{#if shuttingDown}
|
|
1159
|
+
<span class="shutting">Server is shutting down — you can close this tab.</span>
|
|
1160
|
+
{:else}
|
|
1161
|
+
<Button type="submit" variant="danger">Delete & shut down</Button>
|
|
1162
|
+
{/if}
|
|
1163
|
+
</form>
|
|
1164
|
+
</section>
|
|
1165
|
+
|
|
1166
|
+
<CoinButton />
|
|
1167
|
+
</main>
|
|
1168
|
+
</div>
|
|
1169
|
+
|
|
1170
|
+
<!-- Settings renders outside AppShell, which hosts the app-wide Toaster. -->
|
|
1171
|
+
<Toaster
|
|
1172
|
+
toastOptions={{
|
|
1173
|
+
style:
|
|
1174
|
+
'border:1px solid var(--ink); background:var(--bg-card); color:var(--ink); box-shadow:4px 4px 0 var(--ink); font-family:var(--font-mono); font-size:13px;'
|
|
1175
|
+
}}
|
|
1176
|
+
/>
|
|
1177
|
+
|
|
1178
|
+
<style>
|
|
1179
|
+
.page {
|
|
1180
|
+
display: flex;
|
|
1181
|
+
flex-direction: column;
|
|
1182
|
+
min-height: 100vh;
|
|
1183
|
+
}
|
|
1184
|
+
.title {
|
|
1185
|
+
display: inline-flex;
|
|
1186
|
+
align-items: center;
|
|
1187
|
+
padding: 0 14px;
|
|
1188
|
+
font-family: var(--font-mono);
|
|
1189
|
+
font-size: 12px;
|
|
1190
|
+
text-transform: uppercase;
|
|
1191
|
+
letter-spacing: 0.08em;
|
|
1192
|
+
color: var(--ink-soft);
|
|
1193
|
+
}
|
|
1194
|
+
.content {
|
|
1195
|
+
flex: 1;
|
|
1196
|
+
display: flex;
|
|
1197
|
+
flex-direction: column;
|
|
1198
|
+
align-items: center;
|
|
1199
|
+
gap: 16px;
|
|
1200
|
+
padding: 32px 20px;
|
|
1201
|
+
}
|
|
1202
|
+
.card {
|
|
1203
|
+
width: 100%;
|
|
1204
|
+
max-width: 560px;
|
|
1205
|
+
background: var(--bg-card);
|
|
1206
|
+
border: 1px solid var(--rule);
|
|
1207
|
+
height: max-content;
|
|
1208
|
+
}
|
|
1209
|
+
.danger-card {
|
|
1210
|
+
border-color: var(--danger);
|
|
1211
|
+
}
|
|
1212
|
+
.disabled-card {
|
|
1213
|
+
opacity: 0.55;
|
|
1214
|
+
pointer-events: none;
|
|
1215
|
+
}
|
|
1216
|
+
/* Keep the action button on one line; in the flex row it would otherwise shrink and wrap. */
|
|
1217
|
+
.danger-card :global(.btn) {
|
|
1218
|
+
flex: none;
|
|
1219
|
+
white-space: nowrap;
|
|
1220
|
+
}
|
|
1221
|
+
.shutting {
|
|
1222
|
+
flex: none;
|
|
1223
|
+
max-width: 200px;
|
|
1224
|
+
font-family: var(--font-mono);
|
|
1225
|
+
font-size: 12px;
|
|
1226
|
+
color: var(--danger);
|
|
1227
|
+
line-height: 1.4;
|
|
1228
|
+
text-align: right;
|
|
1229
|
+
}
|
|
1230
|
+
.row {
|
|
1231
|
+
display: flex;
|
|
1232
|
+
align-items: center;
|
|
1233
|
+
justify-content: space-between;
|
|
1234
|
+
gap: 16px;
|
|
1235
|
+
padding: 18px 18px;
|
|
1236
|
+
}
|
|
1237
|
+
.row.divided {
|
|
1238
|
+
border-top: 1px solid var(--rule);
|
|
1239
|
+
align-items: flex-start;
|
|
1240
|
+
}
|
|
1241
|
+
.row.divided :global(.btn) {
|
|
1242
|
+
flex: none;
|
|
1243
|
+
white-space: nowrap;
|
|
1244
|
+
}
|
|
1245
|
+
.sub {
|
|
1246
|
+
padding: 0 18px 14px;
|
|
1247
|
+
}
|
|
1248
|
+
code {
|
|
1249
|
+
font-family: var(--font-mono);
|
|
1250
|
+
font-size: 0.92em;
|
|
1251
|
+
padding: 1px 4px;
|
|
1252
|
+
background: var(--bg);
|
|
1253
|
+
border: 1px solid var(--rule);
|
|
1254
|
+
border-radius: 3px;
|
|
1255
|
+
}
|
|
1256
|
+
.label {
|
|
1257
|
+
display: flex;
|
|
1258
|
+
align-items: flex-start;
|
|
1259
|
+
gap: 12px;
|
|
1260
|
+
color: var(--ink);
|
|
1261
|
+
min-width: 0;
|
|
1262
|
+
}
|
|
1263
|
+
.text {
|
|
1264
|
+
min-width: 0;
|
|
1265
|
+
}
|
|
1266
|
+
.name {
|
|
1267
|
+
font-family: var(--font-mono);
|
|
1268
|
+
font-size: 13px;
|
|
1269
|
+
text-transform: uppercase;
|
|
1270
|
+
letter-spacing: 0.06em;
|
|
1271
|
+
}
|
|
1272
|
+
.arch {
|
|
1273
|
+
margin-left: 8px;
|
|
1274
|
+
padding: 2px 6px;
|
|
1275
|
+
font-size: 11px;
|
|
1276
|
+
letter-spacing: 0.04em;
|
|
1277
|
+
color: var(--ink-soft);
|
|
1278
|
+
border: 1px solid var(--rule);
|
|
1279
|
+
border-radius: 3px;
|
|
1280
|
+
vertical-align: middle;
|
|
1281
|
+
}
|
|
1282
|
+
.tip {
|
|
1283
|
+
width: 100%;
|
|
1284
|
+
margin-top: 4px;
|
|
1285
|
+
padding: 8px 10px;
|
|
1286
|
+
color: var(--ink-soft);
|
|
1287
|
+
background: color-mix(in srgb, var(--info) 10%, transparent);
|
|
1288
|
+
border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
|
|
1289
|
+
border-radius: 4px;
|
|
1290
|
+
}
|
|
1291
|
+
.desc {
|
|
1292
|
+
margin-top: 4px;
|
|
1293
|
+
font-family: var(--font-mono);
|
|
1294
|
+
font-size: 12px;
|
|
1295
|
+
color: var(--ink-faint);
|
|
1296
|
+
line-height: 1.4;
|
|
1297
|
+
}
|
|
1298
|
+
.image-row {
|
|
1299
|
+
flex-wrap: wrap;
|
|
1300
|
+
}
|
|
1301
|
+
/* Manual-token rows: same input+Save layout, allowed to wrap so the help text and
|
|
1302
|
+
the "Saved." message drop below the field on narrow widths. */
|
|
1303
|
+
.token-row {
|
|
1304
|
+
flex-wrap: wrap;
|
|
1305
|
+
}
|
|
1306
|
+
.image-controls {
|
|
1307
|
+
display: flex;
|
|
1308
|
+
align-items: center;
|
|
1309
|
+
gap: 8px;
|
|
1310
|
+
flex: 1;
|
|
1311
|
+
min-width: 220px;
|
|
1312
|
+
justify-content: flex-end;
|
|
1313
|
+
}
|
|
1314
|
+
.image-input {
|
|
1315
|
+
flex: 1;
|
|
1316
|
+
min-width: 0;
|
|
1317
|
+
padding: 8px 10px;
|
|
1318
|
+
font-family: var(--font-mono);
|
|
1319
|
+
font-size: 12px;
|
|
1320
|
+
color: var(--ink);
|
|
1321
|
+
background: var(--bg);
|
|
1322
|
+
border: 1px solid var(--rule);
|
|
1323
|
+
}
|
|
1324
|
+
.image-input:focus-visible {
|
|
1325
|
+
outline: 2px solid var(--ink);
|
|
1326
|
+
outline-offset: 1px;
|
|
1327
|
+
}
|
|
1328
|
+
.image-controls :global(.btn) {
|
|
1329
|
+
flex: none;
|
|
1330
|
+
}
|
|
1331
|
+
.msg {
|
|
1332
|
+
width: 100%;
|
|
1333
|
+
margin-top: 2px;
|
|
1334
|
+
font-family: var(--font-mono);
|
|
1335
|
+
font-size: 12px;
|
|
1336
|
+
line-height: 1.4;
|
|
1337
|
+
}
|
|
1338
|
+
.msg.error {
|
|
1339
|
+
color: var(--danger);
|
|
1340
|
+
}
|
|
1341
|
+
.msg.ok {
|
|
1342
|
+
color: var(--ink-soft);
|
|
1343
|
+
}
|
|
1344
|
+
.switch {
|
|
1345
|
+
position: relative;
|
|
1346
|
+
flex: none;
|
|
1347
|
+
cursor: pointer;
|
|
1348
|
+
}
|
|
1349
|
+
.switch input {
|
|
1350
|
+
position: absolute;
|
|
1351
|
+
opacity: 0;
|
|
1352
|
+
inset: 0;
|
|
1353
|
+
margin: 0;
|
|
1354
|
+
cursor: pointer;
|
|
1355
|
+
}
|
|
1356
|
+
.track {
|
|
1357
|
+
display: block;
|
|
1358
|
+
width: 44px;
|
|
1359
|
+
height: 24px;
|
|
1360
|
+
border: 1px solid var(--rule);
|
|
1361
|
+
background: var(--bg);
|
|
1362
|
+
border-radius: 999px;
|
|
1363
|
+
transition: background 0.15s ease;
|
|
1364
|
+
}
|
|
1365
|
+
.thumb {
|
|
1366
|
+
display: block;
|
|
1367
|
+
width: 18px;
|
|
1368
|
+
height: 18px;
|
|
1369
|
+
margin: 2px;
|
|
1370
|
+
background: var(--ink-faint);
|
|
1371
|
+
border-radius: 999px;
|
|
1372
|
+
transition:
|
|
1373
|
+
transform 0.15s ease,
|
|
1374
|
+
background 0.15s ease;
|
|
1375
|
+
}
|
|
1376
|
+
.switch input:checked + .track {
|
|
1377
|
+
background: var(--switch-on-bg);
|
|
1378
|
+
}
|
|
1379
|
+
.switch input:checked + .track .thumb {
|
|
1380
|
+
transform: translateX(20px);
|
|
1381
|
+
background: var(--ink);
|
|
1382
|
+
}
|
|
1383
|
+
.switch input:focus-visible + .track {
|
|
1384
|
+
outline: 2px solid var(--ink);
|
|
1385
|
+
outline-offset: 2px;
|
|
1386
|
+
}
|
|
1387
|
+
.model-fields {
|
|
1388
|
+
display: flex;
|
|
1389
|
+
flex-direction: column;
|
|
1390
|
+
gap: 6px;
|
|
1391
|
+
flex: 1;
|
|
1392
|
+
min-width: 220px;
|
|
1393
|
+
}
|
|
1394
|
+
.model-row {
|
|
1395
|
+
display: flex;
|
|
1396
|
+
align-items: center;
|
|
1397
|
+
gap: 8px;
|
|
1398
|
+
}
|
|
1399
|
+
.model-label {
|
|
1400
|
+
flex: none;
|
|
1401
|
+
width: 72px;
|
|
1402
|
+
font-family: var(--font-mono);
|
|
1403
|
+
font-size: 11px;
|
|
1404
|
+
color: var(--ink-soft);
|
|
1405
|
+
text-align: right;
|
|
1406
|
+
}
|
|
1407
|
+
.model-row .image-input {
|
|
1408
|
+
flex: 1;
|
|
1409
|
+
}
|
|
1410
|
+
.model-save-row {
|
|
1411
|
+
display: flex;
|
|
1412
|
+
justify-content: flex-end;
|
|
1413
|
+
margin-top: 4px;
|
|
1414
|
+
}
|
|
1415
|
+
.var-row {
|
|
1416
|
+
flex-direction: column;
|
|
1417
|
+
align-items: stretch;
|
|
1418
|
+
gap: 10px;
|
|
1419
|
+
}
|
|
1420
|
+
.var-list {
|
|
1421
|
+
display: flex;
|
|
1422
|
+
flex-direction: column;
|
|
1423
|
+
gap: 6px;
|
|
1424
|
+
}
|
|
1425
|
+
.var-item {
|
|
1426
|
+
display: flex;
|
|
1427
|
+
align-items: center;
|
|
1428
|
+
gap: 8px;
|
|
1429
|
+
padding: 6px 8px;
|
|
1430
|
+
background: var(--bg);
|
|
1431
|
+
border: 1px solid var(--rule);
|
|
1432
|
+
}
|
|
1433
|
+
.var-name {
|
|
1434
|
+
flex: 1;
|
|
1435
|
+
min-width: 0;
|
|
1436
|
+
font-family: var(--font-mono);
|
|
1437
|
+
font-size: 12px;
|
|
1438
|
+
color: var(--ink);
|
|
1439
|
+
word-break: break-all;
|
|
1440
|
+
}
|
|
1441
|
+
.var-status {
|
|
1442
|
+
flex: none;
|
|
1443
|
+
font-family: var(--font-mono);
|
|
1444
|
+
font-size: 11px;
|
|
1445
|
+
letter-spacing: 0.04em;
|
|
1446
|
+
color: var(--danger);
|
|
1447
|
+
}
|
|
1448
|
+
.var-status.present {
|
|
1449
|
+
color: var(--ink-soft);
|
|
1450
|
+
}
|
|
1451
|
+
.var-remove {
|
|
1452
|
+
flex: none;
|
|
1453
|
+
display: inline-flex;
|
|
1454
|
+
align-items: center;
|
|
1455
|
+
justify-content: center;
|
|
1456
|
+
width: 22px;
|
|
1457
|
+
height: 22px;
|
|
1458
|
+
padding: 0;
|
|
1459
|
+
color: var(--ink-soft);
|
|
1460
|
+
background: transparent;
|
|
1461
|
+
border: 1px solid var(--rule);
|
|
1462
|
+
cursor: pointer;
|
|
1463
|
+
}
|
|
1464
|
+
.var-remove:hover:not(:disabled) {
|
|
1465
|
+
color: var(--danger);
|
|
1466
|
+
border-color: var(--danger);
|
|
1467
|
+
}
|
|
1468
|
+
.var-remove:disabled {
|
|
1469
|
+
opacity: 0.4;
|
|
1470
|
+
cursor: not-allowed;
|
|
1471
|
+
}
|
|
1472
|
+
.var-empty {
|
|
1473
|
+
font-family: var(--font-mono);
|
|
1474
|
+
font-size: 12px;
|
|
1475
|
+
color: var(--ink-faint);
|
|
1476
|
+
}
|
|
1477
|
+
.var-add {
|
|
1478
|
+
display: flex;
|
|
1479
|
+
gap: 8px;
|
|
1480
|
+
}
|
|
1481
|
+
.var-add :global(.btn) {
|
|
1482
|
+
flex: none;
|
|
1483
|
+
}
|
|
1484
|
+
</style>
|