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.
Files changed (36) hide show
  1. package/.mochi/manifest.json +107 -99
  2. package/.mochi/svelte-client/HydratableIsland-zrdsrsmg.js +1 -0
  3. package/.mochi/svelte-client/{_hydrate-AppShell_2rtnxn6pgp2rh-5kb6kh2d.js → _hydrate-AppShell_2rtnxn6pgp2rh-v7y7dh53.js} +2 -2
  4. package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-c51gt15m.js → _hydrate-AvatarsView_257s9yq1wwr4f-tmwd6fpb.js} +1 -1
  5. package/.mochi/svelte-client/{_hydrate-InstanceView_2ma45l9bybfhl-d256bw54.js → _hydrate-InstanceView_2ma45l9bybfhl-m7t64dbh.js} +1 -1
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-wedc9x9y.js +39 -0
  7. package/.mochi/svelte-client/{chunk-2eaagsfc.js → chunk-0mc09cyb.js} +1 -1
  8. package/.mochi/svelte-client/{chunk-a5e8vytv.js → chunk-5jwfs4p9.js} +2 -2
  9. package/.mochi/svelte-client/chunk-6qg4q98a.js +2 -0
  10. package/.mochi/svelte-client/{chunk-d8rdrqsh.js → chunk-ccfh8hxz.js} +1 -1
  11. package/.mochi/svelte-client/{chunk-d9bszcp8.js → chunk-phf7v5jj.js} +1 -1
  12. package/.mochi/svelte-client/{chunk-y1xjps59.js → chunk-spk1b2r4.js} +1 -1
  13. package/.mochi/svelte-client/chunk-tmeh70wg.js +2 -0
  14. package/.mochi/svelte-client/{chunk-6v5d7dex.js → chunk-vs1yjbqt.js} +1 -1
  15. package/.mochi/svelte-client/{chunk-1ttqgrz7.js → chunk-x0m3k0rn.js} +1 -1
  16. package/.mochi/svelte-client/{chunk-s70rry6z.js → chunk-y7f4jbdm.js} +1 -1
  17. package/.mochi/svelte-compile/{App-dn3kh0xn.server.js → App-94xsdn0w.server.js} +2 -2
  18. package/.mochi/svelte-compile/{Avatars-7k5055m8.server.js → Avatars-fnccw47h.server.js} +1 -1
  19. package/.mochi/svelte-compile/{Instance-hj76qnha.server.js → Instance-knnkcn2p.server.js} +1 -1
  20. package/.mochi/svelte-compile/{Settings-m2bfa4k4.server.js → Settings-h7gsjyz3.server.js} +169 -114
  21. package/.mochi/svelte-compile/{chunk-qc0f73f3.js → chunk-apdte4nh.js} +7 -8
  22. package/.mochi/svelte-compile/{chunk-zd49axkf.js → chunk-w0hca7va.js} +2 -1
  23. package/package.json +1 -1
  24. package/src/avatars/bee.ts +7 -8
  25. package/src/components/SettingsView.svelte +74 -0
  26. package/src/container-injections/claude-code-install.ts +12 -7
  27. package/src/container-injections/claude-code-update.ts +45 -10
  28. package/src/container-injections/claude-output-style.ts +84 -0
  29. package/src/lib/injections.server.ts +5 -1
  30. package/src/pages/Settings.svelte +9 -1
  31. package/src/routes.ts +10 -0
  32. package/src/types.ts +16 -0
  33. package/.mochi/svelte-client/HydratableIsland-4j3bazeq.js +0 -1
  34. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-rw5z89pa.js +0 -39
  35. package/.mochi/svelte-client/chunk-pz6dq0v3.js +0 -2
  36. 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.12.1",
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",
@@ -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`) — the standalone installer puts the binary under that
12
- * user's `$HOME`, so running it as root would strand it in `/root`.
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
- 'if command -v npm >/dev/null 2>&1 && npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1; then\n' +
17
- ' p="$(npm prefix -g 2>/dev/null)"\n' +
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
- ' if command -v claude >/dev/null 2>&1 || [ -x "$p/bin/claude" ]; then exit 0; fi\n' +
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
- "installed=$(claude --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n1); " +
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
- 'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1 || exit 1; ' +
20
- 'echo "updated $installed -> $latest"';
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
- "installed=$(claude --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n1); " +
61
+ RUN_AS_PRELUDE +
62
+ 'installed="$(ver)"; ' +
31
63
  'if [ "$installed" = "$latest" ]; then echo "current $latest"; exit 0; fi; ' +
32
- 'npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1 || exit 1; ' +
33
- 'echo "updated $installed -> $latest"';
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
- // Omitting remoteUser runs as root, which the global npm reinstall needs.
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 { ClaudeEffortLevel, ClaudePermissionMode, InstanceMode } from '../types.ts';
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};