codebay 0.12.1 → 0.13.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 +107 -99
- package/.mochi/svelte-client/HydratableIsland-zrdsrsmg.js +1 -0
- package/.mochi/svelte-client/{_hydrate-AppShell_2rtnxn6pgp2rh-5kb6kh2d.js → _hydrate-AppShell_2rtnxn6pgp2rh-v7y7dh53.js} +2 -2
- package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-c51gt15m.js → _hydrate-AvatarsView_257s9yq1wwr4f-tmwd6fpb.js} +1 -1
- package/.mochi/svelte-client/{_hydrate-InstanceView_2ma45l9bybfhl-d256bw54.js → _hydrate-InstanceView_2ma45l9bybfhl-m7t64dbh.js} +1 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-wedc9x9y.js +39 -0
- package/.mochi/svelte-client/{chunk-2eaagsfc.js → chunk-0mc09cyb.js} +1 -1
- package/.mochi/svelte-client/{chunk-a5e8vytv.js → chunk-5jwfs4p9.js} +2 -2
- package/.mochi/svelte-client/chunk-6qg4q98a.js +2 -0
- package/.mochi/svelte-client/{chunk-d8rdrqsh.js → chunk-ccfh8hxz.js} +1 -1
- package/.mochi/svelte-client/{chunk-d9bszcp8.js → chunk-phf7v5jj.js} +1 -1
- package/.mochi/svelte-client/{chunk-y1xjps59.js → chunk-spk1b2r4.js} +1 -1
- package/.mochi/svelte-client/chunk-tmeh70wg.js +2 -0
- package/.mochi/svelte-client/{chunk-6v5d7dex.js → chunk-vs1yjbqt.js} +1 -1
- package/.mochi/svelte-client/{chunk-1ttqgrz7.js → chunk-x0m3k0rn.js} +1 -1
- package/.mochi/svelte-client/{chunk-s70rry6z.js → chunk-y7f4jbdm.js} +1 -1
- package/.mochi/svelte-compile/{App-dn3kh0xn.server.js → App-94xsdn0w.server.js} +2 -2
- package/.mochi/svelte-compile/{Avatars-7k5055m8.server.js → Avatars-fnccw47h.server.js} +1 -1
- package/.mochi/svelte-compile/{Instance-hj76qnha.server.js → Instance-knnkcn2p.server.js} +1 -1
- package/.mochi/svelte-compile/{Settings-m2bfa4k4.server.js → Settings-h7gsjyz3.server.js} +169 -114
- package/.mochi/svelte-compile/{chunk-qc0f73f3.js → chunk-apdte4nh.js} +7 -8
- package/.mochi/svelte-compile/{chunk-zd49axkf.js → chunk-w0hca7va.js} +2 -1
- package/package.json +1 -1
- package/src/avatars/bee.ts +7 -8
- package/src/components/SettingsView.svelte +74 -0
- package/src/container-injections/claude-code-install.ts +12 -7
- package/src/container-injections/claude-code-update.ts +45 -10
- package/src/container-injections/claude-output-style.ts +84 -0
- package/src/lib/injections.server.ts +5 -1
- package/src/pages/Settings.svelte +9 -1
- package/src/routes.ts +10 -0
- package/src/types.ts +16 -0
- package/.mochi/svelte-client/HydratableIsland-4j3bazeq.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-rw5z89pa.js +0 -39
- package/.mochi/svelte-client/chunk-pz6dq0v3.js +0 -2
- package/.mochi/svelte-client/chunk-wcfbjj3x.js +0 -2
|
@@ -70,16 +70,15 @@ var bear_default = art4;
|
|
|
70
70
|
// src/avatars/bee.ts
|
|
71
71
|
var art5 = {
|
|
72
72
|
name: "bee",
|
|
73
|
-
robot: true,
|
|
74
73
|
pixels: [
|
|
75
|
-
".#....#.",
|
|
76
|
-
"##.##.##",
|
|
77
|
-
"#.####.#",
|
|
78
|
-
".######.",
|
|
79
|
-
".######.",
|
|
80
74
|
"........",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
75
|
+
"...++##.",
|
|
76
|
+
"..++###.",
|
|
77
|
+
"...+#...",
|
|
78
|
+
".#####..",
|
|
79
|
+
"#.+#+###",
|
|
80
|
+
"##+#+##.",
|
|
81
|
+
".#####.."
|
|
83
82
|
]
|
|
84
83
|
};
|
|
85
84
|
var bee_default = art5;
|
|
@@ -11,6 +11,7 @@ var CLAUDE_PERMISSION_MODES = [
|
|
|
11
11
|
"plan"
|
|
12
12
|
];
|
|
13
13
|
var CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
14
|
+
var CLAUDE_OUTPUT_STYLES = ["default", "none", "Concise"];
|
|
14
15
|
function ideUrl(instance) {
|
|
15
16
|
const base = `/p/${instance.id}/`;
|
|
16
17
|
return instance.remote_workspace_folder ? `${base}?folder=${encodeURIComponent(instance.remote_workspace_folder)}` : base;
|
|
@@ -62,4 +63,4 @@ function applyTheme(theme) {
|
|
|
62
63
|
document.documentElement.dataset.theme = theme;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export { CLAUDE_PERMISSION_MODES, CLAUDE_EFFORT_LEVELS, ideUrl, apiFetch, apiPost, apiDelete, getTheme, setTheme, applyTheme };
|
|
66
|
+
export { CLAUDE_PERMISSION_MODES, CLAUDE_EFFORT_LEVELS, CLAUDE_OUTPUT_STYLES, ideUrl, apiFetch, apiPost, apiDelete, getTheme, setTheme, applyTheme };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
package/src/avatars/bee.ts
CHANGED
|
@@ -2,16 +2,15 @@ import type { AvatarArt } from './types.ts';
|
|
|
2
2
|
|
|
3
3
|
const art: AvatarArt = {
|
|
4
4
|
name: 'bee',
|
|
5
|
-
robot: true,
|
|
6
5
|
pixels: [
|
|
7
|
-
'.#....#.',
|
|
8
|
-
'##.##.##',
|
|
9
|
-
'#.####.#',
|
|
10
|
-
'.######.',
|
|
11
|
-
'.######.',
|
|
12
6
|
'........',
|
|
13
|
-
'
|
|
14
|
-
'
|
|
7
|
+
'...++##.',
|
|
8
|
+
'..++###.',
|
|
9
|
+
'...+#...',
|
|
10
|
+
'.#####..',
|
|
11
|
+
'#.+#+###',
|
|
12
|
+
'##+#+##.',
|
|
13
|
+
'.#####..'
|
|
15
14
|
]
|
|
16
15
|
};
|
|
17
16
|
|
|
@@ -42,14 +42,17 @@
|
|
|
42
42
|
import {
|
|
43
43
|
CLAUDE_PERMISSION_MODES,
|
|
44
44
|
CLAUDE_EFFORT_LEVELS,
|
|
45
|
+
CLAUDE_OUTPUT_STYLES,
|
|
45
46
|
type ClaudePermissionMode,
|
|
46
47
|
type ClaudeEffortLevel,
|
|
48
|
+
type ClaudeOutputStyle,
|
|
47
49
|
type InstanceMode
|
|
48
50
|
} from '../types.ts';
|
|
49
51
|
import Terminal from '@lucide/svelte/icons/terminal';
|
|
50
52
|
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
|
|
51
53
|
import ShieldCheck from '@lucide/svelte/icons/shield-check';
|
|
52
54
|
import Gauge from '@lucide/svelte/icons/gauge';
|
|
55
|
+
import MessageSquare from '@lucide/svelte/icons/message-square';
|
|
53
56
|
import { installPopupBackTrap } from '../lib/popup-nav.ts';
|
|
54
57
|
|
|
55
58
|
/** Every settings form action fails with the same `{ error }` shape. */
|
|
@@ -60,6 +63,7 @@
|
|
|
60
63
|
defaultMode,
|
|
61
64
|
claudePermissionMode,
|
|
62
65
|
claudeEffortLevel,
|
|
66
|
+
claudeOutputStyle,
|
|
63
67
|
defaultImage,
|
|
64
68
|
builtinImage,
|
|
65
69
|
disableBuildCache,
|
|
@@ -100,6 +104,7 @@
|
|
|
100
104
|
defaultMode: InstanceMode;
|
|
101
105
|
claudePermissionMode: ClaudePermissionMode;
|
|
102
106
|
claudeEffortLevel: ClaudeEffortLevel;
|
|
107
|
+
claudeOutputStyle: ClaudeOutputStyle;
|
|
103
108
|
defaultImage: string;
|
|
104
109
|
builtinImage: string;
|
|
105
110
|
disableBuildCache: boolean;
|
|
@@ -292,6 +297,33 @@
|
|
|
292
297
|
effortFormEl?.requestSubmit();
|
|
293
298
|
}
|
|
294
299
|
|
|
300
|
+
// svelte-ignore state_referenced_locally
|
|
301
|
+
let outputStyleChoice = $state<ClaudeOutputStyle>(claudeOutputStyle);
|
|
302
|
+
let savingOutputStyle = $state(false);
|
|
303
|
+
let outputStyleError = $state<string | null>(null);
|
|
304
|
+
let outputStyleFormEl: HTMLFormElement | undefined;
|
|
305
|
+
|
|
306
|
+
const outputStyleOpts = saveOpts<{ style: ClaudeOutputStyle }>({
|
|
307
|
+
setSaving: (v) => (savingOutputStyle = v),
|
|
308
|
+
setError: (v) => (outputStyleError = v),
|
|
309
|
+
setMsg: () => {},
|
|
310
|
+
onSuccess: (data) => {
|
|
311
|
+
if (data?.style) outputStyleChoice = data.style;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const OUTPUT_STYLE_LABELS: Record<ClaudeOutputStyle, string> = {
|
|
316
|
+
default: 'Default',
|
|
317
|
+
none: 'None',
|
|
318
|
+
Concise: 'Concise'
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
function chooseOutputStyle(next: ClaudeOutputStyle) {
|
|
322
|
+
if (next === outputStyleChoice) return;
|
|
323
|
+
flushSync(() => (outputStyleChoice = next));
|
|
324
|
+
outputStyleFormEl?.requestSubmit();
|
|
325
|
+
}
|
|
326
|
+
|
|
295
327
|
// svelte-ignore state_referenced_locally
|
|
296
328
|
let image = $state(defaultImage);
|
|
297
329
|
let savingImage = $state(false);
|
|
@@ -973,6 +1005,48 @@
|
|
|
973
1005
|
{/if}
|
|
974
1006
|
</section>
|
|
975
1007
|
|
|
1008
|
+
<section class="card">
|
|
1009
|
+
<form
|
|
1010
|
+
class="row"
|
|
1011
|
+
method="POST"
|
|
1012
|
+
action="?/claudeOutputStyle"
|
|
1013
|
+
bind:this={outputStyleFormEl}
|
|
1014
|
+
{@attach enhance(outputStyleOpts)}
|
|
1015
|
+
>
|
|
1016
|
+
<div class="label">
|
|
1017
|
+
<MessageSquare size={20} />
|
|
1018
|
+
<div class="text">
|
|
1019
|
+
<div class="name">Claude output style</div>
|
|
1020
|
+
<div class="desc">
|
|
1021
|
+
The output style Claude Code starts new sessions in, written to
|
|
1022
|
+
<code>~/.claude/settings.json</code>. <strong>Default</strong> inherits your host
|
|
1023
|
+
<code>~/.claude/settings.json</code> (including any custom styles);
|
|
1024
|
+
<strong>None</strong>
|
|
1025
|
+
forces it off. Applies on create and rebuild, not to already-running instances.
|
|
1026
|
+
</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
<input type="hidden" name="style" value={outputStyleChoice} />
|
|
1030
|
+
<div class="mode-toggle" role="group" aria-label="Claude output style">
|
|
1031
|
+
{#each CLAUDE_OUTPUT_STYLES as option (option)}
|
|
1032
|
+
<button
|
|
1033
|
+
type="button"
|
|
1034
|
+
class="mode-btn"
|
|
1035
|
+
class:active={outputStyleChoice === option}
|
|
1036
|
+
aria-pressed={outputStyleChoice === option}
|
|
1037
|
+
disabled={savingOutputStyle}
|
|
1038
|
+
onclick={() => chooseOutputStyle(option)}
|
|
1039
|
+
>
|
|
1040
|
+
{OUTPUT_STYLE_LABELS[option]}
|
|
1041
|
+
</button>
|
|
1042
|
+
{/each}
|
|
1043
|
+
</div>
|
|
1044
|
+
</form>
|
|
1045
|
+
{#if outputStyleError}
|
|
1046
|
+
<div class="sub"><div class="msg error">{outputStyleError}</div></div>
|
|
1047
|
+
{/if}
|
|
1048
|
+
</section>
|
|
1049
|
+
|
|
976
1050
|
<section class="card">
|
|
977
1051
|
<form
|
|
978
1052
|
class="row image-row"
|
|
@@ -8,20 +8,25 @@ import type { Injection } from '../lib/injections.server.ts';
|
|
|
8
8
|
* Node, else the standalone installer, which needs no Node at all.
|
|
9
9
|
*
|
|
10
10
|
* `$1` is the remote user (the injection passes it; the build-time feature falls back to the
|
|
11
|
-
* devcontainer CLI's `$_REMOTE_USER`)
|
|
12
|
-
*
|
|
11
|
+
* devcontainer CLI's `$_REMOTE_USER`). Claude Code 2.x's native binary — installed by npm's
|
|
12
|
+
* postinstall as much as by the standalone installer — lands under that user's `$HOME`, so both
|
|
13
|
+
* branches run as that user via `run_as`; running as root would strand the binary in `/root`.
|
|
14
|
+
* Success is judged by `claude --version` (as the user), not `command -v claude`, so a stranded
|
|
15
|
+
* binary the wrapper still resolves falls through to the standalone installer instead of passing.
|
|
13
16
|
*/
|
|
14
17
|
export const INSTALL_SCRIPT =
|
|
15
18
|
'if command -v claude >/dev/null 2>&1; then exit 0; fi\n' +
|
|
16
|
-
'
|
|
17
|
-
'
|
|
19
|
+
'u="${1:-${_REMOTE_USER:-root}}"\n' +
|
|
20
|
+
// Fall back to a home the remote user owns, never root's, so `su -m` can't strand the binary in /root.
|
|
21
|
+
'h="$(getent passwd "$u" 2>/dev/null | cut -d: -f6)"; [ -n "$h" ] || { [ "$u" = root ] && h=/root || h="/home/$u"; }\n' +
|
|
22
|
+
'run_as() { if [ "$(id -un)" = "$u" ]; then HOME="$h" sh -c "$1"; else HOME="$h" su -m "$u" -c "$1"; fi; }\n' +
|
|
23
|
+
"if command -v npm >/dev/null 2>&1 && run_as 'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1'; then\n" +
|
|
24
|
+
' p="$(run_as \'npm prefix -g 2>/dev/null\')"\n' +
|
|
18
25
|
// npm's global bin is off PATH whenever the image sets its own NPM_CONFIG_PREFIX, so a bare
|
|
19
26
|
// `command -v` would miss an install that just succeeded — and so would the terminal launcher.
|
|
20
27
|
' [ -n "$p" ] && [ -x "$p/bin/claude" ] && ln -sf "$p/bin/claude" /usr/local/bin/claude 2>/dev/null\n' +
|
|
21
|
-
|
|
28
|
+
" if run_as 'claude --version >/dev/null 2>&1'; then exit 0; fi\n" +
|
|
22
29
|
'fi\n' +
|
|
23
|
-
'u="${1:-${_REMOTE_USER:-root}}"\n' +
|
|
24
|
-
'h="$(getent passwd "$u" 2>/dev/null | cut -d: -f6)"; [ -n "$h" ] || h="$HOME"\n' +
|
|
25
30
|
'f="$(mktemp)"\n' +
|
|
26
31
|
'if command -v curl >/dev/null 2>&1; then curl -fsSL https://claude.ai/install.sh -o "$f"\n' +
|
|
27
32
|
'elif command -v wget >/dev/null 2>&1; then wget -qO "$f" https://claude.ai/install.sh\n' +
|
|
@@ -8,29 +8,63 @@ import type { Injection } from '../lib/injections.server.ts';
|
|
|
8
8
|
* was current when the layer was first baked. This re-checks at boot and only reinstalls when
|
|
9
9
|
* behind — no-op if Claude Code isn't installed (project-owned images own their tooling) or
|
|
10
10
|
* npm/registry is unreachable, and it echoes `updated <old> -> <new>` so `apply()` can log it.
|
|
11
|
+
*
|
|
12
|
+
* `$1` is the container's remote user. Claude Code 2.x ships a native binary that npm's
|
|
13
|
+
* postinstall drops into the *invoking* user's HOME, so the reinstall (and the `claude --version`
|
|
14
|
+
* probes) run as that user via `run_as` — a root reinstall would strand the binary in `/root`,
|
|
15
|
+
* exactly as the standalone installer in `claude-code-install.ts` would. Success is judged by a
|
|
16
|
+
* post-install `claude --version` (as the remote user), not npm's exit code, so a stranded or
|
|
17
|
+
* half-downloaded binary surfaces as a failure instead of a false `updated`.
|
|
11
18
|
*/
|
|
19
|
+
const RUN_AS_PRELUDE =
|
|
20
|
+
'u="${1:-root}"; ' +
|
|
21
|
+
// Fall back to a home the remote user actually owns, never root's — otherwise `su -m` below
|
|
22
|
+
// would strand the native binary in /root, the exact failure this exists to prevent.
|
|
23
|
+
'h="$(getent passwd "$u" 2>/dev/null | cut -d: -f6)"; [ -n "$h" ] || { [ "$u" = root ] && h=/root || h="/home/$u"; }; ' +
|
|
24
|
+
'run_as() { if [ "$(id -un)" = "$u" ]; then HOME="$h" sh -c "$1"; else HOME="$h" su -m "$u" -c "$1"; fi; }; ' +
|
|
25
|
+
"ver() { run_as 'claude --version 2>/dev/null' | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n1; }; ";
|
|
26
|
+
|
|
27
|
+
const NPM_INSTALL_CMD = 'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prefer the remote user — correct HOME and ownership on the common image whose global prefix is
|
|
31
|
+
* chowned to that user. Fall back to a root install with HOME pointed at the user's home when the
|
|
32
|
+
* user can't write a root-owned global prefix (`su` would EACCES there). Success is decided by the
|
|
33
|
+
* post-install `ver()`, so neither branch's exit status matters.
|
|
34
|
+
*/
|
|
35
|
+
const REINSTALL =
|
|
36
|
+
`if run_as '${NPM_INSTALL_CMD}'; then :; ` +
|
|
37
|
+
`elif [ "$(id -un)" != "$u" ]; then HOME="$h" ${NPM_INSTALL_CMD} || true; fi; `;
|
|
38
|
+
|
|
12
39
|
export const UPDATE_SCRIPT =
|
|
13
40
|
'command -v claude >/dev/null 2>&1 || exit 0; ' +
|
|
14
41
|
'command -v npm >/dev/null 2>&1 || exit 0; ' +
|
|
15
|
-
|
|
42
|
+
RUN_AS_PRELUDE +
|
|
43
|
+
'installed="$(ver)"; ' +
|
|
16
44
|
'latest=$(npm view @anthropic-ai/claude-code version 2>/dev/null); ' +
|
|
17
45
|
'if [ -z "$latest" ]; then exit 0; fi; ' +
|
|
18
46
|
'if [ "$installed" = "$latest" ]; then echo "current $latest"; exit 0; fi; ' +
|
|
19
|
-
|
|
20
|
-
'
|
|
47
|
+
REINSTALL +
|
|
48
|
+
'now="$(ver)"; ' +
|
|
49
|
+
'if [ "$now" = "$latest" ]; then echo "updated ${installed:-none} -> $now"; exit 0; fi; ' +
|
|
50
|
+
'exit 1';
|
|
21
51
|
|
|
22
52
|
/**
|
|
23
53
|
* Same check with the latest version supplied by the host as `$0` (exec args, never interpolated
|
|
24
|
-
* into the script), skipping the in-container `npm view` registry round-trip.
|
|
54
|
+
* into the script), skipping the in-container `npm view` registry round-trip. `$1` is the remote
|
|
55
|
+
* user — see `UPDATE_SCRIPT` above for why the reinstall must run as that user.
|
|
25
56
|
*/
|
|
26
57
|
export const PINNED_UPDATE_SCRIPT =
|
|
27
58
|
'latest="$0"; ' +
|
|
28
59
|
'command -v claude >/dev/null 2>&1 || exit 0; ' +
|
|
29
60
|
'command -v npm >/dev/null 2>&1 || exit 0; ' +
|
|
30
|
-
|
|
61
|
+
RUN_AS_PRELUDE +
|
|
62
|
+
'installed="$(ver)"; ' +
|
|
31
63
|
'if [ "$installed" = "$latest" ]; then echo "current $latest"; exit 0; fi; ' +
|
|
32
|
-
|
|
33
|
-
'
|
|
64
|
+
REINSTALL +
|
|
65
|
+
'now="$(ver)"; ' +
|
|
66
|
+
'if [ "$now" = "$latest" ]; then echo "updated ${installed:-none} -> $now"; exit 0; fi; ' +
|
|
67
|
+
'exit 1';
|
|
34
68
|
|
|
35
69
|
/** Anything looser could smuggle shell metacharacters out of a corrupted DB row into a container. */
|
|
36
70
|
export const VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
@@ -84,12 +118,13 @@ export const claudeCodeUpdate: Injection = {
|
|
|
84
118
|
async apply(target, log) {
|
|
85
119
|
log('Checking Claude Code is up to date…\n');
|
|
86
120
|
const latest = await resolveLatestVersion();
|
|
87
|
-
//
|
|
121
|
+
// Runs as root (remoteUser omitted) so the script can `su -m` into the remote user for the
|
|
122
|
+
// reinstall, keeping Claude Code 2.x's native binary in that user's HOME (see UPDATE_SCRIPT).
|
|
88
123
|
const res = await execInContainer(
|
|
89
124
|
{ containerId: target.containerId },
|
|
90
125
|
latest
|
|
91
|
-
? { script: PINNED_UPDATE_SCRIPT, args: [latest], capture: true }
|
|
92
|
-
: { script: UPDATE_SCRIPT, capture: true }
|
|
126
|
+
? { script: PINNED_UPDATE_SCRIPT, args: [latest, target.remoteUser ?? ''], capture: true }
|
|
127
|
+
: { script: UPDATE_SCRIPT, args: ['claude-update', target.remoteUser ?? ''], capture: true }
|
|
93
128
|
);
|
|
94
129
|
if (!res.ok) {
|
|
95
130
|
log(`⚠ Claude Code update check failed: ${res.error} — keeping the installed version\n`);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getOption } from '../lib/db.server.ts';
|
|
5
|
+
import { writeContainerFile } from '../lib/container-files.server.ts';
|
|
6
|
+
import {
|
|
7
|
+
claudeConfigFile,
|
|
8
|
+
mergeClaudeSettings,
|
|
9
|
+
readClaudeSettings
|
|
10
|
+
} from '../lib/claude-settings.server.ts';
|
|
11
|
+
import { readHostClaudeSettings } from './claude-statusline.ts';
|
|
12
|
+
import { normalizeOutputStyle } from '../types.ts';
|
|
13
|
+
import type { ClaudeOutputStyle } from '../types.ts';
|
|
14
|
+
import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
|
|
15
|
+
|
|
16
|
+
/** Also read by the settings-page `serverProps` to seed the UI. */
|
|
17
|
+
export function getClaudeOutputStyle(): ClaudeOutputStyle {
|
|
18
|
+
return normalizeOutputStyle(getOption('claude_output_style'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The `outputStyle` actually applied: the setting itself, the host's value when inheriting, or null. */
|
|
22
|
+
export async function resolveEffectiveOutputStyle(): Promise<string | null> {
|
|
23
|
+
const setting = getClaudeOutputStyle();
|
|
24
|
+
if (setting === 'none') return null;
|
|
25
|
+
if (setting !== 'default') return setting;
|
|
26
|
+
const style = (await readHostClaudeSettings())?.outputStyle;
|
|
27
|
+
return typeof style === 'string' && style.trim() ? style.trim() : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The host's custom output-style definitions, so a non-built-in name resolves inside the container. */
|
|
31
|
+
export async function readHostOutputStyleFiles(): Promise<{ name: string; content: string }[]> {
|
|
32
|
+
const dir = join(homedir(), '.claude', 'output-styles');
|
|
33
|
+
let names: string[];
|
|
34
|
+
try {
|
|
35
|
+
names = (await readdir(dir)).filter((n) => n.endsWith('.md'));
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const files = await Promise.all(
|
|
40
|
+
names.map(async (name) => {
|
|
41
|
+
try {
|
|
42
|
+
return { name, content: await readFile(join(dir, name), 'utf8') };
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
return files.filter((f): f is { name: string; content: string } => f !== null);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Writes Claude Code's `outputStyle` default into the container, mirroring host custom-style files. */
|
|
52
|
+
export const claudeOutputStyle: Injection = {
|
|
53
|
+
id: 'claude-output-style',
|
|
54
|
+
label: 'output style',
|
|
55
|
+
|
|
56
|
+
async apply(target: ContainerTarget, log) {
|
|
57
|
+
const style = await resolveEffectiveOutputStyle();
|
|
58
|
+
if (!style) {
|
|
59
|
+
log('No Claude output style to apply; skipped\n');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const file of await readHostOutputStyleFiles()) {
|
|
63
|
+
await writeContainerFile(
|
|
64
|
+
target,
|
|
65
|
+
claudeConfigFile(`output-styles/${file.name}`, '644'),
|
|
66
|
+
file.content
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
log(`Setting Claude output style (${style})…\n`);
|
|
70
|
+
const result = await mergeClaudeSettings(target, { outputStyle: style });
|
|
71
|
+
log(
|
|
72
|
+
result.ok
|
|
73
|
+
? '✓ Claude output style configured in container\n'
|
|
74
|
+
: `⚠ Claude output style injection failed: ${result.error}\n`
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async check(target) {
|
|
79
|
+
const expected = await resolveEffectiveOutputStyle();
|
|
80
|
+
const settings = await readClaudeSettings(target);
|
|
81
|
+
const actual = settings?.outputStyle;
|
|
82
|
+
return expected === null ? actual == null : actual === expected;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
@@ -17,6 +17,7 @@ import { attentionHooks } from '../container-injections/attention-hooks.ts';
|
|
|
17
17
|
import { claudeStatusline } from '../container-injections/claude-statusline.ts';
|
|
18
18
|
import { claudeModel } from '../container-injections/claude-model.ts';
|
|
19
19
|
import { claudeEffortLevel } from '../container-injections/claude-effort-level.ts';
|
|
20
|
+
import { claudeOutputStyle } from '../container-injections/claude-output-style.ts';
|
|
20
21
|
import { claudePermissionMode } from '../container-injections/claude-permission-mode.ts';
|
|
21
22
|
import { claudeTrust } from '../container-injections/claude-trust.ts';
|
|
22
23
|
import { claudeAliases } from '../container-injections/claude-aliases.ts';
|
|
@@ -78,7 +79,10 @@ function buildStages(claudeInjection: Injection): Injection[][] {
|
|
|
78
79
|
// claude-trust edits ~/.claude.json, which the stage-1 Claude slot also writes.
|
|
79
80
|
// custom-env-vars writes nothing (values ride containerEnv) — it only verifies + health-checks.
|
|
80
81
|
[claudeTrust, hostEnvVars, customEnvVars],
|
|
81
|
-
[claudeNoCoauthor]
|
|
82
|
+
[claudeNoCoauthor],
|
|
83
|
+
// A settings.json writer of its own: every other stage already has one, and same-stage
|
|
84
|
+
// settings.json writers race — so output style merges last, on its own.
|
|
85
|
+
[claudeOutputStyle]
|
|
82
86
|
];
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
import '@fontsource-variable/jetbrains-mono';
|
|
4
4
|
import SettingsView from '../components/SettingsView.svelte';
|
|
5
5
|
import type { AvatarArt } from '../avatars/index.ts';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ClaudeEffortLevel,
|
|
8
|
+
ClaudeOutputStyle,
|
|
9
|
+
ClaudePermissionMode,
|
|
10
|
+
InstanceMode
|
|
11
|
+
} from '../types.ts';
|
|
7
12
|
|
|
8
13
|
let {
|
|
9
14
|
pet,
|
|
10
15
|
defaultMode,
|
|
11
16
|
claudePermissionMode,
|
|
12
17
|
claudeEffortLevel,
|
|
18
|
+
claudeOutputStyle,
|
|
13
19
|
defaultImage,
|
|
14
20
|
builtinImage,
|
|
15
21
|
disableBuildCache,
|
|
@@ -50,6 +56,7 @@
|
|
|
50
56
|
defaultMode: InstanceMode;
|
|
51
57
|
claudePermissionMode: ClaudePermissionMode;
|
|
52
58
|
claudeEffortLevel: ClaudeEffortLevel;
|
|
59
|
+
claudeOutputStyle: ClaudeOutputStyle;
|
|
53
60
|
defaultImage: string;
|
|
54
61
|
builtinImage: string;
|
|
55
62
|
disableBuildCache: boolean;
|
|
@@ -93,6 +100,7 @@
|
|
|
93
100
|
{defaultMode}
|
|
94
101
|
{claudePermissionMode}
|
|
95
102
|
{claudeEffortLevel}
|
|
103
|
+
{claudeOutputStyle}
|
|
96
104
|
{defaultImage}
|
|
97
105
|
{builtinImage}
|
|
98
106
|
{disableBuildCache}
|
package/src/routes.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from './container-injections/claude-code-update.ts';
|
|
28
28
|
import { getClaudePermissionMode } from './container-injections/claude-permission-mode.ts';
|
|
29
29
|
import { getClaudeEffortLevel } from './container-injections/claude-effort-level.ts';
|
|
30
|
+
import { getClaudeOutputStyle } from './container-injections/claude-output-style.ts';
|
|
30
31
|
import { browse } from './lib/picker.server.ts';
|
|
31
32
|
import { pickNamePrompt } from './avatars/name-prompts.ts';
|
|
32
33
|
import { avatars, findAvatar } from './avatars/index.ts';
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
normalizeMode,
|
|
74
75
|
normalizePermissionMode,
|
|
75
76
|
normalizeEffortLevel,
|
|
77
|
+
normalizeOutputStyle,
|
|
76
78
|
type InstanceFilter
|
|
77
79
|
} from './types.ts';
|
|
78
80
|
|
|
@@ -205,6 +207,7 @@ export const routes: Record<string, MochiRouteValue> = {
|
|
|
205
207
|
defaultMode: getDefaultMode(),
|
|
206
208
|
claudePermissionMode: getClaudePermissionMode(),
|
|
207
209
|
claudeEffortLevel: getClaudeEffortLevel(),
|
|
210
|
+
claudeOutputStyle: getClaudeOutputStyle(),
|
|
208
211
|
defaultImage: getOption('default_image') ?? DEFAULT_IMAGE,
|
|
209
212
|
builtinImage: DEFAULT_IMAGE,
|
|
210
213
|
disableBuildCache: getOption('disable_build_cache') === '1',
|
|
@@ -277,6 +280,13 @@ export const routes: Record<string, MochiRouteValue> = {
|
|
|
277
280
|
return success({ level });
|
|
278
281
|
},
|
|
279
282
|
|
|
283
|
+
// Injected into ~/.claude/settings.json at provision time, so it lands on create/rebuild.
|
|
284
|
+
claudeOutputStyle: ({ formData }) => {
|
|
285
|
+
const style = normalizeOutputStyle(str(formData, 'style'));
|
|
286
|
+
setOption('claude_output_style', style);
|
|
287
|
+
return success({ style });
|
|
288
|
+
},
|
|
289
|
+
|
|
280
290
|
// Persist the default container image used when a source folder ships no devcontainer.json.
|
|
281
291
|
defaultImage: ({ formData }) => {
|
|
282
292
|
const image = str(formData, 'image');
|
package/src/types.ts
CHANGED
|
@@ -73,6 +73,22 @@ export function normalizeEffortLevel(value: unknown): ClaudeEffortLevel {
|
|
|
73
73
|
: 'high';
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Claude Code's output style for new sessions, written to `settings.json` as `outputStyle`.
|
|
78
|
+
* `'default'` inherits whatever the host has; `'none'` forces it off; anything else is a literal
|
|
79
|
+
* Claude output-style name — append future built-ins (`'Explanatory'`, `'Learning'`, …) here.
|
|
80
|
+
*/
|
|
81
|
+
export type ClaudeOutputStyle = 'default' | 'none' | 'Concise';
|
|
82
|
+
|
|
83
|
+
export const CLAUDE_OUTPUT_STYLES: ClaudeOutputStyle[] = ['default', 'none', 'Concise'];
|
|
84
|
+
|
|
85
|
+
/** Anything unrecognised falls back to inheriting the host's output style. */
|
|
86
|
+
export function normalizeOutputStyle(value: unknown): ClaudeOutputStyle {
|
|
87
|
+
return CLAUDE_OUTPUT_STYLES.includes(value as ClaudeOutputStyle)
|
|
88
|
+
? (value as ClaudeOutputStyle)
|
|
89
|
+
: 'default';
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/** Dashboard run-state view filter: All | Active (running/creating) | Stopped (stopped/error). */
|
|
77
93
|
export type InstanceFilter = 'all' | 'active' | 'stopped';
|
|
78
94
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{lb as a}from"/_mochi/client/chunk-d9bszcp8.js";import"/_mochi/client/chunk-2eaagsfc.js";export{a as registerComponent};
|