codebay 0.3.0 → 0.5.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 (56) hide show
  1. package/.mochi/manifest.json +296 -268
  2. package/.mochi/svelte-client/HydratableIsland-79z9xq4b.js +1 -0
  3. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-f5rzyjm8.js +2 -0
  4. package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-3w16y9jh.js → _hydrate-AvatarsView_257s9yq1wwr4f-fxyd5qx4.js} +2 -2
  5. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-nv31ptdp.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-r9vvc7rx.js +28 -0
  7. package/.mochi/svelte-client/chunk-6eexkr6p.js +2 -0
  8. package/.mochi/svelte-client/chunk-7znwaf7t.js +2 -0
  9. package/.mochi/svelte-client/chunk-dz1019cy.js +1 -0
  10. package/.mochi/svelte-client/{chunk-qxwt25hn.js → chunk-nmf4dq86.js} +1 -1
  11. package/.mochi/svelte-client/{chunk-0jtmkpx4.js → chunk-ssa0ad2f.js} +2 -2
  12. package/.mochi/svelte-client/chunk-zmmb1c4b.js +2 -0
  13. package/.mochi/svelte-compile/{App-f0q5n4rk.server.js → App-9qjy14bc.server.js} +38 -23
  14. package/.mochi/svelte-compile/{Avatars-p8gfef7x.server.js → Avatars-sq8mjn6k.server.js} +2 -2
  15. package/.mochi/svelte-compile/{Instance-atjwsczx.server.js → Instance-13j59daw.server.js} +2 -2
  16. package/.mochi/svelte-compile/{Settings-q88xw6h8.server.js → Settings-p0s2awg1.server.js} +404 -103
  17. package/.mochi/svelte-compile/{chunk-9bjk7yx6.js → chunk-6ghzacpg.js} +1 -1
  18. package/.mochi/svelte-compile/{chunk-4kwy539a.js → chunk-bacb4b0r.js} +1 -1
  19. package/.mochi/svelte-compile/{chunk-sxqys3nw.js → chunk-dfe2km6a.js} +5 -2
  20. package/.mochi/svelte-compile/{chunk-v6796ga4.js → chunk-fpe16ywq.js} +28 -6
  21. package/.mochi/svelte-css/Brand-1bh2b800g5vrn.css +1 -0
  22. package/.mochi/svelte-css/SettingsView-3uycxa8z548m4.css +1 -0
  23. package/README.md +4 -0
  24. package/package.json +8 -8
  25. package/src/avatars/index.ts +5 -0
  26. package/src/components/AppBar.svelte +8 -1
  27. package/src/components/AppShell.svelte +17 -3
  28. package/src/components/Brand.svelte +15 -4
  29. package/src/components/DashboardView.svelte +5 -2
  30. package/src/components/IdeBar.svelte +9 -1
  31. package/src/components/InstanceCard.svelte +7 -3
  32. package/src/components/InstanceView.svelte +4 -1
  33. package/src/components/SettingsCog.svelte +9 -1
  34. package/src/components/SettingsView.svelte +476 -18
  35. package/src/components/TopBar.svelte +4 -1
  36. package/src/container-injections/claude-code-models.ts +74 -0
  37. package/src/container-injections/claude-model.ts +66 -0
  38. package/src/container-injections/claude-no-coauthor.ts +11 -1
  39. package/src/container-injections/claude-statusline.ts +24 -9
  40. package/src/container-injections/git-identity.ts +30 -3
  41. package/src/lib/injections.server.ts +5 -0
  42. package/src/lib/instances.server.ts +10 -1
  43. package/src/lib/popup-nav.ts +37 -0
  44. package/src/pages/App.svelte +10 -3
  45. package/src/pages/Settings.svelte +31 -0
  46. package/src/routes.ts +88 -2
  47. package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +0 -1
  48. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-faqf4hmc.js +0 -2
  49. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-5g8meb6a.js +0 -1
  50. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-jsw4edxb.js +0 -23
  51. package/.mochi/svelte-client/chunk-04qv4w6w.js +0 -2
  52. package/.mochi/svelte-client/chunk-597d17x0.js +0 -1
  53. package/.mochi/svelte-client/chunk-5p31aqh0.js +0 -2
  54. package/.mochi/svelte-client/chunk-gjjhya9m.js +0 -2
  55. package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +0 -1
  56. package/.mochi/svelte-css/SettingsView-1nfdwnm16cpp2.css +0 -1
