codebay 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mochi/manifest.json +114 -106
- package/.mochi/svelte-client/HydratableIsland-4j3bazeq.js +1 -0
- package/.mochi/svelte-client/{_hydrate-AppShell_2rtnxn6pgp2rh-v65ctd99.js → _hydrate-AppShell_2rtnxn6pgp2rh-aqm4k957.js} +2 -2
- package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-htjfv8q9.js → _hydrate-AvatarsView_257s9yq1wwr4f-m7h7xdxn.js} +1 -1
- package/.mochi/svelte-client/{_hydrate-InstanceView_2ma45l9bybfhl-v07seqge.js → _hydrate-InstanceView_2ma45l9bybfhl-d256bw54.js} +1 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-pwbtv8ma.js +39 -0
- package/.mochi/svelte-client/{chunk-wjagstre.js → chunk-1ttqgrz7.js} +1 -1
- package/.mochi/svelte-client/{chunk-czkxd74n.js → chunk-2eaagsfc.js} +1 -1
- package/.mochi/svelte-client/chunk-6v5d7dex.js +2 -0
- package/.mochi/svelte-client/{chunk-qx1defbm.js → chunk-a5e8vytv.js} +2 -2
- package/.mochi/svelte-client/{chunk-hxydjtda.js → chunk-d8rdrqsh.js} +1 -1
- package/.mochi/svelte-client/{chunk-566rqq4c.js → chunk-d9bszcp8.js} +1 -1
- package/.mochi/svelte-client/chunk-krevp8yb.js +2 -0
- package/.mochi/svelte-client/{chunk-0zkdsrth.js → chunk-s70rry6z.js} +1 -1
- package/.mochi/svelte-client/{chunk-79aj59e0.js → chunk-wcfbjj3x.js} +2 -2
- package/.mochi/svelte-client/{chunk-y9yy1004.js → chunk-y1xjps59.js} +1 -1
- package/.mochi/svelte-compile/{App-mzn3dqjm.server.js → App-bhak71kt.server.js} +4 -3
- package/.mochi/svelte-compile/{Settings-tvc5yp08.server.js → Settings-j3aygrjb.server.js} +178 -147
- package/.mochi/svelte-compile/{chunk-bpqwf1nw.js → chunk-pqb403fh.js} +12 -1
- package/.mochi/svelte-css/AppShell-22m1mjm8nq2hn.css +1 -0
- package/.mochi/svelte-css/{InstanceView-oozmmctp8m3p.css → InstanceView-32j12hyg8v9u0.css} +1 -1
- package/package.json +1 -1
- package/src/components/AppShell.svelte +5 -1
- package/src/components/InstanceView.svelte +1 -1
- package/src/components/SettingsView.svelte +36 -1
- package/src/components/UiShowcase.svelte +4 -1
- package/src/container-injections/code-server-dark.ts +216 -0
- package/src/lib/devcontainer.server.ts +37 -16
- package/src/lib/injections.server.ts +4 -2
- package/src/lib/instances.server.ts +6 -4
- package/src/settings.ts +13 -0
- package/src/shell.html +21 -6
- package/.mochi/svelte-client/HydratableIsland-dammyce5.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-hj9yvqc4.js +0 -38
- package/.mochi/svelte-client/chunk-r3wxmmc0.js +0 -2
- package/.mochi/svelte-client/chunk-vs6pd2b4.js +0 -2
- package/.mochi/svelte-css/AppShell-36mpg5smnbmuw.css +0 -1
|
@@ -55,6 +55,17 @@ function setSoundEnabled(on) {
|
|
|
55
55
|
return;
|
|
56
56
|
localStorage.setItem(SOUND_KEY, on ? "on" : "off");
|
|
57
57
|
}
|
|
58
|
+
var TAB_SHORTCUTS_KEY = "codebay.tabShortcuts";
|
|
59
|
+
function tabShortcutsEnabled() {
|
|
60
|
+
if (typeof localStorage === "undefined")
|
|
61
|
+
return true;
|
|
62
|
+
return localStorage.getItem(TAB_SHORTCUTS_KEY) !== "off";
|
|
63
|
+
}
|
|
64
|
+
function setTabShortcutsEnabled(on) {
|
|
65
|
+
if (typeof localStorage === "undefined")
|
|
66
|
+
return;
|
|
67
|
+
localStorage.setItem(TAB_SHORTCUTS_KEY, on ? "on" : "off");
|
|
68
|
+
}
|
|
58
69
|
|
|
59
70
|
// src/sound.ts
|
|
60
71
|
var SOUNDS = {
|
|
@@ -110,4 +121,4 @@ function playChime(state) {
|
|
|
110
121
|
el.play().then(() => console.log(`[chime] playChime(${state}) \u2014 playing`)).catch((err) => console.warn(`[chime] playChime(${state}) \u2014 play rejected:`, err?.message ?? err));
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
export { Terminal, Layout_template, Plus, soundEnabled, setSoundEnabled, unlockAudio, playChime };
|
|
124
|
+
export { Terminal, Layout_template, Plus, soundEnabled, setSoundEnabled, tabShortcutsEnabled, setTabShortcutsEnabled, unlockAudio, playChime };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.app.ide.svelte-1duc7gd{display:flex;flex-direction:column;height:100vh}.panes.svelte-1duc7gd{position:relative;flex:1;min-height:0}.panes.hidden.svelte-1duc7gd{display:none}.pane.svelte-1duc7gd{position:absolute;inset:0}.pane.svelte-1duc7gd:not(.active){display:none}iframe.svelte-1duc7gd{display:block;--buncss-light: ;--buncss-dark:initial;color-scheme:dark;border:0;width:100%;height:100%}.empty.svelte-1duc7gd{position:absolute;display:flex;color:var(--ink-faint);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;justify-content:center;align-items: center;font-size:13px;inset:0}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.topbar.svelte-15fxkvm{display:flex;border-bottom:1px solid var(--rule);align-items: center;gap:18px;padding:16px 24px}.back.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);text-decoration:none;align-items: center;gap:5px;font-size:12px}.back.svelte-15fxkvm:hover{color:var(--ink)}.title.svelte-15fxkvm{display:flex;flex:1;align-items: center;gap:12px}.name.svelte-15fxkvm{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.03em;font-size:20px;font-weight:700}.open.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);text-decoration:none;border:1px solid var(--edge);align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.open.svelte-15fxkvm:hover{background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.restart.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);background:var(--bg);border:1px solid var(--edge);cursor:pointer;align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.restart.svelte-15fxkvm:hover:not(:disabled){background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.restart.svelte-15fxkvm:disabled{opacity:var(--dim-panel);cursor:not-allowed}.restart-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:0;padding:8px 24px 0;font-size:12px}.stage.svelte-15fxkvm{max-width:1200px;margin:0 auto;padding:20px 24px 40px}.meta.svelte-15fxkvm{display:grid;grid-template-columns:max-content 1fr;align-items: center;gap:6px 14px;margin-bottom:18px}.meta.svelte-15fxkvm .k:where(.svelte-15fxkvm){font-family:var(--font-display);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);font-size:12px;font-weight:700}.meta.svelte-15fxkvm code:where(.svelte-15fxkvm){font-family:var(--font-mono);color:var(--ink-soft);font-size:13px}.meta.svelte-15fxkvm code.muted:where(.svelte-15fxkvm){color:var(--ink-faint)}.healthslot.svelte-15fxkvm,.ports.svelte-15fxkvm{margin-bottom:18px}.ports-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.rebuild.svelte-15fxkvm{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--fill-ink);color:var(--fill);border:1px solid var(--fill-ink);cursor:pointer;padding:4px 9px;font-size:10px;font-weight:600}.rebuild.svelte-15fxkvm:hover:not(:disabled){color:var(--fill-ink);background:0 0}.rebuild.svelte-15fxkvm:disabled{opacity:var(--dim-panel);cursor:not-allowed}.ports-body.svelte-15fxkvm{background:var(--bg-card);padding:12px 14px 14px}.port-list.svelte-15fxkvm{list-style:none;display:flex;flex-direction:column;gap:6px;margin:0 0 12px;padding:0}.port-list.svelte-15fxkvm li:where(.svelte-15fxkvm){display:flex;font-family:var(--font-mono);align-items: center;gap:8px;font-size:13px}.port-list.svelte-15fxkvm .cp:where(.svelte-15fxkvm){color:var(--ink);font-weight:600}.port-list.svelte-15fxkvm .arr:where(.svelte-15fxkvm){color:var(--ink-faint)}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm){display:inline-flex;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--ink-faint);align-items: center;gap:4px}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm):hover{border-bottom-color:var(--ink)}.rm.svelte-15fxkvm{border:1px solid var(--ink-faint);color:var(--ink-soft);cursor:pointer;background:0 0;width:20px;height:20px;margin-left:auto;font-size:15px;line-height:1}.rm.svelte-15fxkvm:hover{border-color:var(--danger);color:var(--danger)}.empty.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:0 0 12px;font-size:12px}.add.svelte-15fxkvm{display:flex;gap:8px}.add.svelte-15fxkvm input:where(.svelte-15fxkvm){font-family:var(--font-mono);border:1px solid var(--ink);background:var(--bg);color:var(--ink);outline:none;flex:1;min-width:0;padding:6px 9px;font-size:13px}.add.svelte-15fxkvm button:where(.svelte-15fxkvm){font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--edge);background:var(--bg-card);color:var(--ink);cursor:pointer;padding:6px 14px;font-size:12px;font-weight:600}.add.svelte-15fxkvm button:where(.svelte-15fxkvm):hover{background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.port-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:10px 0 0;font-size:12px}.hint.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:10px 0 0;font-size:11px}.hint.pending.svelte-15fxkvm{color:var(--ink-soft)}.hint.svelte-15fxkvm code:where(.svelte-15fxkvm){color:var(--ink)}.logwrap.svelte-15fxkvm{overflow:hidden}.log-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.log-actions.svelte-15fxkvm{display:flex;align-items: center;gap:6px}.copy.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--fill-ink);color:var(--fill);border:1px solid var(--fill-ink);cursor:pointer;align-items: center;gap:5px;padding:4px 9px;font-size:10px;font-weight:600}.copy.svelte-15fxkvm:hover:not(:disabled),.copy.active.svelte-15fxkvm{color:var(--fill-ink);background:0 0}.dot.svelte-15fxkvm{background:var(--
|
|
1
|
+
.topbar.svelte-15fxkvm{display:flex;border-bottom:1px solid var(--rule);align-items: center;gap:18px;padding:16px 24px}.back.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);text-decoration:none;align-items: center;gap:5px;font-size:12px}.back.svelte-15fxkvm:hover{color:var(--ink)}.title.svelte-15fxkvm{display:flex;flex:1;align-items: center;gap:12px}.name.svelte-15fxkvm{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.03em;font-size:20px;font-weight:700}.open.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);text-decoration:none;border:1px solid var(--edge);align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.open.svelte-15fxkvm:hover{background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.restart.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;color:var(--ink);background:var(--bg);border:1px solid var(--edge);cursor:pointer;align-items: center;gap:6px;padding:7px 12px;font-size:12px;font-weight:600}.restart.svelte-15fxkvm:hover:not(:disabled){background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.restart.svelte-15fxkvm:disabled{opacity:var(--dim-panel);cursor:not-allowed}.restart-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:0;padding:8px 24px 0;font-size:12px}.stage.svelte-15fxkvm{max-width:1200px;margin:0 auto;padding:20px 24px 40px}.meta.svelte-15fxkvm{display:grid;grid-template-columns:max-content 1fr;align-items: center;gap:6px 14px;margin-bottom:18px}.meta.svelte-15fxkvm .k:where(.svelte-15fxkvm){font-family:var(--font-display);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-faint);font-size:12px;font-weight:700}.meta.svelte-15fxkvm code:where(.svelte-15fxkvm){font-family:var(--font-mono);color:var(--ink-soft);font-size:13px}.meta.svelte-15fxkvm code.muted:where(.svelte-15fxkvm){color:var(--ink-faint)}.healthslot.svelte-15fxkvm,.ports.svelte-15fxkvm{margin-bottom:18px}.ports-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.rebuild.svelte-15fxkvm{font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--fill-ink);color:var(--fill);border:1px solid var(--fill-ink);cursor:pointer;padding:4px 9px;font-size:10px;font-weight:600}.rebuild.svelte-15fxkvm:hover:not(:disabled){color:var(--fill-ink);background:0 0}.rebuild.svelte-15fxkvm:disabled{opacity:var(--dim-panel);cursor:not-allowed}.ports-body.svelte-15fxkvm{background:var(--bg-card);padding:12px 14px 14px}.port-list.svelte-15fxkvm{list-style:none;display:flex;flex-direction:column;gap:6px;margin:0 0 12px;padding:0}.port-list.svelte-15fxkvm li:where(.svelte-15fxkvm){display:flex;font-family:var(--font-mono);align-items: center;gap:8px;font-size:13px}.port-list.svelte-15fxkvm .cp:where(.svelte-15fxkvm){color:var(--ink);font-weight:600}.port-list.svelte-15fxkvm .arr:where(.svelte-15fxkvm){color:var(--ink-faint)}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm){display:inline-flex;color:var(--ink);text-decoration:none;border-bottom:1px solid var(--ink-faint);align-items: center;gap:4px}.port-list.svelte-15fxkvm a:where(.svelte-15fxkvm):hover{border-bottom-color:var(--ink)}.rm.svelte-15fxkvm{border:1px solid var(--ink-faint);color:var(--ink-soft);cursor:pointer;background:0 0;width:20px;height:20px;margin-left:auto;font-size:15px;line-height:1}.rm.svelte-15fxkvm:hover{border-color:var(--danger);color:var(--danger)}.empty.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:0 0 12px;font-size:12px}.add.svelte-15fxkvm{display:flex;gap:8px}.add.svelte-15fxkvm input:where(.svelte-15fxkvm){font-family:var(--font-mono);border:1px solid var(--ink);background:var(--bg);color:var(--ink);outline:none;flex:1;min-width:0;padding:6px 9px;font-size:13px}.add.svelte-15fxkvm button:where(.svelte-15fxkvm){font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--edge);background:var(--bg-card);color:var(--ink);cursor:pointer;padding:6px 14px;font-size:12px;font-weight:600}.add.svelte-15fxkvm button:where(.svelte-15fxkvm):hover{background:var(--fill);border-color:var(--fill);color:var(--fill-ink)}.port-err.svelte-15fxkvm{font-family:var(--font-mono);color:var(--danger);margin:10px 0 0;font-size:12px}.hint.svelte-15fxkvm{font-family:var(--font-mono);color:var(--ink-faint);margin:10px 0 0;font-size:11px}.hint.pending.svelte-15fxkvm{color:var(--ink-soft)}.hint.svelte-15fxkvm code:where(.svelte-15fxkvm){color:var(--ink)}.logwrap.svelte-15fxkvm{overflow:hidden}.log-bar.svelte-15fxkvm{display:flex;justify-content:space-between;align-items: center}.log-actions.svelte-15fxkvm{display:flex;align-items: center;gap:6px}.copy.svelte-15fxkvm{display:inline-flex;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em;background:var(--fill-ink);color:var(--fill);border:1px solid var(--fill-ink);cursor:pointer;align-items: center;gap:5px;padding:4px 9px;font-size:10px;font-weight:600}.copy.svelte-15fxkvm:hover:not(:disabled),.copy.active.svelte-15fxkvm{color:var(--fill-ink);background:0 0}.dot.svelte-15fxkvm{background:var(--live-dot);border-radius:50%;flex:none;width:6px;height:6px}.copy.svelte-15fxkvm:disabled{opacity:var(--dim-panel);cursor:not-allowed}.logs.svelte-15fxkvm{position:relative;background:repeating-linear-gradient(0deg,var(--screen-line)0 1px,transparent 1px 3px),var(--screen-bg);color:var(--screen-ink);overflow:auto;overflow-anchor:none;height:calc(100vh - 320px);min-height:360px;padding:14px}.logs.svelte-15fxkvm pre:where(.svelte-15fxkvm){font-family:var(--font-mono);white-space:pre-wrap;word-break:break-word;margin:0;font-size:12.5px;line-height:1.5}@keyframes svelte-15fxkvm-lcd-blink{50%{background:0 0}}.caret.svelte-15fxkvm{display:inline-block;background:var(--screen-ink);vertical-align:text-bottom;animation:svelte-15fxkvm-lcd-blink 1.05s steps(1)infinite;width:.6em;height:1.05em;margin-left:2px}.err.svelte-15fxkvm{background:var(--bg-card);color:var(--ink);border-top:2px solid var(--ink);font-family:var(--font-mono);padding:12px 14px;font-size:13px}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Web UI for spinning up isolated devcontainer instances from any local folder, each with a browser-based VS Code (code-server) — built for parallel Claude Code workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import IdeLoader from './IdeLoader.svelte';
|
|
10
10
|
import TerminalSplit from './TerminalSplit.svelte';
|
|
11
11
|
import { playChime, unlockAudio } from '../sound.ts';
|
|
12
|
+
import { tabShortcutsEnabled } from '../settings.ts';
|
|
12
13
|
import { liveStream } from '../live.ts';
|
|
13
14
|
import { apiPost } from '../api.ts';
|
|
14
15
|
import { syncTheme } from '../theme.ts';
|
|
@@ -189,7 +190,7 @@
|
|
|
189
190
|
// for its own tabs and can't be intercepted, and Ctrl+Alt is AltGr on many European
|
|
190
191
|
// layouts, so it would collide with typing punctuation.
|
|
191
192
|
function jumpToTab(e: KeyboardEvent, fromFrame: boolean) {
|
|
192
|
-
if (!e.altKey || e.ctrlKey || e.metaKey || editingId) return;
|
|
193
|
+
if (!e.altKey || e.ctrlKey || e.metaKey || editingId || !tabShortcutsEnabled()) return;
|
|
193
194
|
// `code`, not `key`: Alt+digit emits punctuation on macOS layouts.
|
|
194
195
|
const digit = /^Digit([1-9])$/.exec(e.code)?.[1];
|
|
195
196
|
if (!digit) return;
|
|
@@ -440,6 +441,9 @@
|
|
|
440
441
|
height: 100%;
|
|
441
442
|
border: 0;
|
|
442
443
|
display: block;
|
|
444
|
+
/* An embedded document takes its prefers-color-scheme from the embedder, so without this
|
|
445
|
+
Codebay's own light theme makes code-server's startup theme resolve light. */
|
|
446
|
+
color-scheme: dark;
|
|
443
447
|
}
|
|
444
448
|
.empty {
|
|
445
449
|
position: absolute;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import Power from '@lucide/svelte/icons/power';
|
|
5
5
|
import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
|
|
6
6
|
import Volume2 from '@lucide/svelte/icons/volume-2';
|
|
7
|
+
import Keyboard from '@lucide/svelte/icons/keyboard';
|
|
7
8
|
import PawPrint from '@lucide/svelte/icons/paw-print';
|
|
8
9
|
import SunMoon from '@lucide/svelte/icons/sun-moon';
|
|
9
10
|
import ThemePicker from './ThemePicker.svelte';
|
|
@@ -27,7 +28,12 @@
|
|
|
27
28
|
import { flushSync } from 'svelte';
|
|
28
29
|
import { enhance } from 'mochi-framework';
|
|
29
30
|
import type { MochiEnhanceOptions } from 'mochi-framework';
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
soundEnabled,
|
|
33
|
+
setSoundEnabled,
|
|
34
|
+
tabShortcutsEnabled,
|
|
35
|
+
setTabShortcutsEnabled
|
|
36
|
+
} from '../settings.ts';
|
|
31
37
|
import { playChime, unlockAudio } from '../sound.ts';
|
|
32
38
|
import Button from './Button.svelte';
|
|
33
39
|
import CoinButton from './CoinButton.svelte';
|
|
@@ -133,6 +139,7 @@
|
|
|
133
139
|
|
|
134
140
|
// Defaults to on during SSR, where localStorage doesn't exist.
|
|
135
141
|
let sound = $state(soundEnabled());
|
|
142
|
+
let tabShortcuts = $state(tabShortcutsEnabled());
|
|
136
143
|
|
|
137
144
|
// DB-backed, so it initializes from the prop. Undefined is the off state — the header keeps its box logo.
|
|
138
145
|
// svelte-ignore state_referenced_locally
|
|
@@ -780,6 +787,11 @@
|
|
|
780
787
|
if (on) playChime('done');
|
|
781
788
|
}
|
|
782
789
|
|
|
790
|
+
function toggleTabShortcuts(on: boolean) {
|
|
791
|
+
tabShortcuts = on;
|
|
792
|
+
setTabShortcutsEnabled(on);
|
|
793
|
+
}
|
|
794
|
+
|
|
783
795
|
// State is object-shaped (the chosen sprite), so this can't reuse the boolean `toggleOpts`.
|
|
784
796
|
const petToggleOpts: MochiEnhanceOptions<{ enabled: boolean; name?: string }, ActionFailure> = {
|
|
785
797
|
onPending: (v) => (savingPet = v),
|
|
@@ -1980,6 +1992,29 @@
|
|
|
1980
1992
|
</div>
|
|
1981
1993
|
</section>
|
|
1982
1994
|
|
|
1995
|
+
<section class="card">
|
|
1996
|
+
<div class="row">
|
|
1997
|
+
<div class="label">
|
|
1998
|
+
<Keyboard size={20} />
|
|
1999
|
+
<div class="text">
|
|
2000
|
+
<div class="name">Tab keyboard shortcuts</div>
|
|
2001
|
+
<div class="desc">
|
|
2002
|
+
Jump between open instance tabs with <code>Alt</code>+<code>1</code>–<code>9</code> (9 is
|
|
2003
|
+
always the last tab). Turn off to leave those keystrokes to the editor.
|
|
2004
|
+
</div>
|
|
2005
|
+
</div>
|
|
2006
|
+
</div>
|
|
2007
|
+
<label class="switch">
|
|
2008
|
+
<input
|
|
2009
|
+
type="checkbox"
|
|
2010
|
+
checked={tabShortcuts}
|
|
2011
|
+
onchange={(e) => toggleTabShortcuts(e.currentTarget.checked)}
|
|
2012
|
+
/>
|
|
2013
|
+
<span class="track"><span class="thumb"></span></span>
|
|
2014
|
+
</label>
|
|
2015
|
+
</div>
|
|
2016
|
+
</section>
|
|
2017
|
+
|
|
1983
2018
|
<section class="card">
|
|
1984
2019
|
<form class="row" method="POST" action="?/petToggle" {@attach enhance(petToggleOpts)}>
|
|
1985
2020
|
<div class="label">
|
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
'--info'
|
|
46
46
|
]
|
|
47
47
|
},
|
|
48
|
-
{
|
|
48
|
+
{
|
|
49
|
+
group: 'Attention',
|
|
50
|
+
tokens: ['--attn-done', '--attn-waiting', '--live-dot', '--switch-on-bg']
|
|
51
|
+
}
|
|
49
52
|
];
|
|
50
53
|
|
|
51
54
|
const chips: Record<string, HTMLElement> = {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { checkPresence, execInContainer, type ExecTarget } from '../lib/exec.server.ts';
|
|
2
|
+
import { editJsonFile, readJsonFile } from '../lib/container-files.server.ts';
|
|
3
|
+
import { CODE_SERVER_SETTINGS } from '../lib/devcontainer.server.ts';
|
|
4
|
+
import type { Injection } from '../lib/injections.server.ts';
|
|
5
|
+
|
|
6
|
+
/** Locates the bundled VS Code from the binary; shared prelude for the resolve and the check. */
|
|
7
|
+
const WALK_TO_BUNDLE =
|
|
8
|
+
`p=$(command -v code-server) || exit 1; p=$(readlink -f "$p"); ` +
|
|
9
|
+
`while [ "$p" != "/" ]; do for c in "$p" "$p/lib/vscode"; do `;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Walks up from the resolved binary rather than assuming /usr/lib/code-server: the feature and the
|
|
13
|
+
* standalone tarball put the wrapper at different depths, and only one of the two layouts keeps the
|
|
14
|
+
* bundled VS Code directly above `bin/`. Prints the root on the last line — `bash -lc` runs profile
|
|
15
|
+
* scripts whose stdout comes first, so callers must read the last line, never the whole capture.
|
|
16
|
+
*/
|
|
17
|
+
export const RESOLVE_ROOT_SCRIPT =
|
|
18
|
+
WALK_TO_BUNDLE +
|
|
19
|
+
`[ -d "$c/extensions/theme-defaults" ] && { printf '\\n%s\\n' "$c"; exit 0; }; done; ` +
|
|
20
|
+
`p=$(dirname "$p"); done; exit 1`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whitespace-tolerant because the shipped manifest is minified (`"uiTheme":"vs"`) while our own
|
|
24
|
+
* rewrite pretty-prints it — a fixed-spacing pattern would miss the unpatched file and report green.
|
|
25
|
+
* The closing quote is what keeps `"vs"` from also matching `"vs-dark"`.
|
|
26
|
+
*/
|
|
27
|
+
const LIGHT_UI_THEME_RE = '"uiTheme"[[:space:]]*:[[:space:]]*"(vs|hc-light)"';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve and probe in one exec: `check()` runs on every health tick, in the same `Promise.all` as
|
|
31
|
+
* the `codeServerAccessible` probe that gates mounting the IDE iframe, so a second round trip here
|
|
32
|
+
* would slow the boot-time fast cadence for no gain — the root can't move for a container's life.
|
|
33
|
+
*/
|
|
34
|
+
export const CHECK_SCRIPT =
|
|
35
|
+
WALK_TO_BUNDLE +
|
|
36
|
+
`m="$c/extensions/theme-defaults/package.json"; ` +
|
|
37
|
+
`[ -f "$m" ] && { grep -qE '${LIGHT_UI_THEME_RE}' "$m" && echo 0 || echo 1; exit 0; }; done; ` +
|
|
38
|
+
`p=$(dirname "$p"); done; echo 0`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The builtin-extension scan is cached against the mtime of the `extensions/` **directory**, which
|
|
42
|
+
* rewriting a manifest inside it does not touch — so without invalidating both the cache file and
|
|
43
|
+
* that mtime, the running server and the next window keep serving the pre-rewrite themes.
|
|
44
|
+
*/
|
|
45
|
+
const CLEAR_BUILTIN_CACHE =
|
|
46
|
+
`rm -f ~/.local/share/code-server/CachedProfilesData/*/extensions.builtin.cache 2>/dev/null; ` +
|
|
47
|
+
`exit 0`;
|
|
48
|
+
|
|
49
|
+
const SETTINGS_DIR = '$h/.local/share/code-server/User';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* VS Code matches `workbench.colorTheme` against a theme's *settingsId*, which is its manifest `id`
|
|
53
|
+
* (the label is only a fallback for entries without one) — so this reads ids, never the nls labels.
|
|
54
|
+
* The order tracks the build default (`ThemeSettingDefaults.COLOR_THEME_DARK`) across versions,
|
|
55
|
+
* because pinning exactly that id is what lets the workbench paint its real dark colors before any
|
|
56
|
+
* extension loads. Anything unrecognised falls back to whatever dark theme the build does ship.
|
|
57
|
+
*/
|
|
58
|
+
const PREFERRED_DARK_IDS = ['Dark 2026', 'Default Dark Modern', 'Dark Modern', 'Dark+'];
|
|
59
|
+
|
|
60
|
+
const THEME_KEYS = [
|
|
61
|
+
'workbench.colorTheme',
|
|
62
|
+
'workbench.preferredDarkColorTheme',
|
|
63
|
+
'workbench.preferredLightColorTheme'
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** A root path we are about to interpolate into a shell script, so nothing exotic may pass. */
|
|
67
|
+
const SAFE_PATH = /^\/[A-Za-z0-9._+\-/]+$/;
|
|
68
|
+
|
|
69
|
+
/** Mirrors `checkPresence`: only the last line is ours, everything before it is login-shell noise. */
|
|
70
|
+
function lastLine(stdout: string): string {
|
|
71
|
+
return stdout.trimEnd().split('\n').pop()?.trim() ?? '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ThemeContribution {
|
|
75
|
+
id?: string;
|
|
76
|
+
uiTheme?: string;
|
|
77
|
+
path?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function themesOf(manifest: Record<string, unknown>): ThemeContribution[] | null {
|
|
81
|
+
const contributes = manifest.contributes;
|
|
82
|
+
if (typeof contributes !== 'object' || contributes === null) return null;
|
|
83
|
+
const themes = (contributes as Record<string, unknown>).themes;
|
|
84
|
+
return Array.isArray(themes) ? (themes as ThemeContribution[]) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Null when the build ships no identifiable dark theme — better to leave the staged settings alone than pin an id nothing resolves. */
|
|
88
|
+
export function pickDarkThemeId(manifest: Record<string, unknown>): string | null {
|
|
89
|
+
const ids = (themesOf(manifest) ?? [])
|
|
90
|
+
.filter((t) => t?.uiTheme === 'vs-dark' && typeof t.id === 'string' && t.id.trim())
|
|
91
|
+
.map((t) => t.id!.trim());
|
|
92
|
+
if (!ids.length) return null;
|
|
93
|
+
return PREFERRED_DARK_IDS.find((id) => ids.includes(id)) ?? ids[0]!;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Selecting by `uiTheme` rather than by filename is load-bearing: the light set is
|
|
98
|
+
* 2026-light/light_modern/light_plus/light_vs *and* hc_light, so a `light*.json` glob silently
|
|
99
|
+
* misses two of them. Repointing `path` too, since the light JSONs stay light on disk.
|
|
100
|
+
*/
|
|
101
|
+
export function darkenThemeManifest(manifest: Record<string, unknown>): {
|
|
102
|
+
next: Record<string, unknown>;
|
|
103
|
+
changed: number;
|
|
104
|
+
} {
|
|
105
|
+
const themes = themesOf(manifest);
|
|
106
|
+
if (!themes) return { next: manifest, changed: 0 };
|
|
107
|
+
|
|
108
|
+
const pathFor = (ui: string) =>
|
|
109
|
+
themes.find((t) => t?.uiTheme === ui && typeof t.path === 'string')?.path;
|
|
110
|
+
const darkPath = pathFor('vs-dark');
|
|
111
|
+
const hcDarkPath = pathFor('hc-black');
|
|
112
|
+
|
|
113
|
+
let changed = 0;
|
|
114
|
+
const nextThemes = themes.map((theme) => {
|
|
115
|
+
if (theme?.uiTheme === 'vs' && darkPath) {
|
|
116
|
+
changed++;
|
|
117
|
+
return { ...theme, uiTheme: 'vs-dark', path: darkPath };
|
|
118
|
+
}
|
|
119
|
+
if (theme?.uiTheme === 'hc-light' && hcDarkPath) {
|
|
120
|
+
changed++;
|
|
121
|
+
return { ...theme, uiTheme: 'hc-black', path: hcDarkPath };
|
|
122
|
+
}
|
|
123
|
+
return theme;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const contributes = manifest.contributes as Record<string, unknown>;
|
|
127
|
+
return {
|
|
128
|
+
next: { ...manifest, contributes: { ...contributes, themes: nextThemes } },
|
|
129
|
+
changed
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Only the writes need root; resolution runs as the remote user, whose PATH also carries a per-user standalone install. */
|
|
134
|
+
const rootTarget = (target: ExecTarget): ExecTarget => ({ containerId: target.containerId });
|
|
135
|
+
|
|
136
|
+
async function resolveRoot(target: ExecTarget): Promise<string | null> {
|
|
137
|
+
const res = await execInContainer(target, { script: RESOLVE_ROOT_SCRIPT, capture: true });
|
|
138
|
+
if (!res.ok) return null;
|
|
139
|
+
const root = lastLine(res.stdout);
|
|
140
|
+
return SAFE_PATH.test(root) ? root : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* VS Code Web's pre-extension paint falls back to `isWeb ? LIGHT : DARK` whenever it has no usable
|
|
145
|
+
* cached theme, so pinning the theme in settings alone can never keep the first frame dark. This
|
|
146
|
+
* closes both halves: it pins the theme id *this* build uses (so the workbench stops discarding its
|
|
147
|
+
* own cache), and rewrites the bundled manifest so the light themes render dark if one is selected.
|
|
148
|
+
*/
|
|
149
|
+
export const codeServerDark: Injection = {
|
|
150
|
+
id: 'code-server-dark',
|
|
151
|
+
label: 'code-server dark theme',
|
|
152
|
+
// code-server-only; terminal-mode instances never run a workbench.
|
|
153
|
+
modes: ['ide'],
|
|
154
|
+
|
|
155
|
+
async apply(target, log) {
|
|
156
|
+
log('Forcing code-server dark theme…\n');
|
|
157
|
+
const root = await resolveRoot(target);
|
|
158
|
+
if (!root) {
|
|
159
|
+
log('⚠ code-server install not found — dark theme left to the staged settings\n');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const themeDir = `${root}/extensions/theme-defaults`;
|
|
164
|
+
const manifest = await readJsonFile(target, { dir: themeDir, name: 'package.json' });
|
|
165
|
+
if (!manifest) {
|
|
166
|
+
log('⚠ Could not read the bundled theme manifest — dark theme left to the staged settings\n');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const darkTheme = pickDarkThemeId(manifest);
|
|
171
|
+
if (!darkTheme) {
|
|
172
|
+
log('⚠ No dark theme found in the bundled manifest — staged settings left as they are\n');
|
|
173
|
+
} else {
|
|
174
|
+
// Seeded from the staged defaults so this still writes a complete file on the path where
|
|
175
|
+
// the launcher's own copy silently failed, rather than a settings.json of three keys.
|
|
176
|
+
const settings = await editJsonFile(
|
|
177
|
+
target,
|
|
178
|
+
{ dir: SETTINGS_DIR, name: 'settings.json' },
|
|
179
|
+
(current) => ({
|
|
180
|
+
...CODE_SERVER_SETTINGS,
|
|
181
|
+
...current,
|
|
182
|
+
...Object.fromEntries(THEME_KEYS.map((k) => [k, darkTheme]))
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
log(
|
|
186
|
+
settings.ok
|
|
187
|
+
? `✓ Pinned code-server theme to "${darkTheme}"\n`
|
|
188
|
+
: `⚠ Could not pin the code-server theme: ${settings.error}\n`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (darkenThemeManifest(manifest).changed === 0) {
|
|
193
|
+
log('· No light themes to darken in this build\n');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const written = await editJsonFile(
|
|
197
|
+
rootTarget(target),
|
|
198
|
+
{ dir: themeDir, name: 'package.json' },
|
|
199
|
+
(current) => darkenThemeManifest(current).next
|
|
200
|
+
);
|
|
201
|
+
if (!written.ok) {
|
|
202
|
+
log(`⚠ Could not darken the built-in light themes: ${written.error}\n`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
await execInContainer(target, { script: CLEAR_BUILTIN_CACHE });
|
|
206
|
+
// Bumping the scan's cache key; root because the bundled tree isn't the remote user's.
|
|
207
|
+
await execInContainer(rootTarget(target), {
|
|
208
|
+
script: `touch "${root}/extensions" 2>/dev/null; exit 0`
|
|
209
|
+
});
|
|
210
|
+
log('✓ Built-in light themes now render dark\n');
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
check(target) {
|
|
214
|
+
return checkPresence(target, CHECK_SCRIPT);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
@@ -124,12 +124,17 @@ const MANAGER_GIT_EXCLUDES = [
|
|
|
124
124
|
const EXCLUDE_MARKER_START = '# >>> codebay (auto-generated) >>>';
|
|
125
125
|
const EXCLUDE_MARKER_END = '# <<< codebay <<<';
|
|
126
126
|
|
|
127
|
-
const CODE_SERVER_SETTINGS = {
|
|
127
|
+
export const CODE_SERVER_SETTINGS = {
|
|
128
|
+
// Overwritten at boot by the code-server-dark injection with the id this build actually
|
|
129
|
+
// ships; the legacy label only has to carry a code-server old enough to still use it.
|
|
128
130
|
'workbench.colorTheme': 'Default Dark Modern',
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
131
|
+
// Counter-intuitive, and the opposite of what this file used to say: VS Code Web's
|
|
132
|
+
// pre-extension paint is `getPreferredColorScheme() ?? (isWeb ? LIGHT : DARK)`, and
|
|
133
|
+
// getPreferredColorScheme returns undefined unless detection is on — so turning detection
|
|
134
|
+
// OFF is what forced that first paint to light. On is only safe because both preferred
|
|
135
|
+
// themes below are the same dark theme, making either branch dark.
|
|
136
|
+
'window.autoDetectColorScheme': true,
|
|
137
|
+
// Left off so the high-contrast branch (which could resolve hcLight) stays unreachable.
|
|
133
138
|
'window.autoDetectHighContrast': false,
|
|
134
139
|
'workbench.preferredDarkColorTheme': 'Default Dark Modern',
|
|
135
140
|
'workbench.preferredLightColorTheme': 'Default Dark Modern',
|
|
@@ -241,10 +246,20 @@ const terminalTask = (permissionMode: ClaudePermissionMode) => ({
|
|
|
241
246
|
problemMatcher: []
|
|
242
247
|
});
|
|
243
248
|
|
|
249
|
+
const CODE_SERVER_USER_DIR = '~/.local/share/code-server/User';
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Seeds the file only when it's missing. `relaunchSurface` re-runs this launcher *after* the
|
|
253
|
+
* injections have rewritten the theme id to the one the installed build uses, so an unconditional
|
|
254
|
+
* copy would put the stale staged id back and re-arm the very cache mismatch this all exists to
|
|
255
|
+
* avoid. A rebuild always gets a fresh container, so the staged defaults still land on every build.
|
|
256
|
+
* `[ -f ]` rather than `cp -n`, which busybox coreutils don't reliably carry.
|
|
257
|
+
*/
|
|
244
258
|
const CODE_SERVER_APPLY_SETTINGS =
|
|
245
|
-
`mkdir -p
|
|
259
|
+
`mkdir -p ${CODE_SERVER_USER_DIR}; ` +
|
|
260
|
+
`[ -f ${CODE_SERVER_USER_DIR}/settings.json ] || ` +
|
|
246
261
|
`cp -f \\"$PWD/.devcontainer/${CODE_SERVER_SETTINGS_FILE}\\" ` +
|
|
247
|
-
|
|
262
|
+
`${CODE_SERVER_USER_DIR}/settings.json 2>/dev/null;`;
|
|
248
263
|
|
|
249
264
|
// Fully detached (redirects + </dev/null + &) because `devcontainer up` waits for postStart's
|
|
250
265
|
// pipes to close — an attached download would keep the whole boot on the Open VSX critical path.
|
|
@@ -456,7 +471,10 @@ export function findDevcontainerConfig(dir: string): string | null {
|
|
|
456
471
|
const CODEBAY_CONFIG_NESTED = 'codebay.devcontainer.json';
|
|
457
472
|
const CODEBAY_CONFIG_FLAT = '.codebay.devcontainer.json';
|
|
458
473
|
|
|
459
|
-
/**
|
|
474
|
+
/**
|
|
475
|
+
* Sits in the same directory as the project's own config, which is what `--config` points the CLI at:
|
|
476
|
+
* relative dockerfile/compose/feature paths resolve against that directory either way.
|
|
477
|
+
*/
|
|
460
478
|
export function overrideConfigPath(
|
|
461
479
|
workspaceDir: string,
|
|
462
480
|
canonical: string | null = findDevcontainerConfig(workspaceDir)
|
|
@@ -524,7 +542,7 @@ const HOST_GATEWAY_ARG = '--add-host=host.docker.internal:host-gateway';
|
|
|
524
542
|
|
|
525
543
|
/**
|
|
526
544
|
* Merges the project's config (read-only) into a Codebay-owned sibling file that `devcontainer up
|
|
527
|
-
* --config` boots from, so the project's own devcontainer.json never shows as modified in git.
|
|
545
|
+
* --override-config` boots from, so the project's own devcontainer.json never shows as modified in git.
|
|
528
546
|
*/
|
|
529
547
|
export async function writeOverrideConfig(
|
|
530
548
|
workspaceDir: string,
|
|
@@ -534,7 +552,7 @@ export async function writeOverrideConfig(
|
|
|
534
552
|
mode: InstanceMode = 'ide',
|
|
535
553
|
permissionMode: ClaudePermissionMode = 'default',
|
|
536
554
|
envVars: { name: string; value: string }[] = []
|
|
537
|
-
): Promise<{ imageSource: string; configPath: string }> {
|
|
555
|
+
): Promise<{ imageSource: string; configPath: string | null; overrideConfigPath: string }> {
|
|
538
556
|
const isTerminal = mode === 'terminal';
|
|
539
557
|
const canonical = findDevcontainerConfig(workspaceDir);
|
|
540
558
|
const target = overrideConfigPath(workspaceDir, canonical);
|
|
@@ -640,7 +658,7 @@ export async function writeOverrideConfig(
|
|
|
640
658
|
|
|
641
659
|
await writeLocalGitExclude(workspaceDir);
|
|
642
660
|
|
|
643
|
-
return { imageSource, configPath: target };
|
|
661
|
+
return { imageSource, configPath: canonical, overrideConfigPath: target };
|
|
644
662
|
}
|
|
645
663
|
|
|
646
664
|
/** `codebay-tmux` has been injected into every config this manager has ever written. */
|
|
@@ -902,10 +920,10 @@ export interface UpResult {
|
|
|
902
920
|
description?: string;
|
|
903
921
|
}
|
|
904
922
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
) {
|
|
923
|
+
/** `configPath` is the project's own config (`--config`), `overrideConfigPath` the Codebay-owned one. */
|
|
924
|
+
type UpOptions = { noCache?: boolean; configPath?: string; overrideConfigPath?: string };
|
|
925
|
+
|
|
926
|
+
export function devcontainerUpArgs(workspaceDir: string, opts: UpOptions = {}) {
|
|
909
927
|
const args = [
|
|
910
928
|
devcontainerBin(),
|
|
911
929
|
'up',
|
|
@@ -913,7 +931,10 @@ export function devcontainerUpArgs(
|
|
|
913
931
|
workspaceDir,
|
|
914
932
|
'--remove-existing-container'
|
|
915
933
|
];
|
|
934
|
+
// --config rejects any basename but devcontainer.json/.devcontainer.json, so the Codebay-owned
|
|
935
|
+
// file rides --override-config; --config only pins which path relative resolution is based on.
|
|
916
936
|
if (opts.configPath) args.push('--config', opts.configPath);
|
|
937
|
+
if (opts.overrideConfigPath) args.push('--override-config', opts.overrideConfigPath);
|
|
917
938
|
// Only takes effect because --remove-existing-container drops the container before the build.
|
|
918
939
|
if (opts.noCache) args.push('--build-no-cache');
|
|
919
940
|
return args;
|
|
@@ -931,7 +952,7 @@ export function devcontainerUpEnv(): Record<string, string | undefined> {
|
|
|
931
952
|
export async function devcontainerUp(
|
|
932
953
|
workspaceDir: string,
|
|
933
954
|
onLog: (chunk: string) => void,
|
|
934
|
-
opts:
|
|
955
|
+
opts: UpOptions = {}
|
|
935
956
|
): Promise<UpResult> {
|
|
936
957
|
const proc = Bun.spawn(devcontainerUpArgs(workspaceDir, opts), {
|
|
937
958
|
cwd: workspaceDir,
|
|
@@ -8,6 +8,7 @@ import { gitIdentity } from '../container-injections/git-identity.ts';
|
|
|
8
8
|
import { claudeCodeCredentials } from '../container-injections/claude-code-credentials.ts';
|
|
9
9
|
import { claudeCodeCustom } from '../container-injections/claude-code-custom.ts';
|
|
10
10
|
import { claudeCodeIdeExtension } from '../container-injections/claude-code-ide-extension.ts';
|
|
11
|
+
import { codeServerDark } from '../container-injections/code-server-dark.ts';
|
|
11
12
|
import { claudeCodeInstall } from '../container-injections/claude-code-install.ts';
|
|
12
13
|
import { claudeCodeUpdate } from '../container-injections/claude-code-update.ts';
|
|
13
14
|
import { claudeCodeModels } from '../container-injections/claude-code-models.ts';
|
|
@@ -69,8 +70,9 @@ function buildStages(claudeInjection: Injection): Injection[][] {
|
|
|
69
70
|
claudeEffortLevel
|
|
70
71
|
],
|
|
71
72
|
// git-identity needs stage 1's safe.directory; ttyd shares the apt/dpkg lock with tmux and
|
|
72
|
-
// the /usr/local/bin symlink with claude-code-install, so it trails both.
|
|
73
|
-
|
|
73
|
+
// the /usr/local/bin symlink with claude-code-install, so it trails both. code-server-dark
|
|
74
|
+
// drops the extension cache, so it must not run while stage 1 is still installing into it.
|
|
75
|
+
[gitIdentity, attentionHooks, claudeCodeModels, ttyd, claudeCodeUpdate, codeServerDark],
|
|
74
76
|
[githubCredentials, claudeStatusline, claudePermissionMode],
|
|
75
77
|
[claudeModel, claudeAliases],
|
|
76
78
|
// claude-trust edits ~/.claude.json, which the stage-1 Claude slot also writes.
|
|
@@ -467,7 +467,7 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
467
467
|
}));
|
|
468
468
|
appendLog(row.id, `Injecting ${surfaceLabel(row.mode)} (host port ${row.host_port})\n`);
|
|
469
469
|
const defaultImage = getOption('default_image') ?? DEFAULT_IMAGE;
|
|
470
|
-
const { imageSource, configPath } = await writeOverrideConfig(
|
|
470
|
+
const { imageSource, configPath, overrideConfigPath } = await writeOverrideConfig(
|
|
471
471
|
row.workspace_path,
|
|
472
472
|
row.host_port,
|
|
473
473
|
forwards,
|
|
@@ -481,8 +481,9 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
481
481
|
const noCache = opts.noCache || getOption('disable_build_cache') === '1';
|
|
482
482
|
if (noCache) appendLog(row.id, `Building without cache (--build-no-cache)\n`);
|
|
483
483
|
|
|
484
|
-
// The CLI's --remove-existing-container matches by the devcontainer.config_file label,
|
|
485
|
-
//
|
|
484
|
+
// The CLI's --remove-existing-container matches by the devcontainer.config_file label, and
|
|
485
|
+
// deliberately ignores a workspace match whose label differs — so a container left by a build
|
|
486
|
+
// that labelled itself differently would survive and keep the host port. Drop it ourselves.
|
|
486
487
|
if (row.container_id && !(await removeContainerOnly(row.container_id))) {
|
|
487
488
|
appendLog(
|
|
488
489
|
row.id,
|
|
@@ -494,7 +495,8 @@ async function provision(row: InstanceRow, opts: { noCache?: boolean } = {}): Pr
|
|
|
494
495
|
const upStart = Date.now();
|
|
495
496
|
const result = await devcontainerUp(row.workspace_path, (chunk) => appendLog(row.id, chunk), {
|
|
496
497
|
noCache,
|
|
497
|
-
configPath
|
|
498
|
+
configPath: configPath ?? undefined,
|
|
499
|
+
overrideConfigPath
|
|
498
500
|
});
|
|
499
501
|
appendLog(row.id, `⏱ devcontainer up: ${elapsed(upStart)}\n`);
|
|
500
502
|
|
package/src/settings.ts
CHANGED
|
@@ -12,3 +12,16 @@ export function setSoundEnabled(on: boolean): void {
|
|
|
12
12
|
if (typeof localStorage === 'undefined') return;
|
|
13
13
|
localStorage.setItem(SOUND_KEY, on ? 'on' : 'off');
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
const TAB_SHORTCUTS_KEY = 'codebay.tabShortcuts';
|
|
17
|
+
|
|
18
|
+
/** Defaults to on — only an explicit 'off' disables the Alt+1-9 tab jumps. */
|
|
19
|
+
export function tabShortcutsEnabled(): boolean {
|
|
20
|
+
if (typeof localStorage === 'undefined') return true;
|
|
21
|
+
return localStorage.getItem(TAB_SHORTCUTS_KEY) !== 'off';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setTabShortcutsEnabled(on: boolean): void {
|
|
25
|
+
if (typeof localStorage === 'undefined') return;
|
|
26
|
+
localStorage.setItem(TAB_SHORTCUTS_KEY, on ? 'on' : 'off');
|
|
27
|
+
}
|
package/src/shell.html
CHANGED
|
@@ -57,12 +57,16 @@
|
|
|
57
57
|
--screen-ink: light-dark(#d8d9cf, #e8b055);
|
|
58
58
|
--screen-line: light-dark(rgba(255, 255, 255, 0.04), rgba(232, 176, 85, 0.05));
|
|
59
59
|
--attn-done: #22c55e;
|
|
60
|
+
/* The "live" LED on an inverted chip. Green reads as a lit indicator on
|
|
61
|
+
light's near-black chip; on dark's amber one it clashes, so there it
|
|
62
|
+
drops to the chip's own ink (matches --fill-ink). */
|
|
63
|
+
--live-dot: light-dark(#22c55e, #17130a);
|
|
60
64
|
--attn-waiting: light-dark(#f59e0b, #ff7a2f);
|
|
61
65
|
/* Softened in dark — a saturated blue is the one hue that fights the warm ground. */
|
|
62
66
|
--info: light-dark(#388bfd, #6c9fd8);
|
|
63
|
-
/* The pixel matrix.
|
|
67
|
+
/* The pixel matrix. 2.5% white over a near-black ground is invisible, so
|
|
64
68
|
dark gets amber at double the alpha to read as a grid rather than noise. */
|
|
65
|
-
--grid-line: light-dark(rgba(0, 0, 0, 0.
|
|
69
|
+
--grid-line: light-dark(rgba(0, 0, 0, 0.025), rgba(217, 150, 58, 0.05));
|
|
66
70
|
/* --attn-done at reduced opacity — reads correctly on both themes' track color. */
|
|
67
71
|
--switch-on-bg: rgba(34, 197, 94, 0.32);
|
|
68
72
|
/* The two dim levels, named so they stay consistent. Deliberately not
|
|
@@ -116,16 +120,27 @@
|
|
|
116
120
|
body {
|
|
117
121
|
margin: 0;
|
|
118
122
|
font-family: var(--font-sans);
|
|
119
|
-
|
|
120
|
-
background:
|
|
121
|
-
repeating-linear-gradient(0deg, var(--grid-line) 0 1px, transparent 1px 3px),
|
|
122
|
-
repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 3px), var(--bg);
|
|
123
|
+
background: var(--bg);
|
|
123
124
|
color: var(--ink);
|
|
124
125
|
font-size: 14px;
|
|
125
126
|
line-height: 1.55;
|
|
126
127
|
min-height: 100vh;
|
|
127
128
|
-webkit-font-smoothing: antialiased;
|
|
128
129
|
}
|
|
130
|
+
/* The faint pixel-grid matrix overlay lives on a viewport-fixed layer
|
|
131
|
+
so it rasters once and is never re-tiled as the page scrolls. The 4px
|
|
132
|
+
cell keeps the 1px lines sparse enough to avoid moiré against the
|
|
133
|
+
physical pixel grid under fractional display scaling. */
|
|
134
|
+
body::before {
|
|
135
|
+
content: '';
|
|
136
|
+
position: fixed;
|
|
137
|
+
inset: 0;
|
|
138
|
+
z-index: -1;
|
|
139
|
+
pointer-events: none;
|
|
140
|
+
background:
|
|
141
|
+
repeating-linear-gradient(0deg, var(--grid-line) 0 1px, transparent 1px 4px),
|
|
142
|
+
repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 4px);
|
|
143
|
+
}
|
|
129
144
|
</style>
|
|
130
145
|
{{mochi.head}} {{mochi.css}}
|
|
131
146
|
</head>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jb as a}from"/_mochi/client/chunk-566rqq4c.js";import"/_mochi/client/chunk-czkxd74n.js";export{a as registerComponent};
|