codebay 0.11.1 → 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 +116 -108
- 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/package.json +1 -1
- package/src/components/AppShell.svelte +5 -1
- package/src/components/SettingsView.svelte +36 -1
- package/src/container-injections/code-server-dark.ts +216 -0
- package/src/lib/devcontainer.server.ts +22 -7
- package/src/lib/injections.server.ts +4 -2
- package/src/settings.ts +13 -0
- package/src/shell.html +17 -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}
|
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">
|
|
@@ -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.
|
|
@@ -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.
|
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
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
--attn-waiting: light-dark(#f59e0b, #ff7a2f);
|
|
65
65
|
/* Softened in dark — a saturated blue is the one hue that fights the warm ground. */
|
|
66
66
|
--info: light-dark(#388bfd, #6c9fd8);
|
|
67
|
-
/* The pixel matrix.
|
|
67
|
+
/* The pixel matrix. 2.5% white over a near-black ground is invisible, so
|
|
68
68
|
dark gets amber at double the alpha to read as a grid rather than noise. */
|
|
69
|
-
--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));
|
|
70
70
|
/* --attn-done at reduced opacity — reads correctly on both themes' track color. */
|
|
71
71
|
--switch-on-bg: rgba(34, 197, 94, 0.32);
|
|
72
72
|
/* The two dim levels, named so they stay consistent. Deliberately not
|
|
@@ -120,16 +120,27 @@
|
|
|
120
120
|
body {
|
|
121
121
|
margin: 0;
|
|
122
122
|
font-family: var(--font-sans);
|
|
123
|
-
|
|
124
|
-
background:
|
|
125
|
-
repeating-linear-gradient(0deg, var(--grid-line) 0 1px, transparent 1px 3px),
|
|
126
|
-
repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 3px), var(--bg);
|
|
123
|
+
background: var(--bg);
|
|
127
124
|
color: var(--ink);
|
|
128
125
|
font-size: 14px;
|
|
129
126
|
line-height: 1.55;
|
|
130
127
|
min-height: 100vh;
|
|
131
128
|
-webkit-font-smoothing: antialiased;
|
|
132
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
|
+
}
|
|
133
144
|
</style>
|
|
134
145
|
{{mochi.head}} {{mochi.css}}
|
|
135
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};
|