@@ -3,6 +3,7 @@
3
3
  import Component from '@lucide/svelte/icons/component';
4
4
  import { isDev } from 'mochi-framework';
5
5
  import type { AuthProvider } from '../types.ts';
6
+ import type { AvatarArt } from '../avatars/index.ts';
6
7
  import Brand from './Brand.svelte';
7
8
  import SettingsCog from './SettingsCog.svelte';
8
9
  import CredMenu from './CredMenu.svelte';
@@ -11,6 +12,7 @@
11
12
  // Action state is injected so the bar stays presentational and reusable on /debug.
12
13
  let {
13
14
  auth,
15
+ pet,
14
16
  canDelete = false,
15
17
  ready = true,
16
18
  creating = false,
@@ -18,6 +20,7 @@
18
20
  onDeleteAll
19
21
  }: {
20
22
  auth: AuthProvider[];
23
+ pet?: AvatarArt;
21
24
  canDelete?: boolean;
22
25
  ready?: boolean;
23
26
  creating?: boolean;
@@ -27,7 +30,7 @@
27
30
  </script>
28
31
 
29
32
  <header class="topbar">
30
- <Brand />
33
+ <Brand {pet} />
31
34
  <div class="topbar-actions">
32
35
  {#if isDev}
33
36
  <Button variant="default" size="sm" icon={Component} href="/debug">Debug</Button>
@@ -0,0 +1,74 @@
1
+ import { getOption } from '../lib/db.server.ts';
2
+ import { execInContainer } from '../lib/exec.server.ts';
3
+ import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
4
+
5
+ /** The option key → Claude env var mapping; order is the field order in Settings. */
6
+ const MODEL_ENV: Array<[key: string, env: string]> = [
7
+ ['manual_opus_model', 'ANTHROPIC_DEFAULT_OPUS_MODEL'],
8
+ ['manual_sonnet_model', 'ANTHROPIC_DEFAULT_SONNET_MODEL'],
9
+ ['manual_haiku_model', 'ANTHROPIC_DEFAULT_HAIKU_MODEL'],
10
+ ['manual_small_fast_model', 'ANTHROPIC_SMALL_FAST_MODEL'],
11
+ ['manual_model', 'ANTHROPIC_MODEL']
12
+ ];
13
+
14
+ /**
15
+ * Only for the standard (subscription) path — null when disabled, when LiteLLM
16
+ * owns the models instead, or when no field is filled (nothing to export).
17
+ */
18
+ export function manualModelConfig(): Record<string, string> | null {
19
+ if (getOption('manual_model_override_enabled') !== '1') return null;
20
+ if (getOption('custom_endpoint_enabled') === '1') return null;
21
+ const filled: Record<string, string> = {};
22
+ for (const [key, env] of MODEL_ENV) {
23
+ const value = getOption(key)?.trim();
24
+ if (value) filled[env] = value;
25
+ }
26
+ return Object.keys(filled).length ? filled : null;
27
+ }
28
+
29
+ const ENV_FILE = '~/.codebay-claude-models-env';
30
+
31
+ /** Model IDs are non-secret, so every value rides `args` ($1, $2, …) — no stdin. */
32
+ async function injectModels(
33
+ target: ContainerTarget,
34
+ config: Record<string, string>
35
+ ): Promise<{ ok: boolean; error?: string }> {
36
+ const entries = Object.entries(config);
37
+ const writes = entries.map(([env], i) => `printf 'export ${env}=%s\\n' "$${i + 1}"; `).join('');
38
+ const script =
39
+ 'set -e; f=$(eval echo "' +
40
+ ENV_FILE +
41
+ '"); ' +
42
+ '{ ' +
43
+ writes +
44
+ '} > "$f"; chmod 600 "$f"; ' +
45
+ // The grep guard keeps a re-apply from stacking duplicate source lines.
46
+ 'h=$(eval echo ~$(id -un)); src="[ -f \\"$f\\" ] && . \\"$f\\""; ' +
47
+ 'for rc in "$h/.bashrc" "$h/.zshrc"; do ' +
48
+ 'grep -qF "$src" "$rc" 2>/dev/null || printf \'%s\\n\' "$src" >> "$rc"; ' +
49
+ 'done';
50
+
51
+ const res = await execInContainer(target, {
52
+ script,
53
+ args: ['claude-models', ...entries.map(([, value]) => value)]
54
+ });
55
+ return res.ok ? { ok: true } : { ok: false, error: res.error };
56
+ }
57
+
58
+ /** Sets Claude model env vars on the standard path; skipped when LiteLLM is active. */
59
+ export const claudeCodeModels: Injection = {
60
+ id: 'claude-code-models',
61
+ label: 'Model override',
62
+
63
+ async apply(target, log) {
64
+ const config = manualModelConfig();
65
+ if (!config) return;
66
+ log(`Injecting Claude model override (${Object.keys(config).join(', ')})…\n`);
67
+ const result = await injectModels(target, config);
68
+ log(
69
+ result.ok
70
+ ? '✓ Claude model override configured in container\n'
71
+ : `⚠ Claude model override injection failed: ${result.error}\n`
72
+ );
73
+ }
74
+ };
@@ -0,0 +1,66 @@
1
+ import { getOption } from '../lib/db.server.ts';
2
+ import { checkPresence, execInContainer } from '../lib/exec.server.ts';
3
+ import { mergeClaudeSettingsScript } from './attention-hooks.ts';
4
+ import { readHostClaudeSettings } from './claude-statusline.ts';
5
+ import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
6
+
7
+ /** The app's own model config (manual override / LiteLLM) exports ANTHROPIC_MODEL, which wins over settings.json. */
8
+ function appOwnsModel(): boolean {
9
+ return (
10
+ getOption('manual_model_override_enabled') === '1' ||
11
+ getOption('custom_endpoint_enabled') === '1'
12
+ );
13
+ }
14
+
15
+ /** The `model` default from host ~/.claude/settings.json, or null when unset or app-owned. */
16
+ export async function hostClaudeModel(): Promise<string | null> {
17
+ if (appOwnsModel()) return null;
18
+ const settings = await readHostClaudeSettings();
19
+ const model = settings?.model;
20
+ return typeof model === 'string' && model.trim() ? model.trim() : null;
21
+ }
22
+
23
+ async function injectModel(
24
+ target: ContainerTarget,
25
+ model: string
26
+ ): Promise<{ ok: boolean; error?: string }> {
27
+ const res = await execInContainer(target, {
28
+ script: mergeClaudeSettingsScript(),
29
+ stdin: JSON.stringify({ model })
30
+ });
31
+ return res.ok ? { ok: true } : { ok: false, error: res.error };
32
+ }
33
+
34
+ /** Mirrors the host's default `model` into the container so subscription instances match the host. */
35
+ export const claudeModel: Injection = {
36
+ id: 'claude-model',
37
+ label: 'model default',
38
+
39
+ auth: {
40
+ hint: 'set `model` in ~/.claude/settings.json',
41
+ async status() {
42
+ const model = await hostClaudeModel();
43
+ return { available: model !== null, source: model ? '~/.claude/settings.json' : null };
44
+ }
45
+ },
46
+
47
+ async apply(target, log) {
48
+ const model = await hostClaudeModel();
49
+ if (!model) return;
50
+ log(`Injecting Claude model default (${model})…\n`);
51
+ const result = await injectModel(target, model);
52
+ log(
53
+ result.ok
54
+ ? '✓ Claude model default configured in container\n'
55
+ : `⚠ Claude model default injection failed: ${result.error}\n`
56
+ );
57
+ },
58
+
59
+ async check(target) {
60
+ return checkPresence(
61
+ target,
62
+ 'h=$(eval echo ~$(id -un)); d="${CLAUDE_CONFIG_DIR:-$h/.claude}"; ' +
63
+ '[ -s "$d/settings.json" ] && grep -q \'"model"\' "$d/settings.json" && echo 1 || echo 0'
64
+ );
65
+ }
66
+ };
@@ -2,10 +2,20 @@ import { checkPresence, execInContainer } from '../lib/exec.server.ts';
2
2
  import { mergeClaudeSettingsScript } from './attention-hooks.ts';
3
3
  import type { ContainerTarget, Injection } from '../lib/injections.server.ts';
4
4
 
5
+ /**
6
+ * Both schemas on purpose: newer `claude` reads the nested `attribution` object, older builds
7
+ * the root-level `includeCoAuthoredBy` — setting both suppresses the byline across versions.
8
+ * Empty `commit`/`pr` strings drop the commit trailer and PR footer.
9
+ */
10
+ export const NO_COAUTHOR_SETTINGS = {
11
+ includeCoAuthoredBy: false,
12
+ attribution: { commit: '', pr: '', includeCoAuthoredBy: false }
13
+ };
14
+
5
15
  async function injectNoCoauthor(target: ContainerTarget): Promise<{ ok: boolean; error?: string }> {
6
16
  const res = await execInContainer(target, {
7
17
  script: mergeClaudeSettingsScript(),
8
- stdin: JSON.stringify({ includeCoAuthoredBy: false })
18
+ stdin: JSON.stringify(NO_COAUTHOR_SETTINGS)
9
19
  });
10
20
  return res.ok ? { ok: true } : { ok: false, error: res.error };
11
21
  }
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, statSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
@@ -15,7 +15,7 @@ interface StatusLineConfig {
15
15
  script?: string;
16
16
  }
17
17
 
18
- async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
18
+ export async function readHostClaudeSettings(): Promise<Record<string, unknown> | null> {
19
19
  const file = join(homedir(), '.claude', 'settings.json');
20
20
  if (!existsSync(file)) return null;
21
21
  try {
@@ -25,9 +25,23 @@ async function readHostClaudeSettings(): Promise<Record<string, unknown> | null>
25
25
  }
26
26
  }
27
27
 
28
- /** A heuristic: a statusLine command is either a script path or a package runner with no file. */
28
+ export const expandTilde = (p: string): string =>
29
+ p.startsWith('~/') ? join(homedir(), p.slice(2)) : p;
30
+
31
+ /** Only a real file counts — else jq's `//` operator and a bare `/` resolve to the root dir. */
32
+ const isExistingFile = (p: string): boolean => {
33
+ try {
34
+ return statSync(p).isFile();
35
+ } catch {
36
+ return false;
37
+ }
38
+ };
39
+
40
+ /** A heuristic: a statusLine command is either a script path or an inline snippet with no file. */
29
41
  export function extractScriptPath(command: string): string | null {
30
- const token = command.split(/\s+/).find((t) => t.startsWith('/') && existsSync(t));
42
+ const token = command
43
+ .split(/\s+/)
44
+ .find((t) => (t.startsWith('/') || t.startsWith('~/')) && isExistingFile(expandTilde(t)));
31
45
  return token ?? null;
32
46
  }
33
47
 
@@ -37,16 +51,17 @@ export async function readStatusLineConfig(): Promise<StatusLineConfig | null> {
37
51
  const statusLine = settings?.statusLine as { type?: string; command?: string } | undefined;
38
52
  if (!statusLine || statusLine.type !== 'command' || !statusLine.command) return null;
39
53
 
40
- const hostPath = extractScriptPath(statusLine.command);
41
- if (!hostPath) return { statusLine };
54
+ const token = extractScriptPath(statusLine.command);
55
+ if (!token) return { statusLine };
42
56
 
43
57
  let script: string;
44
58
  try {
45
- script = await readFile(hostPath, 'utf8');
59
+ script = await readFile(expandTilde(token), 'utf8');
46
60
  } catch {
47
- return null;
61
+ // A false-positive match must not disable the feature — inject the command verbatim.
62
+ return { statusLine };
48
63
  }
49
- const command = statusLine.command.replaceAll(hostPath, CONTAINER_SCRIPT_PATH);
64
+ const command = statusLine.command.replaceAll(token, CONTAINER_SCRIPT_PATH);
50
65
  return { statusLine: { ...statusLine, command }, script };
51
66
  }
52
67
 
@@ -1,5 +1,6 @@
1
1
  import { checkPresence, execInContainer } from '../lib/exec.server.ts';
2
2
  import { spawnCapture } from '../lib/spawn.server.ts';
3
+ import { getOption } from '../lib/db.server.ts';
3
4
  import type { Injection } from '../lib/injections.server.ts';
4
5
 
5
6
  interface GitIdentity {
@@ -11,8 +12,29 @@ function readGitConfig(key: string): Promise<string | null> {
11
12
  return spawnCapture(['git', 'config', '--global', '--get', key]);
12
13
  }
13
14
 
14
- /** `--global` so this reads the host user's identity, not the manager checkout's override. */
15
- async function readGitIdentity(): Promise<GitIdentity | null> {
15
+ /**
16
+ * The toggle is the source of truth, but predates the option: an unset value falls
17
+ * back to "on when both fields are already filled" so existing overrides keep working.
18
+ */
19
+ export function gitIdentityEnabled(): boolean {
20
+ const explicit = getOption('git_identity_enabled');
21
+ if (explicit === '1') return true;
22
+ if (explicit === '0') return false;
23
+ return !!(getOption('git_identity_name')?.trim() && getOption('git_identity_email')?.trim());
24
+ }
25
+
26
+ /** Both fields are required together — a lone name or email is treated as not configured. */
27
+ function overrideIdentity(): GitIdentity | null {
28
+ if (!gitIdentityEnabled()) return null;
29
+ const name = getOption('git_identity_name')?.trim() || '';
30
+ const email = getOption('git_identity_email')?.trim() || '';
31
+ return name && email ? { name, email } : null;
32
+ }
33
+
34
+ /** `--global` so the host fallback reads the host user's identity, not the manager checkout's. */
35
+ export async function readGitIdentity(): Promise<GitIdentity | null> {
36
+ const override = overrideIdentity();
37
+ if (override) return override;
16
38
  const [name, email] = await Promise.all([
17
39
  readGitConfig('user.name'),
18
40
  readGitConfig('user.email')
@@ -30,7 +52,12 @@ export const gitIdentity: Injection = {
30
52
  async status() {
31
53
  const identity = await readGitIdentity();
32
54
  return identity
33
- ? { available: true, source: `git config — ${identity.name}` }
55
+ ? {
56
+ available: true,
57
+ source: overrideIdentity()
58
+ ? `Settings override — ${identity.name}`
59
+ : `git config — ${identity.name}`
60
+ }
34
61
  : { available: false, source: null };
35
62
  }
36
63
  },
@@ -6,9 +6,11 @@ import { tmux } from '../container-injections/tmux.ts';
6
6
  import { gitIdentity } from '../container-injections/git-identity.ts';
7
7
  import { claudeCodeCredentials } from '../container-injections/claude-code-credentials.ts';
8
8
  import { claudeCodeCustom } from '../container-injections/claude-code-custom.ts';
9
+ import { claudeCodeModels } from '../container-injections/claude-code-models.ts';
9
10
  import { githubCredentials } from '../container-injections/github-credentials.ts';
10
11
  import { attentionHooks } from '../container-injections/attention-hooks.ts';
11
12
  import { claudeStatusline } from '../container-injections/claude-statusline.ts';
13
+ import { claudeModel } from '../container-injections/claude-model.ts';
12
14
  import { claudeSkipPermissions } from '../container-injections/claude-skip-permissions.ts';
13
15
  import { claudeAliases } from '../container-injections/claude-aliases.ts';
14
16
  import { claudeNoCoauthor } from '../container-injections/claude-no-coauthor.ts';
@@ -48,9 +50,12 @@ const BASE_INJECTIONS_HEAD: Injection[] = [
48
50
  ];
49
51
 
50
52
  const BASE_INJECTIONS_TAIL: Injection[] = [
53
+ // Self-skips unless manual override is on and LiteLLM off, so it's safe in the always-run tail.
54
+ claudeCodeModels,
51
55
  githubCredentials,
52
56
  attentionHooks,
53
57
  claudeStatusline,
58
+ claudeModel,
54
59
  claudeSkipPermissions,
55
60
  claudeAliases,
56
61
  claudeNoCoauthor,
@@ -104,7 +104,9 @@ export function subscribeLogs(id: string, onChunk: (chunk: string) => void): ()
104
104
  export type StreamEvent =
105
105
  | { type: 'instances'; data: Instance[] }
106
106
  | { type: 'health'; data: { id: string; health: InstanceHealth } }
107
- | { type: 'preflight'; data: { docker: boolean; cli: boolean } };
107
+ | { type: 'preflight'; data: { docker: boolean; cli: boolean } }
108
+ // `name` is the chosen sprite, or null for the default box logo. Lets the header swap live.
109
+ | { type: 'pet'; data: { name: string | null } };
108
110
 
109
111
  interface StreamHub {
110
112
  sockets: Set<ServerWebSocket<unknown>>;
@@ -150,6 +152,11 @@ export function broadcastHealth(id: string, health: InstanceHealth): void {
150
152
  broadcast({ type: 'health', data: { id, health } });
151
153
  }
152
154
 
155
+ /** Called from the settings action so every open dashboard swaps its header logo without a reload. */
156
+ export function broadcastPet(name: string | null): void {
157
+ broadcast({ type: 'pet', data: { name } });
158
+ }
159
+
153
160
  async function reconcileInstances(force = false): Promise<void> {
154
161
  const list = await listInstances();
155
162
  const listJson = JSON.stringify(list);
@@ -189,6 +196,8 @@ export function streamOpen(ws: ServerWebSocket<unknown>): void {
189
196
  void listInstances().then((list) => sendTo(ws, { type: 'instances', data: list }));
190
197
  for (const snap of currentHealthSnapshots()) sendTo(ws, { type: 'health', data: snap });
191
198
  void backgroundPreflight().then((pf) => sendTo(ws, { type: 'preflight', data: pf }));
199
+ // Keeps a reconnecting client correct even if the pet changed while it was away.
200
+ sendTo(ws, { type: 'pet', data: { name: getOption('pet') || null } });
192
201
  }
193
202
 
194
203
  export function streamClose(ws: ServerWebSocket<unknown>): void {
@@ -0,0 +1,37 @@
1
+ const POPUP_PARAM = 'popup';
2
+
3
+ /** Assumes `href` has no existing query string. */
4
+ export function withPopupMarker(href: string): string {
5
+ return `${href}?${POPUP_PARAM}=1`;
6
+ }
7
+
8
+ export function isPopupPage(): boolean {
9
+ return new URLSearchParams(location.search).has(POPUP_PARAM);
10
+ }
11
+
12
+ function closeOrGoHome() {
13
+ window.close();
14
+ // Runs only if close() was refused (tab wasn't opened by script) — window.close()
15
+ // tears the page down near-instantly when it succeeds, so this never fires then.
16
+ setTimeout(() => {
17
+ location.href = '/';
18
+ }, 100);
19
+ }
20
+
21
+ export function onBackLinkClick(e: MouseEvent) {
22
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
23
+ return;
24
+ if (!isPopupPage()) return;
25
+ e.preventDefault();
26
+ closeOrGoHome();
27
+ }
28
+
29
+ export function installPopupBackTrap(): () => void {
30
+ if (!isPopupPage()) return () => {};
31
+ // A fresh popup tab has one history entry, so back is otherwise a no-op the
32
+ // browser just disables — push a duplicate so the first back press fires popstate.
33
+ history.pushState({}, '', location.href);
34
+ const onPop = () => closeOrGoHome();
35
+ window.addEventListener('popstate', onPop);
36
+ return () => window.removeEventListener('popstate', onPop);
37
+ }
@@ -3,12 +3,19 @@
3
3
  import '@fontsource-variable/jetbrains-mono';
4
4
  import AppShell from '../components/AppShell.svelte';
5
5
  import type { Instance, Preflight } from '../types.ts';
6
+ import type { AvatarArt } from '../avatars/index.ts';
6
7
 
7
8
  let {
8
9
  preflight,
9
10
  initialPath,
10
- snapshot
11
- }: { preflight: Preflight; initialPath: string; snapshot: Instance[] } = $props();
11
+ snapshot,
12
+ pet
13
+ }: {
14
+ preflight: Preflight;
15
+ initialPath: string;
16
+ snapshot: Instance[];
17
+ pet?: AvatarArt;
18
+ } = $props();
12
19
  </script>
13
20
 
14
- <AppShell {preflight} {initialPath} {snapshot} mochi:hydrate />
21
+ <AppShell {preflight} {initialPath} {snapshot} {pet} mochi:hydrate />
@@ -2,13 +2,18 @@
2
2
  import '@fontsource-variable/doto';
3
3
  import '@fontsource-variable/jetbrains-mono';
4
4
  import SettingsView from '../components/SettingsView.svelte';
5
+ import type { AvatarArt } from '../avatars/index.ts';
5
6
 
6
7
  let {
8
+ pet,
7
9
  defaultImage,
8
10
  builtinImage,
9
11
  disableBuildCache,
10
12
  copyIgnorePatterns,
11
13
  builtinCopyIgnore,
14
+ gitIdentityEnabled,
15
+ gitIdentityName,
16
+ gitIdentityEmail,
12
17
  dockerArch,
13
18
  manualTokensEnabled,
14
19
  githubTokenSet,
@@ -21,16 +26,26 @@
21
26
  customEndpointHaikuModel,
22
27
  customEndpointSmallFastModel,
23
28
  customEndpointModel,
29
+ manualModelOverrideEnabled,
30
+ manualOpusModel,
31
+ manualSonnetModel,
32
+ manualHaikuModel,
33
+ manualSmallFastModel,
34
+ manualModel,
24
35
  hostEnvVarsEnabled,
25
36
  hostEnvVarNames,
26
37
  hostEnvVarPresence,
27
38
  version
28
39
  }: {
40
+ pet?: AvatarArt;
29
41
  defaultImage: string;
30
42
  builtinImage: string;
31
43
  disableBuildCache: boolean;
32
44
  copyIgnorePatterns: string;
33
45
  builtinCopyIgnore: string;
46
+ gitIdentityEnabled: boolean;
47
+ gitIdentityName: string;
48
+ gitIdentityEmail: string;
34
49
  dockerArch: string | null;
35
50
  manualTokensEnabled: boolean;
36
51
  githubTokenSet: boolean;
@@ -43,6 +58,12 @@
43
58
  customEndpointHaikuModel: string;
44
59
  customEndpointSmallFastModel: string;
45
60
  customEndpointModel: string;
61
+ manualModelOverrideEnabled: boolean;
62
+ manualOpusModel: string;
63
+ manualSonnetModel: string;
64
+ manualHaikuModel: string;
65
+ manualSmallFastModel: string;
66
+ manualModel: string;
46
67
  hostEnvVarsEnabled: boolean;
47
68
  hostEnvVarNames: string[];
48
69
  hostEnvVarPresence: Record<string, boolean>;
@@ -51,11 +72,15 @@
51
72
  </script>
52
73
 
53
74
  <SettingsView
75
+ {pet}
54
76
  {defaultImage}
55
77
  {builtinImage}
56
78
  {disableBuildCache}
57
79
  {copyIgnorePatterns}
58
80
  {builtinCopyIgnore}
81
+ {gitIdentityEnabled}
82
+ {gitIdentityName}
83
+ {gitIdentityEmail}
59
84
  {dockerArch}
60
85
  {manualTokensEnabled}
61
86
  {githubTokenSet}
@@ -68,6 +93,12 @@
68
93
  {customEndpointHaikuModel}
69
94
  {customEndpointSmallFastModel}
70
95
  {customEndpointModel}
96
+ {manualModelOverrideEnabled}
97
+ {manualOpusModel}
98
+ {manualSonnetModel}
99
+ {manualHaikuModel}
100
+ {manualSmallFastModel}
101
+ {manualModel}
71
102
  {hostEnvVarsEnabled}
72
103
  {hostEnvVarNames}
73
104
  {hostEnvVarPresence}