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.
- package/.mochi/manifest.json +296 -268
- package/.mochi/svelte-client/HydratableIsland-79z9xq4b.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-f5rzyjm8.js +2 -0
- package/.mochi/svelte-client/{_hydrate-AvatarsView_257s9yq1wwr4f-3w16y9jh.js → _hydrate-AvatarsView_257s9yq1wwr4f-fxyd5qx4.js} +2 -2
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-nv31ptdp.js +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-r9vvc7rx.js +28 -0
- package/.mochi/svelte-client/chunk-6eexkr6p.js +2 -0
- package/.mochi/svelte-client/chunk-7znwaf7t.js +2 -0
- package/.mochi/svelte-client/chunk-dz1019cy.js +1 -0
- package/.mochi/svelte-client/{chunk-qxwt25hn.js → chunk-nmf4dq86.js} +1 -1
- package/.mochi/svelte-client/{chunk-0jtmkpx4.js → chunk-ssa0ad2f.js} +2 -2
- package/.mochi/svelte-client/chunk-zmmb1c4b.js +2 -0
- package/.mochi/svelte-compile/{App-f0q5n4rk.server.js → App-9qjy14bc.server.js} +38 -23
- package/.mochi/svelte-compile/{Avatars-p8gfef7x.server.js → Avatars-sq8mjn6k.server.js} +2 -2
- package/.mochi/svelte-compile/{Instance-atjwsczx.server.js → Instance-13j59daw.server.js} +2 -2
- package/.mochi/svelte-compile/{Settings-q88xw6h8.server.js → Settings-p0s2awg1.server.js} +404 -103
- package/.mochi/svelte-compile/{chunk-9bjk7yx6.js → chunk-6ghzacpg.js} +1 -1
- package/.mochi/svelte-compile/{chunk-4kwy539a.js → chunk-bacb4b0r.js} +1 -1
- package/.mochi/svelte-compile/{chunk-sxqys3nw.js → chunk-dfe2km6a.js} +5 -2
- package/.mochi/svelte-compile/{chunk-v6796ga4.js → chunk-fpe16ywq.js} +28 -6
- package/.mochi/svelte-css/Brand-1bh2b800g5vrn.css +1 -0
- package/.mochi/svelte-css/SettingsView-3uycxa8z548m4.css +1 -0
- package/README.md +4 -0
- package/package.json +8 -8
- package/src/avatars/index.ts +5 -0
- package/src/components/AppBar.svelte +8 -1
- package/src/components/AppShell.svelte +17 -3
- package/src/components/Brand.svelte +15 -4
- package/src/components/DashboardView.svelte +5 -2
- package/src/components/IdeBar.svelte +9 -1
- package/src/components/InstanceCard.svelte +7 -3
- package/src/components/InstanceView.svelte +4 -1
- package/src/components/SettingsCog.svelte +9 -1
- package/src/components/SettingsView.svelte +476 -18
- package/src/components/TopBar.svelte +4 -1
- package/src/container-injections/claude-code-models.ts +74 -0
- package/src/container-injections/claude-model.ts +66 -0
- package/src/container-injections/claude-no-coauthor.ts +11 -1
- package/src/container-injections/claude-statusline.ts +24 -9
- package/src/container-injections/git-identity.ts +30 -3
- package/src/lib/injections.server.ts +5 -0
- package/src/lib/instances.server.ts +10 -1
- package/src/lib/popup-nav.ts +37 -0
- package/src/pages/App.svelte +10 -3
- package/src/pages/Settings.svelte +31 -0
- package/src/routes.ts +88 -2
- package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-faqf4hmc.js +0 -2
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-5g8meb6a.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-jsw4edxb.js +0 -23
- package/.mochi/svelte-client/chunk-04qv4w6w.js +0 -2
- package/.mochi/svelte-client/chunk-597d17x0.js +0 -1
- package/.mochi/svelte-client/chunk-5p31aqh0.js +0 -2
- package/.mochi/svelte-client/chunk-gjjhya9m.js +0 -2
- package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +0 -1
- 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(
|
|
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
|
-
|
|
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
|
|
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
|
|
41
|
-
if (!
|
|
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(
|
|
59
|
+
script = await readFile(expandTilde(token), 'utf8');
|
|
46
60
|
} catch {
|
|
47
|
-
|
|
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(
|
|
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
|
-
/**
|
|
15
|
-
|
|
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
|
-
? {
|
|
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
|
+
}
|
package/src/pages/App.svelte
CHANGED
|
@@ -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
|
-
|
|
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}
|