diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8abddd8..768c275 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,3 +44,9 @@ jobs: - name: Verify run: npm run verify + + - name: Verify standalone website + run: | + npm ci --prefix website + npm test --prefix website + npm run build --prefix website diff --git a/.gitignore b/.gitignore index a978653..e5cf955 100755 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,5 @@ tasks/ gajae-app-source.tar.gz gajae-app-sidebar.html electron/*.tar.gz +# Marketing website build output +/website/dist/ diff --git a/scripts/check-identity.mjs b/scripts/check-identity.mjs index 46835e4..363abe0 100644 --- a/scripts/check-identity.mjs +++ b/scripts/check-identity.mjs @@ -22,6 +22,7 @@ const MAX_FILE_BYTES = 64 * 1024 * 1024; const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024; const MAX_REPORTED_ERRORS = 100; const GENERATED_DIRECTORIES = ['dist', 'dist-server', 'release']; +const SKIPPED_RELATIVE_DIRECTORIES = new Set(['website/dist']); const SKIPPED_DIRECTORIES = new Set([ '.codegraph', '.desktop-build', @@ -258,6 +259,9 @@ async function walkDirectory(directoryPath, category) { if (category === 'source' && GENERATED_DIRECTORIES.includes(entry.name)) { continue; } + if (SKIPPED_RELATIVE_DIRECTORIES.has(relativePath)) { + continue; + } scanPath(relativePath); await walkDirectory(absolutePath, category); diff --git a/server/gjc-bun-sdk-adapter.ts b/server/gjc-bun-sdk-adapter.ts index bc5a3a1..b3030b5 100644 --- a/server/gjc-bun-sdk-adapter.ts +++ b/server/gjc-bun-sdk-adapter.ts @@ -28,6 +28,7 @@ export type SdkRunConfig = { credential: ExactCredentialRef; modelId: string; modelProfile?: string; + effort?: 'default' | 'inherit' | 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'; toolNames: string[]; spawns: string; bashPolicy: AppBashPolicy; @@ -84,6 +85,9 @@ function configFromOptions(value: Record): SdkRunConfig { || !exactCredentialRef(candidate.credential) || typeof candidate.modelId !== 'string' || !candidate.modelId || (candidate.modelProfile !== undefined && (typeof candidate.modelProfile !== 'string' || !candidate.modelProfile)) + || (candidate.effort !== undefined && ![ + 'default', 'inherit', 'off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', + ].includes(String(candidate.effort))) || !Array.isArray(candidate.toolNames) || candidate.toolNames.some((name) => typeof name !== 'string' || !name) || typeof candidate.spawns !== 'string' || !object(candidate.bashPolicy) || !Array.isArray(candidate.bashPolicy.allowedPrefixes) @@ -305,6 +309,11 @@ export class GjcBunSdkAdapter implements GjcWorkerRuntime { authStorage: this.authStorage, modelRegistry: this.modelRegistry, model, + ...( + config.effort && config.effort !== 'default' && config.effort !== 'inherit' + ? { thinkingLevel: config.effort } + : {} + ), providerSessionId: resumedId ?? sessionManager.getSessionId(), ...(resolvedCredential.credentialSelector ? { credentialSelector: resolvedCredential.credentialSelector } diff --git a/server/gjc-sdk-contract.bun.test.ts b/server/gjc-sdk-contract.bun.test.ts index b3dd07b..689bf1e 100644 --- a/server/gjc-sdk-contract.bun.test.ts +++ b/server/gjc-sdk-contract.bun.test.ts @@ -883,6 +883,20 @@ test('resume opens the sole exact session file and never re-emits session.create assert.equal(methods(f.frames).includes('session.created'), false); } finally { await f.close(); } }); +test('session effort is passed to the SDK as the turn thinking level', async () => { + const f = await fixture(); + try { + const run = f.host.handle(request('session.start', 'reasoning-effort', { + message: 'reason', + options: { ...f.options, effort: 'high' }, + })); + const session = await firstSession(f.sessions); + await session.promptStarted.promise; + session.complete(); + await run; + assert.equal(f.factoryOptions[0]!.thinkingLevel, 'high'); + } finally { await f.close(); } +}); test('sequential runs clone global settings for each cwd while retaining the session root', async () => { const f = await fixture(); const firstCwd = await mkdtemp(join(tmpdir(), 'gjc-cwd-one-')); diff --git a/server/gjc-worker-client.test.ts b/server/gjc-worker-client.test.ts index 432be8c..5de56b5 100644 --- a/server/gjc-worker-client.test.ts +++ b/server/gjc-worker-client.test.ts @@ -58,10 +58,11 @@ test('resume root resolution selects either allowlisted store from indexed sessi await Promise.all([writeFile(paths.live, '{}\n'), writeFile(paths.saved, '{}\n')]); const lookup = async (sessionId: string) => paths[sessionId as keyof typeof paths]; - // The resolver returns the canonical (realpath) root; macOS resolves the - // temp store under /var to /private/var, so normalize expectations too. + // The resolver returns the directory SessionManager must scan. macOS + // resolves the temp store under /var to /private/var, so normalize + // expectations too. assert.equal(await resolveGjcResumeSessionRoot('live', liveRoot, lookup), await realpath(liveRoot)); - assert.equal(await resolveGjcResumeSessionRoot('saved', liveRoot, lookup), await realpath(savedRoot)); + assert.equal(await resolveGjcResumeSessionRoot('saved', liveRoot, lookup), await realpath(savedDirectory)); } finally { await Promise.all([ rm(tempDirectory, { recursive: true, force: true }), diff --git a/server/gjc-worker-client.ts b/server/gjc-worker-client.ts index caf7e52..2c716a7 100644 --- a/server/gjc-worker-client.ts +++ b/server/gjc-worker-client.ts @@ -220,6 +220,7 @@ export async function resolveGjcResumeSessionRoot( ): Promise { try { const sessionPath = await realpath(await lookup(sessionId) ?? ''); + const sessionDirectory = dirname(sessionPath); const roots = [ join(homedir(), '.gjc', 'agent', 'sessions'), liveSessionRoot, @@ -227,7 +228,10 @@ export async function resolveGjcResumeSessionRoot( for (const root of roots) { try { const canonicalRoot = await realpath(root); - if (containedBy(canonicalRoot, sessionPath)) return canonicalRoot; + // SessionManager.list() scans only the supplied directory. Managed + // sessions live one level below the global sessions root, so returning + // that global root makes every historical resume look missing. + if (containedBy(canonicalRoot, sessionPath)) return sessionDirectory; } catch { // A missing or inaccessible allowlist root cannot contain a resumable session. } @@ -270,6 +274,7 @@ export async function enrichGjcSdkRunOptions(options: GjcWorkerOptions): Promise credential: options.credential ?? { kind: 'stored' }, modelId, ...(modelProfile ? { modelProfile } : {}), + effort: typeof options.effort === 'string' && options.effort ? options.effort : 'default', toolNames: options.toolNames ?? [...GJC_AGENT_TOOL_NAMES], spawns: options.spawns ?? '*', bashPolicy: options.bashPolicy ?? { allowedPrefixes: [] }, diff --git a/server/modules/providers/index.ts b/server/modules/providers/index.ts index 6d46a31..55d627e 100644 --- a/server/modules/providers/index.ts +++ b/server/modules/providers/index.ts @@ -1,4 +1,6 @@ export { sessionSynchronizerService } from './services/session-synchronizer.service.js'; +export { providerModelsService } from './services/provider-models.service.js'; + export { initializeSessionsWatcher } from './services/sessions-watcher.service.js'; export { closeSessionsWatcher } from './services/sessions-watcher.service.js'; diff --git a/server/modules/providers/list/gjc/gjc-builtin-model-profiles.ts b/server/modules/providers/list/gjc/gjc-builtin-model-profiles.ts index 483abdd..0ec49f6 100644 --- a/server/modules/providers/list/gjc/gjc-builtin-model-profiles.ts +++ b/server/modules/providers/list/gjc/gjc-builtin-model-profiles.ts @@ -8,372 +8,565 @@ export type GjcBuiltinModelProfile = { }; /** - * GJC 0.11.8 built-in model profiles. + * GJC 0.13.1 built-in model profiles. * * The upstream runtime module currently imports Bun-only utilities, so the * Node sidecar cannot load it directly. This catalog is generated from the - * exact @gajae-code/coding-agent dependency used by the app; `label` and - * `group` are app-authored display strings the SDK does not carry. + * exact @gajae-code/coding-agent dependency used by the app (regenerate by + * dumping BUILTIN_MODEL_PROFILES with dist-native/bun after an SDK bump); + * label and group are app-authored display strings the SDK does not carry. */ export const GJC_BUILTIN_MODEL_PROFILES: readonly GjcBuiltinModelProfile[] = [ { - "name": "codex-eco", - "label": "Codex Eco", - "group": "CODEX", - "roles": { - "default": "openai-codex/gpt-5.6-terra:low", - "executor": "openai-codex/gpt-5.6-luna:low", - "planner": "openai-codex/gpt-5.6-luna:high", - "critic": "openai-codex/gpt-5.6-terra:xhigh", - "architect": "openai-codex/gpt-5.6-terra:high" - } - }, - { - "name": "codex-medium", - "label": "Codex Medium", - "group": "CODEX", - "roles": { - "default": "openai-codex/gpt-5.6-sol:low", - "executor": "openai-codex/gpt-5.6-terra:low", - "planner": "openai-codex/gpt-5.6-terra:high", - "critic": "openai-codex/gpt-5.6-sol:xhigh", - "architect": "openai-codex/gpt-5.6-sol:high" - } - }, - { - "name": "codex-pro", - "label": "Codex Pro", - "group": "CODEX", - "roles": { - "default": "openai-codex/gpt-5.6-sol:medium", - "executor": "openai-codex/gpt-5.6-terra:medium", - "planner": "openai-codex/gpt-5.6-sol:high", - "critic": "openai-codex/gpt-5.6-sol:max", - "architect": "openai-codex/gpt-5.6-sol:xhigh" - } - }, - { - "name": "opencodego", - "label": "OpenCodeGo", - "group": "OPENCODEGO", - "roles": { - "default": "opencode-go/kimi-k2.6", - "executor": "opencode-go/deepseek-v4-flash", - "planner": "opencode-go/qwen3.7-max", - "critic": "opencode-go/mimo-v2.5-pro", - "architect": "opencode-go/deepseek-v4-pro" - } - }, - { - "name": "claude-opus", - "label": "Claude Opus", - "group": "CLAUDE", - "roles": { - "default": "anthropic/claude-opus-4-8:xhigh", - "executor": "anthropic/claude-sonnet-5", - "planner": "anthropic/claude-opus-4-8:low", - "critic": "anthropic/claude-opus-4-8:high", - "architect": "anthropic/claude-opus-4-8:xhigh" - } - }, - { - "name": "claude-fable", - "label": "Claude Fable", - "group": "CLAUDE", - "roles": { - "default": "anthropic/claude-fable-5:xhigh", - "executor": "anthropic/claude-sonnet-5", - "planner": "anthropic/claude-fable-5:low", - "critic": "anthropic/claude-fable-5:high", - "architect": "anthropic/claude-fable-5:xhigh" - } - }, - { - "name": "glm-eco", - "label": "GLM Eco", - "group": "GLM", - "roles": { - "default": "zai/glm-5.2:low", - "executor": "zai/glm-5.2:minimal", - "planner": "zai/glm-5.2:low", - "critic": "zai/glm-5.2:medium", - "architect": "zai/glm-5.2:high" - } - }, - { - "name": "glm-medium", - "label": "GLM Medium", - "group": "GLM", - "roles": { - "default": "zai/glm-5.2:medium", - "executor": "zai/glm-5.2:low", - "planner": "zai/glm-5.2:medium", - "critic": "zai/glm-5.2:high", - "architect": "zai/glm-5.2:xhigh" - } - }, - { - "name": "glm-pro", - "label": "GLM Pro", - "group": "GLM", - "roles": { - "default": "zai/glm-5.2:xhigh", - "executor": "zai/glm-5.2:medium", - "planner": "zai/glm-5.2:high", - "critic": "zai/glm-5.2:xhigh", - "architect": "zai/glm-5.2:xhigh" - } - }, - { - "name": "kimi-coding-plan-eco", - "label": "Kimi Coding Plan Eco", - "group": "KIMI CODING PLAN", - "roles": { - "default": "kimi-code/k3:low", - "executor": "kimi-code/k3:low", - "planner": "kimi-code/k3:low", - "critic": "kimi-code/k3:high", - "architect": "kimi-code/k3:high" - } - }, - { - "name": "kimi-coding-plan-medium", - "label": "Kimi Coding Plan Medium", - "group": "KIMI CODING PLAN", - "roles": { - "default": "kimi-code/k3:high", - "executor": "kimi-code/k3:low", - "planner": "kimi-code/k3:high", - "critic": "kimi-code/k3:high", - "architect": "kimi-code/k3:max" - } - }, - { - "name": "kimi-coding-plan-pro", - "label": "Kimi Coding Plan Pro", - "group": "KIMI CODING PLAN", - "roles": { - "default": "kimi-code/k3:max", - "executor": "kimi-code/k3:high", - "planner": "kimi-code/k3:high", - "critic": "kimi-code/k3:max", - "architect": "kimi-code/k3:max" - } - }, - { - "name": "mimo-eco", - "label": "Mimo Eco", - "group": "MIMO", - "roles": { - "default": "xiaomi/mimo-v2.5-pro:low", - "executor": "xiaomi/mimo-v2.5-pro:minimal", - "planner": "xiaomi/mimo-v2.5-pro:low", - "critic": "xiaomi/mimo-v2.5-pro:medium", - "architect": "xiaomi/mimo-v2.5-pro:high" - } - }, - { - "name": "mimo-medium", - "label": "Mimo Medium", - "group": "MIMO", - "roles": { - "default": "xiaomi/mimo-v2.5-pro:medium", - "executor": "xiaomi/mimo-v2.5-pro:low", - "planner": "xiaomi/mimo-v2.5-pro:medium", - "critic": "xiaomi/mimo-v2.5-pro:high", - "architect": "xiaomi/mimo-v2.5-pro:xhigh" - } - }, - { - "name": "mimo-pro", - "label": "Mimo Pro", - "group": "MIMO", - "roles": { - "default": "xiaomi/mimo-v2.5-pro:xhigh", - "executor": "xiaomi/mimo-v2.5-pro:medium", - "planner": "xiaomi/mimo-v2.5-pro:high", - "critic": "xiaomi/mimo-v2.5-pro:xhigh", - "architect": "xiaomi/mimo-v2.5-pro:xhigh" - } - }, - { - "name": "grok-eco", - "label": "Grok Eco", - "group": "GROK", - "roles": { - "default": "xai/grok-4.3:low", - "executor": "xai/grok-4.3:minimal", - "planner": "xai/grok-4.3:low", - "critic": "xai/grok-4.3:medium", - "architect": "xai/grok-4.3:high" - } - }, - { - "name": "grok-medium", - "label": "Grok Medium", - "group": "GROK", - "roles": { - "default": "xai/grok-4.3:medium", - "executor": "xai/grok-4.3:low", - "planner": "xai/grok-4.3:medium", - "critic": "xai/grok-4.3:high", - "architect": "xai/grok-4.3:xhigh" - } - }, - { - "name": "grok-pro", - "label": "Grok Pro", - "group": "GROK", - "roles": { - "default": "xai/grok-4.3:xhigh", - "executor": "xai/grok-4.3:medium", - "planner": "xai/grok-4.3:high", - "critic": "xai/grok-4.3:xhigh", - "architect": "xai/grok-4.3:xhigh" - } - }, - { - "name": "grok-build-pro", - "label": "Grok Build Pro", - "group": "GROK", - "roles": { - "default": "grok-build/grok-composer-2.5-fast", - "executor": "grok-build/grok-build", - "planner": "grok-build/grok-composer-2.5-fast", - "critic": "grok-build/grok-composer-2.5-fast", - "architect": "grok-build/grok-build" - } - }, - { - "name": "cursor-eco", - "label": "Cursor Eco", - "group": "CURSOR", - "roles": { - "default": "cursor/composer-1.5:low", - "executor": "cursor/composer-1.5:minimal", - "planner": "cursor/composer-1.5:low", - "critic": "cursor/composer-1.5:medium", - "architect": "cursor/composer-1.5:high" - } - }, - { - "name": "cursor-medium", - "label": "Cursor Medium", - "group": "CURSOR", - "roles": { - "default": "cursor/composer-1.5:medium", - "executor": "cursor/composer-1.5:low", - "planner": "cursor/composer-1.5:medium", - "critic": "cursor/composer-1.5:high", - "architect": "cursor/composer-1.5:xhigh" - } - }, - { - "name": "cursor-pro", - "label": "Cursor Pro", - "group": "CURSOR", - "roles": { - "default": "cursor/composer-1.5:xhigh", - "executor": "cursor/composer-1.5:medium", - "planner": "cursor/composer-1.5:high", - "critic": "cursor/composer-1.5:xhigh", - "architect": "cursor/composer-1.5:xhigh" - } - }, - { - "name": "minimax-eco", - "label": "MiniMax Eco", - "group": "MINIMAX", - "roles": { - "default": "minimax-code/minimax-m3:low", - "executor": "minimax-code/minimax-m3:minimal", - "planner": "minimax-code/minimax-m3:low", - "critic": "minimax-code/minimax-m3:medium", - "architect": "minimax-code/minimax-m3:high" - } - }, - { - "name": "minimax-medium", - "label": "MiniMax Medium", - "group": "MINIMAX", - "roles": { - "default": "minimax-code/minimax-m3:medium", - "executor": "minimax-code/minimax-m3:low", - "planner": "minimax-code/minimax-m3:medium", - "critic": "minimax-code/minimax-m3:high", - "architect": "minimax-code/minimax-m3:xhigh" - } - }, - { - "name": "minimax-pro", - "label": "MiniMax Pro", - "group": "MINIMAX", - "roles": { - "default": "minimax-code/minimax-m3:xhigh", - "executor": "minimax-code/minimax-m3:medium", - "planner": "minimax-code/minimax-m3:high", - "critic": "minimax-code/minimax-m3:xhigh", - "architect": "minimax-code/minimax-m3:xhigh" - } - }, - { - "name": "alibaba-token-plan-balanced", - "label": "Alibaba Token Plan Balanced", - "group": "ALIBABA TOKEN PLAN", - "roles": { - "default": "alibaba-token-plan/qwen3.8-max-preview:medium", - "executor": "alibaba-token-plan/deepseek-v4-pro:xhigh", - "planner": "alibaba-token-plan/glm-5.2:high", - "critic": "alibaba-token-plan/glm-5.2:high", - "architect": "alibaba-token-plan/qwen3.8-max-preview:xhigh" - } - }, - { - "name": "alibaba-token-plan-qwenmaxxing", - "label": "Alibaba Token Plan Qwenmaxxing", - "group": "ALIBABA TOKEN PLAN", - "roles": { - "default": "alibaba-token-plan/qwen3.8-max-preview:medium", - "executor": "alibaba-token-plan/qwen3.8-max-preview:low", - "planner": "alibaba-token-plan/qwen3.8-max-preview:medium", - "critic": "alibaba-token-plan/qwen3.8-max-preview:xhigh", - "architect": "alibaba-token-plan/qwen3.8-max-preview:xhigh" - } - }, - { - "name": "opus-codex", - "label": "Opus + Codex", - "group": "COMBOS", - "roles": { - "default": "anthropic/claude-opus-4-8:xhigh", - "executor": "openai-codex/gpt-5.6-terra:low", - "planner": "anthropic/claude-sonnet-5", - "critic": "openai-codex/gpt-5.6-sol:xhigh", - "architect": "openai-codex/gpt-5.6-sol:high" - } - }, - { - "name": "codex-opencodego", - "label": "Codex + OpenCodeGo", - "group": "COMBOS", - "roles": { - "default": "openai-codex/gpt-5.6-sol:low", - "executor": "opencode-go/deepseek-v4-pro", - "planner": "opencode-go/kimi-k2.6", - "critic": "opencode-go/mimo-v2.5-pro", - "architect": "openai-codex/gpt-5.6-sol:high" - } - }, - { - "name": "fable-opus-codex", - "label": "Fable + Opus + Codex", - "group": "COMBOS", - "roles": { - "default": "anthropic/claude-fable-5:high", - "executor": "openai-codex/gpt-5.6-terra:medium", - "planner": "anthropic/claude-opus-4-8:medium", - "critic": "anthropic/claude-opus-4-8:high", - "architect": "openai-codex/gpt-5.6-sol:xhigh" - } + "name": "codex-eco", + "label": "Codex Eco", + "group": "CODEX", + "roles": { + "default": "openai-codex/gpt-5.6-terra:low", + "planner": "openai-codex/gpt-5.6-luna:high", + "executor": "openai-codex/gpt-5.6-luna:low", + "architect": "openai-codex/gpt-5.6-terra:high", + "critic": "openai-codex/gpt-5.6-terra:xhigh" + } + }, + { + "name": "codex-medium", + "label": "Codex Medium", + "group": "CODEX", + "roles": { + "default": "openai-codex/gpt-5.6-sol:low", + "planner": "openai-codex/gpt-5.6-terra:high", + "executor": "openai-codex/gpt-5.6-terra:low", + "architect": "openai-codex/gpt-5.6-sol:high", + "critic": "openai-codex/gpt-5.6-sol:xhigh" + } + }, + { + "name": "codex-pro", + "label": "Codex Pro", + "group": "CODEX", + "roles": { + "default": "openai-codex/gpt-5.6-sol:medium", + "planner": "openai-codex/gpt-5.6-sol:high", + "executor": "openai-codex/gpt-5.6-terra:medium", + "architect": "openai-codex/gpt-5.6-sol:xhigh", + "critic": "openai-codex/gpt-5.6-sol:max" + } + }, + { + "name": "lunamaxxing", + "label": "Lunamaxxing", + "group": "CODEX", + "roles": { + "default": "openai-codex/gpt-5.6-luna:medium", + "planner": "openai-codex/gpt-5.6-luna:max", + "executor": "openai-codex/gpt-5.6-luna:xhigh", + "architect": "openai-codex/gpt-5.6-luna:max", + "critic": "openai-codex/gpt-5.6-luna:max" + } + }, + { + "name": "opencodego", + "label": "OpenCodeGo", + "group": "OPENCODEGO", + "roles": { + "default": "opencode-go/kimi-k3", + "planner": "opencode-go/kimi-k3", + "executor": "opencode-go/deepseek-v4-flash", + "architect": "opencode-go/deepseek-v4-pro", + "critic": "opencode-go/mimo-v2.5-pro" + } + }, + { + "name": "open-weights-glm", + "label": "Open Weights GLM", + "group": "OPEN WEIGHTS", + "roles": { + "default": "glm-5.2:medium", + "planner": "glm-5.2:high", + "executor": "glm-5.2:low", + "architect": "glm-5.2:xhigh", + "critic": "glm-5.2:high" + } + }, + { + "name": "open-weights-deepseek", + "label": "Open Weights DeepSeek", + "group": "OPEN WEIGHTS", + "roles": { + "default": "deepseek-v4-flash:high", + "planner": "deepseek-v4-flash:high", + "executor": "deepseek-v4-flash:medium", + "architect": "deepseek-v4-flash:xhigh", + "critic": "deepseek-v4-flash:xhigh" + } + }, + { + "name": "open-weights-kimi", + "label": "Open Weights Kimi", + "group": "OPEN WEIGHTS", + "roles": { + "default": "kimi-k3:high", + "planner": "kimi-k3:xhigh", + "executor": "kimi-k3:high", + "architect": "kimi-k3:xhigh", + "critic": "kimi-k3:high" + } + }, + { + "name": "open-weights-luna", + "label": "Open Weights Luna", + "group": "OPEN WEIGHTS", + "roles": { + "default": "gpt-5.6-luna:high", + "planner": "gpt-5.6-luna:xhigh", + "executor": "gpt-5.6-luna:high", + "architect": "gpt-5.6-luna:xhigh", + "critic": "gpt-5.6-luna:xhigh" + } + }, + { + "name": "open-weights-glm-deepseek", + "label": "Open Weights GLM + DeepSeek", + "group": "OPEN WEIGHTS", + "roles": { + "default": "glm-5.2:medium", + "planner": "glm-5.2:high", + "executor": "deepseek-v4-flash:high", + "architect": "glm-5.2:xhigh", + "critic": "deepseek-v4-flash:xhigh" + } + }, + { + "name": "open-weights-kimi-deepseek", + "label": "Open Weights Kimi + DeepSeek", + "group": "OPEN WEIGHTS", + "roles": { + "default": "kimi-k3:high", + "planner": "kimi-k3:xhigh", + "executor": "deepseek-v4-flash:high", + "architect": "kimi-k3:xhigh", + "critic": "deepseek-v4-flash:xhigh" + } + }, + { + "name": "open-weights-kimi-glm", + "label": "Open Weights Kimi + GLM", + "group": "OPEN WEIGHTS", + "roles": { + "default": "glm-5.2:high", + "planner": "kimi-k3:high", + "executor": "glm-5.2:high", + "architect": "kimi-k3:xhigh", + "critic": "glm-5.2:xhigh" + } + }, + { + "name": "open-weights-kimi-glm-deepseek", + "label": "Open Weights Kimi + GLM + DeepSeek", + "group": "OPEN WEIGHTS", + "roles": { + "default": "glm-5.2:medium", + "planner": "kimi-k3:high", + "executor": "deepseek-v4-flash:high", + "architect": "kimi-k3:xhigh", + "critic": "glm-5.2:high" + } + }, + { + "name": "open-weights-all", + "label": "Open Weights All", + "group": "OPEN WEIGHTS", + "roles": { + "default": "gpt-5.6-luna:high", + "planner": "kimi-k3:high", + "executor": "deepseek-v4-flash:high", + "architect": "gpt-5.6-luna:xhigh", + "critic": "glm-5.2:high" + } + }, + { + "name": "claude-opus", + "label": "Claude Opus", + "group": "CLAUDE", + "roles": { + "default": "anthropic/claude-opus-5:xhigh", + "planner": "anthropic/claude-opus-5:low", + "executor": "anthropic/claude-sonnet-5", + "architect": "anthropic/claude-opus-5:xhigh", + "critic": "anthropic/claude-opus-5:high" + } + }, + { + "name": "claude-fable", + "label": "Claude Fable", + "group": "CLAUDE", + "roles": { + "default": "anthropic/claude-fable-5:xhigh", + "planner": "anthropic/claude-fable-5:low", + "executor": "anthropic/claude-sonnet-5", + "architect": "anthropic/claude-fable-5:xhigh", + "critic": "anthropic/claude-fable-5:high" + } + }, + { + "name": "glm-eco", + "label": "GLM Eco", + "group": "GLM", + "roles": { + "default": "zai/glm-5.2:low", + "planner": "zai/glm-5.2:low", + "executor": "zai/glm-5.2:minimal", + "architect": "zai/glm-5.2:high", + "critic": "zai/glm-5.2:medium" + } + }, + { + "name": "glm-medium", + "label": "GLM Medium", + "group": "GLM", + "roles": { + "default": "zai/glm-5.2:medium", + "planner": "zai/glm-5.2:medium", + "executor": "zai/glm-5.2:low", + "architect": "zai/glm-5.2:xhigh", + "critic": "zai/glm-5.2:high" + } + }, + { + "name": "glm-pro", + "label": "GLM Pro", + "group": "GLM", + "roles": { + "default": "zai/glm-5.2:xhigh", + "planner": "zai/glm-5.2:high", + "executor": "zai/glm-5.2:medium", + "architect": "zai/glm-5.2:xhigh", + "critic": "zai/glm-5.2:xhigh" + } + }, + { + "name": "kimi-coding-plan-eco", + "label": "Kimi Coding Plan Eco", + "group": "KIMI CODING PLAN", + "roles": { + "default": "kimi-code/k3:low", + "planner": "kimi-code/k3:low", + "executor": "kimi-code/k3:low", + "architect": "kimi-code/k3:high", + "critic": "kimi-code/k3:high" + } + }, + { + "name": "kimi-coding-plan-medium", + "label": "Kimi Coding Plan Medium", + "group": "KIMI CODING PLAN", + "roles": { + "default": "kimi-code/k3:high", + "planner": "kimi-code/k3:high", + "executor": "kimi-code/k3:low", + "architect": "kimi-code/k3:max", + "critic": "kimi-code/k3:high" + } + }, + { + "name": "kimi-coding-plan-pro", + "label": "Kimi Coding Plan Pro", + "group": "KIMI CODING PLAN", + "roles": { + "default": "kimi-code/k3:max", + "planner": "kimi-code/k3:high", + "executor": "kimi-code/k3:high", + "architect": "kimi-code/k3:max", + "critic": "kimi-code/k3:max" + } + }, + { + "name": "mimo-eco", + "label": "Mimo Eco", + "group": "MIMO", + "roles": { + "default": "xiaomi/mimo-v2.5-pro:low", + "planner": "xiaomi/mimo-v2.5-pro:low", + "executor": "xiaomi/mimo-v2.5-pro:minimal", + "architect": "xiaomi/mimo-v2.5-pro:high", + "critic": "xiaomi/mimo-v2.5-pro:medium" + } + }, + { + "name": "mimo-medium", + "label": "Mimo Medium", + "group": "MIMO", + "roles": { + "default": "xiaomi/mimo-v2.5-pro:medium", + "planner": "xiaomi/mimo-v2.5-pro:medium", + "executor": "xiaomi/mimo-v2.5-pro:low", + "architect": "xiaomi/mimo-v2.5-pro:xhigh", + "critic": "xiaomi/mimo-v2.5-pro:high" + } + }, + { + "name": "mimo-pro", + "label": "Mimo Pro", + "group": "MIMO", + "roles": { + "default": "xiaomi/mimo-v2.5-pro:xhigh", + "planner": "xiaomi/mimo-v2.5-pro:high", + "executor": "xiaomi/mimo-v2.5-pro:medium", + "architect": "xiaomi/mimo-v2.5-pro:xhigh", + "critic": "xiaomi/mimo-v2.5-pro:xhigh" + } + }, + { + "name": "grok-eco", + "label": "Grok Eco", + "group": "GROK", + "roles": { + "default": "xai/grok-4.3:low", + "planner": "xai/grok-4.3:low", + "executor": "xai/grok-4.3:minimal", + "architect": "xai/grok-4.3:high", + "critic": "xai/grok-4.3:medium" + } + }, + { + "name": "grok-medium", + "label": "Grok Medium", + "group": "GROK", + "roles": { + "default": "xai/grok-4.3:medium", + "planner": "xai/grok-4.3:medium", + "executor": "xai/grok-4.3:low", + "architect": "xai/grok-4.3:xhigh", + "critic": "xai/grok-4.3:high" + } + }, + { + "name": "grok-pro", + "label": "Grok Pro", + "group": "GROK", + "roles": { + "default": "xai/grok-4.3:xhigh", + "planner": "xai/grok-4.3:high", + "executor": "xai/grok-4.3:medium", + "architect": "xai/grok-4.3:xhigh", + "critic": "xai/grok-4.3:xhigh" + } + }, + { + "name": "grok-45-eco", + "label": "Grok 4.5 Eco", + "group": "GROK", + "roles": { + "default": "xai/grok-4.5:low", + "planner": "xai/grok-4.5:low", + "executor": "xai/grok-4.5:minimal", + "architect": "xai/grok-4.5:high", + "critic": "xai/grok-4.5:medium" + } + }, + { + "name": "grok-45-medium", + "label": "Grok 4.5 Medium", + "group": "GROK", + "roles": { + "default": "xai/grok-4.5:medium", + "planner": "xai/grok-4.5:medium", + "executor": "xai/grok-4.5:low", + "architect": "xai/grok-4.5:high", + "critic": "xai/grok-4.5:high" + } + }, + { + "name": "grok-45-pro", + "label": "Grok 4.5 Pro", + "group": "GROK", + "roles": { + "default": "xai/grok-4.5:high", + "planner": "xai/grok-4.5:high", + "executor": "xai/grok-4.5:medium", + "architect": "xai/grok-4.5:high", + "critic": "xai/grok-4.5:high" + } + }, + { + "name": "grok-build-pro", + "label": "Grok Build Pro", + "group": "GROK", + "roles": { + "default": "grok-build/grok-composer-2.5-fast", + "planner": "grok-build/grok-composer-2.5-fast", + "executor": "grok-build/grok-build", + "architect": "grok-build/grok-build", + "critic": "grok-build/grok-composer-2.5-fast" + } + }, + { + "name": "cursor-eco", + "label": "Cursor Eco", + "group": "CURSOR", + "roles": { + "default": "cursor/composer-2.5", + "planner": "cursor/composer-2.5", + "executor": "cursor/composer-2.5", + "architect": "cursor/composer-2.5", + "critic": "cursor/composer-2.5" + } + }, + { + "name": "cursor-medium", + "label": "Cursor Medium", + "group": "CURSOR", + "roles": { + "default": "cursor/composer-2.5", + "planner": "cursor/composer-2.5", + "executor": "cursor/composer-2.5-fast", + "architect": "cursor/composer-2.5-fast", + "critic": "cursor/composer-2.5-fast" + } + }, + { + "name": "cursor-pro", + "label": "Cursor Pro", + "group": "CURSOR", + "roles": { + "default": "cursor/composer-2.5-fast", + "planner": "cursor/composer-2.5-fast", + "executor": "cursor/composer-2.5-fast", + "architect": "cursor/composer-2.5-fast", + "critic": "cursor/composer-2.5-fast" + } + }, + { + "name": "minimax-eco", + "label": "MiniMax Eco", + "group": "MINIMAX", + "roles": { + "default": "minimax-code/MiniMax-M3:low", + "planner": "minimax-code/MiniMax-M3:low", + "executor": "minimax-code/MiniMax-M3:minimal", + "architect": "minimax-code/MiniMax-M3:high", + "critic": "minimax-code/MiniMax-M3:medium" + } + }, + { + "name": "minimax-medium", + "label": "MiniMax Medium", + "group": "MINIMAX", + "roles": { + "default": "minimax-code/MiniMax-M3:medium", + "planner": "minimax-code/MiniMax-M3:medium", + "executor": "minimax-code/MiniMax-M3:low", + "architect": "minimax-code/MiniMax-M3:xhigh", + "critic": "minimax-code/MiniMax-M3:high" + } + }, + { + "name": "minimax-pro", + "label": "MiniMax Pro", + "group": "MINIMAX", + "roles": { + "default": "minimax-code/MiniMax-M3:xhigh", + "planner": "minimax-code/MiniMax-M3:high", + "executor": "minimax-code/MiniMax-M3:medium", + "architect": "minimax-code/MiniMax-M3:xhigh", + "critic": "minimax-code/MiniMax-M3:xhigh" + } + }, + { + "name": "alibaba-token-plan-balanced", + "label": "Alibaba Token Plan Balanced", + "group": "ALIBABA TOKEN PLAN", + "roles": { + "default": "alibaba-token-plan/qwen3.8-max-preview:medium", + "planner": "alibaba-token-plan/glm-5.2:high", + "executor": "alibaba-token-plan/deepseek-v4-pro:xhigh", + "architect": "alibaba-token-plan/qwen3.8-max-preview:xhigh", + "critic": "alibaba-token-plan/glm-5.2:high" + } + }, + { + "name": "alibaba-token-plan-pro", + "label": "Alibaba Token Plan Pro", + "group": "ALIBABA TOKEN PLAN", + "roles": { + "default": "alibaba-token-plan/qwen3.8-max-preview:medium", + "planner": "alibaba-token-plan/glm-5.2:high", + "executor": "alibaba-token-plan/deepseek-v4-flash-0731:max", + "architect": "alibaba-token-plan/qwen3.8-max-preview:xhigh", + "critic": "alibaba-token-plan/glm-5.2:xhigh" + } + }, + { + "name": "alibaba-token-plan-qwenmaxxing", + "label": "Alibaba Token Plan Qwenmaxxing", + "group": "ALIBABA TOKEN PLAN", + "roles": { + "default": "alibaba-token-plan/qwen3.8-max-preview:medium", + "planner": "alibaba-token-plan/qwen3.8-max-preview:medium", + "executor": "alibaba-token-plan/qwen3.8-max-preview:low", + "architect": "alibaba-token-plan/qwen3.8-max-preview:xhigh", + "critic": "alibaba-token-plan/qwen3.8-max-preview:xhigh" + } + }, + { + "name": "alibaba-token-plan-qwen-deepseek", + "label": "Alibaba Token Plan Qwen + DeepSeek", + "group": "ALIBABA TOKEN PLAN", + "roles": { + "default": "alibaba-token-plan/qwen3.8-max:high", + "planner": "alibaba-token-plan/deepseek-v4-flash-0731:max", + "executor": "alibaba-token-plan/deepseek-v4-flash-0731:high", + "architect": "alibaba-token-plan/qwen3.8-max:xhigh", + "critic": "alibaba-token-plan/qwen3.8-max:xhigh" + } + }, + { + "name": "alibaba-token-plan-glm-deepseek", + "label": "Alibaba Token Plan GLM + DeepSeek", + "group": "ALIBABA TOKEN PLAN", + "roles": { + "default": "alibaba-token-plan/glm-5.2:high", + "planner": "alibaba-token-plan/deepseek-v4-flash-0731:max", + "executor": "alibaba-token-plan/deepseek-v4-flash-0731:high", + "architect": "alibaba-token-plan/glm-5.2:xhigh", + "critic": "alibaba-token-plan/glm-5.2:xhigh" + } + }, + { + "name": "opus-codex", + "label": "Opus + Codex", + "group": "COMBOS", + "roles": { + "default": "anthropic/claude-opus-5:xhigh", + "planner": "anthropic/claude-sonnet-5", + "executor": "openai-codex/gpt-5.6-terra:low", + "architect": "openai-codex/gpt-5.6-sol:high", + "critic": "openai-codex/gpt-5.6-sol:xhigh" + } + }, + { + "name": "codex-opencodego", + "label": "Codex + OpenCodeGo", + "group": "COMBOS", + "roles": { + "default": "openai-codex/gpt-5.6-sol:low", + "planner": "opencode-go/kimi-k3", + "executor": "opencode-go/deepseek-v4-pro", + "architect": "openai-codex/gpt-5.6-sol:high", + "critic": "opencode-go/mimo-v2.5-pro" + } + }, + { + "name": "fable-opus-codex", + "label": "Fable + Opus + Codex", + "group": "COMBOS", + "roles": { + "default": "anthropic/claude-fable-5:high", + "planner": "anthropic/claude-opus-5:medium", + "executor": "openai-codex/gpt-5.6-terra:medium", + "architect": "openai-codex/gpt-5.6-sol:xhigh", + "critic": "anthropic/claude-opus-5:high" + } } ]; diff --git a/server/modules/providers/list/gjc/gjc-models.provider.ts b/server/modules/providers/list/gjc/gjc-models.provider.ts index 997de63..5a1c315 100644 --- a/server/modules/providers/list/gjc/gjc-models.provider.ts +++ b/server/modules/providers/list/gjc/gjc-models.provider.ts @@ -84,6 +84,28 @@ function parseProfiles(source: string): Array<{ name: string; label: string; rol return profiles.filter((profile) => Object.keys(profile.roles).length > 0); } +/** + * `config.yml` roles may reference a profile name (e.g. `modelProfile: + * default: fable-opus-codex`) instead of a `provider/model` selector. Expand + * such references through the profile map so the "Current" option surfaces + * real model ids; keep direct selectors as explicit overrides and drop values + * that are neither. + */ +function resolveConfiguredRoles( + configured: RoleMap, + profiles: Map, +): RoleMap { + const resolved: RoleMap = {}; + const referenced = configured.default && !configured.default.includes('/') + ? profiles.get(configured.default) + : undefined; + if (referenced) Object.assign(resolved, referenced.roles); + for (const [role, value] of Object.entries(configured)) { + if (value.includes('/')) resolved[role as ProfileRole] = value; + } + return resolved; +} + async function getGjcPresetCatalog(homeDir: string): Promise { const agentDir = path.join(homeDir, '.gjc', 'agent'); const [configSource, modelsSource] = await Promise.all([ @@ -114,7 +136,7 @@ async function getGjcPresetCatalog(homeDir: string): Promise ({ value: `profile:${profile.name}`, diff --git a/server/modules/providers/services/provider-models.service.ts b/server/modules/providers/services/provider-models.service.ts index 587274f..a411273 100644 --- a/server/modules/providers/services/provider-models.service.ts +++ b/server/modules/providers/services/provider-models.service.ts @@ -19,7 +19,7 @@ export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000; // Bumped whenever the built-in preset catalog changes or the option shape gains // a field, so an SDK upgrade or a new field is not hidden behind an existing // cache entry for the rest of its 3-day TTL. -const PROVIDER_MODELS_CACHE_VERSION = 6; +const PROVIDER_MODELS_CACHE_VERSION = 7; type ProviderModelsServiceDependencies = { resolveProvider?: (provider: LLMProvider) => Pick; diff --git a/server/modules/providers/tests/gjc-models.provider.test.ts b/server/modules/providers/tests/gjc-models.provider.test.ts index 48df9c3..fa948cd 100644 --- a/server/modules/providers/tests/gjc-models.provider.test.ts +++ b/server/modules/providers/tests/gjc-models.provider.test.ts @@ -34,6 +34,55 @@ test('GJC model catalog merges built-in and custom profiles with custom override assert.equal(catalog.OPTIONS.filter((option) => option.value === 'profile:codex-medium').length, 1); }); +test('a profile-name reference in config.yml resolves Current to real model selectors', async (t) => { + const homeDir = await mkdtemp(path.join(os.tmpdir(), 'gajae-model-config-ref-')); + t.after(() => rm(homeDir, { recursive: true, force: true })); + const agentDir = path.join(homeDir, '.gjc', 'agent'); + await mkdir(agentDir, { recursive: true }); + await writeFile(path.join(agentDir, 'models.yml'), `profiles: + personal: + display_name: Personal + model_mapping: + default: custom/daily-driver + planner: custom/planner +`, 'utf8'); + await writeFile(path.join(agentDir, 'config.yml'), `modelProfile: + default: personal +configSchemaVersion: 1 +`, 'utf8'); + + const catalog = await new GjcProviderModels(homeDir).getSupportedModels(); + const current = catalog.OPTIONS.find((option) => option.value === 'default'); + + // The profile name itself must never surface as a "model". + assert.equal(current?.roles?.default, 'custom/daily-driver'); + assert.equal(current?.roles?.planner, 'custom/planner'); +}); + +test('a direct selector in config.yml overrides the referenced profile role', async (t) => { + const homeDir = await mkdtemp(path.join(os.tmpdir(), 'gajae-model-config-mix-')); + t.after(() => rm(homeDir, { recursive: true, force: true })); + const agentDir = path.join(homeDir, '.gjc', 'agent'); + await mkdir(agentDir, { recursive: true }); + await writeFile(path.join(agentDir, 'models.yml'), `profiles: + personal: + display_name: Personal + model_mapping: + default: custom/daily-driver + critic: custom/critic +`, 'utf8'); + await writeFile(path.join(agentDir, 'config.yml'), `modelProfile: + default: personal + critic: custom/override-critic +`, 'utf8'); + + const catalog = await new GjcProviderModels(homeDir).getSupportedModels(); + const current = catalog.OPTIONS.find((option) => option.value === 'default'); + + assert.equal(current?.roles?.default, 'custom/daily-driver'); + assert.equal(current?.roles?.critic, 'custom/override-critic'); +}); + test('every catalog option carries a group so clients can collapse the preset list', async (t) => { const homeDir = await mkdtemp(path.join(os.tmpdir(), 'gajae-model-groups-')); t.after(() => rm(homeDir, { recursive: true, force: true })); diff --git a/server/modules/websocket/services/chat-websocket.service.ts b/server/modules/websocket/services/chat-websocket.service.ts index 871770a..5401c61 100644 --- a/server/modules/websocket/services/chat-websocket.service.ts +++ b/server/modules/websocket/services/chat-websocket.service.ts @@ -131,10 +131,30 @@ type ChatWebSocketDependencies = { ) => void; /** Provider-runtime approvals included in `chat_subscribed` after reconnect. */ getPendingApprovalsForSession: (providerSessionId: string) => unknown[]; + /** + * Per-session model resolution (injectable for tests). The default consults + * the persisted active-model change store so a model picked for a session + * survives page reloads and session switches instead of depending on the + * client's global localStorage value. + */ + resolveSessionModel?: ( + provider: LLMProvider, + sessionId: string, + requestedModel?: string | null, + ) => Promise; gjcProjection?: GjcJobProjectionService; oauthSupervisor?: OAuthSupervisor; }; +async function defaultResolveSessionModel( + provider: LLMProvider, + sessionId: string, + requestedModel?: string | null, +): Promise { + const { providerModelsService } = await import('@/modules/providers/index.js'); + return providerModelsService.resolveResumeModel(provider, sessionId, requestedModel); +} + /** * Extracts the authenticated request user id in the formats currently produced * by platform and OSS auth code paths. @@ -257,12 +277,28 @@ async function handleChatSend( const clientOptions = (data.options ?? {}) as AnyRecord; + // The session's persisted model choice outranks the client's global default: + // the active-model POST stores per-session picks under the app session id, + // and this is the only place runs are dispatched. + const requestedModel = typeof clientOptions.model === 'string' ? clientOptions.model : null; + let resolvedModel: string | undefined; + try { + resolvedModel = await (dependencies.resolveSessionModel ?? defaultResolveSessionModel)( + provider, + sessionId, + requestedModel, + ); + } catch { + resolvedModel = requestedModel ?? undefined; + } + // The provider runtimes receive the provider-native session id (that is the // id their CLI/SDK understands for resume). Brand-new sessions have no // provider id yet, so the runtime starts fresh and announces one, which the // gateway writer captures and maps back to the app session id. const runtimeOptions: AnyRecord = { ...clientOptions, + ...(resolvedModel ? { model: resolvedModel } : {}), // Image attachments are re-validated server-side: only files inside the // global upload store may reach the provider runtimes' file reads. images: filterImagesToUploadStore(clientOptions.images), diff --git a/server/modules/websocket/tests/chat-websocket.service.test.ts b/server/modules/websocket/tests/chat-websocket.service.test.ts index 4b1bf77..68ee2ea 100644 --- a/server/modules/websocket/tests/chat-websocket.service.test.ts +++ b/server/modules/websocket/tests/chat-websocket.service.test.ts @@ -71,6 +71,86 @@ async function withIsolatedDatabase(runTest: () => void | Promise): Promis } } +test('chat.send prefers the session\'s persisted model choice over the client\'s global default', async () => { + await withIsolatedDatabase(async () => { + sessionsDb.createAppSession('model-override-session', 'gjc', '/workspace/model-project'); + let receivedOptions: Record | undefined; + let resolverArgs: unknown[] = []; + + const server = new WebSocketServer({ host: '127.0.0.1', port: 0 }); + try { + await once(server, 'listening'); + server.on('connection', (socket, request) => { + handleChatConnection( + socket, + Object.assign(request, { user: { id: 'test-user' } }), + { + spawnFns: { + gjc: (_command, options, writer) => { + receivedOptions = options; + (writer as { sendComplete(options: { exitCode: number }): void }).sendComplete({ exitCode: 0 }); + return Promise.resolve(); + }, + }, + abortFns: { gjc: async () => false }, + resolveToolApproval() {}, + getPendingApprovalsForSession: () => [], + resolveSessionModel: async (...args: unknown[]) => { + resolverArgs = args; + return 'anthropic/claude-opus-5'; + }, + }, + ); + }); + + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Expected the websocket test server to bind a TCP port.'); + } + + const client = new WebSocket(`ws://127.0.0.1:${address.port}`); + try { + await once(client, 'open'); + const completed = new Promise((resolve, reject) => { + client.on('message', (raw) => { + try { + if (parseOutboundFrame(String(raw)).kind === 'complete') resolve(); + } catch (error) { + reject(error); + } + }); + }); + + client.send(JSON.stringify({ + type: 'chat.send', + sessionId: 'model-override-session', + content: 'use my session model', + options: { model: 'default' }, + })); + await completed; + + assert.deepEqual(resolverArgs, ['gjc', 'model-override-session', 'default']); + assert.equal(receivedOptions?.model, 'anthropic/claude-opus-5'); + } finally { + client.terminate(); + } + } finally { + for (const client of server.clients) { + client.terminate(); + } + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } + }); +}); + test('chat.send dispatches a non-Git GJC session directly in its persisted project directory', async () => { await withIsolatedDatabase(async () => { sessionsDb.createAppSession('non-git-session', 'gjc', '/workspace/non-git-project'); diff --git a/src/components/chat/OAuthLoginDialog.tsx b/src/components/chat/OAuthLoginDialog.tsx index 6dc7bc6..8f24dcc 100644 --- a/src/components/chat/OAuthLoginDialog.tsx +++ b/src/components/chat/OAuthLoginDialog.tsx @@ -99,6 +99,7 @@ function OAuthLoginDialog({ const isBusy = isStarting || Boolean(attempt && !isTerminal && attempt.phase !== 'awaiting_browser' && attempt.phase !== 'awaiting_input'); const authorizationUrl = safeOAuthAuthorizationUrl(attempt?.authorizationUrl); const passwordInput = attempt?.password === true || attempt?.valueKind === 'password'; + const manualCodeInput = attempt?.valueKind === 'manual_code'; const showAuthorizationLink = shouldDisplayOAuthAuthorizationLink(attempt); const showProviderPicker = !attempt || (isTerminal && attempt.phase !== 'completed'); @@ -254,7 +255,11 @@ function OAuthLoginDialog({ {attempt.instruction || phaseLabel(attempt.phase)} + {iconOnly ? ( + + ) : ( + + )} {open && createPortal(
void; +}; + +const OPTIONS: Array<{ value: ReasoningEffort; label: string }> = [ + { value: 'default', label: 'Default' }, + { value: 'off', label: 'Off' }, + { value: 'minimal', label: 'Minimal' }, + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, + { value: 'xhigh', label: 'Extra high' }, + { value: 'max', label: 'Max' }, +]; + +export default function ReasoningEffortPicker({ value, onSelect }: ReasoningEffortPickerProps) { + const [open, setOpen] = useState(false); + const rootRef = useRef(null); + const popupRef = useRef(null); + const [popupPosition, setPopupPosition] = useState({ bottom: 0, left: 0 }); + const selected = OPTIONS.find((option) => option.value === value) ?? OPTIONS[0]; + + // The composer form clips its children (overflow-hidden rounded corners), so + // the popup must escape through a body portal with fixed positioning. + useEffect(() => { + if (!open) return; + const rect = rootRef.current?.getBoundingClientRect(); + if (rect) { + setPopupPosition({ + bottom: window.innerHeight - rect.top + 8, + left: Math.max(8, Math.min(rect.left, window.innerWidth - 160 - 8)), + }); + } + const close = (event: MouseEvent) => { + const target = event.target as Node; + if (!rootRef.current?.contains(target) && !popupRef.current?.contains(target)) setOpen(false); + }; + document.addEventListener('mousedown', close); + return () => document.removeEventListener('mousedown', close); + }, [open]); + + return ( +
+ + + {open && createPortal( +
+

+ Reasoning +

+ {OPTIONS.map((option) => ( + + ))} +
, + document.body, + )} +
+ ); +} diff --git a/src/components/chat/view/subcomponents/SessionModelPicker.tsx b/src/components/chat/view/subcomponents/SessionModelPicker.tsx new file mode 100644 index 0000000..71d8268 --- /dev/null +++ b/src/components/chat/view/subcomponents/SessionModelPicker.tsx @@ -0,0 +1,224 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Check, ChevronDown, Loader2 } from 'lucide-react'; + +import { cn } from '../../../../lib/utils'; +import type { ProviderModelOption } from '../../../../types/app'; + +export const DEFAULT_MODEL_VALUE = 'default'; + +type SessionModelPickerProps = { + /** Session-scoped selection: raw model id, `profile:*` preset, or `default`. */ + value: string; + /** Model the session runtime last reported; wins for display when present. */ + currentModel?: string; + /** Preset catalog; the raw model choices are derived from its role mappings. */ + presetOptions: ProviderModelOption[]; + loading?: boolean; + onSelect: (modelId: string) => Promise | unknown; +}; + +const compactModel = (modelId: string): string => modelId.split('/').pop() ?? modelId; + +const providerOf = (modelId: string): string => ( + modelId.includes('/') ? modelId.slice(0, modelId.indexOf('/')) : '' +); + +/** + * Preset role selectors read `provider/model:effort`. Reasoning is chosen by + * the effort picker next door, so the model list must offer only the base + * model id — otherwise every effort variant shows up as its own "model". + */ +export const stripEffortSuffix = (selector: string): string => + selector.replace(/:(?:off|minimal|low|medium|high|xhigh|max)$/, ''); + +/** Display order requested for provider groups; unlisted providers follow alphabetically. */ +const PROVIDER_ORDER = ['openai-codex', 'anthropic', 'kimi-code', 'zai', 'xai', 'grok-build']; + +const providerRank = (provider: string): number => { + const index = PROVIDER_ORDER.indexOf(provider); + return index === -1 ? PROVIDER_ORDER.length : index; +}; + +/** + * Unique raw model ids mentioned by any preset role, grouped by provider + * prefix. The preset catalog is the only model inventory the server exposes, + * and every model a preset can route to is by definition runnable, so the + * union of role mappings is exactly the set of valid direct choices. + */ +export function deriveSessionModelOptions( + presetOptions: ProviderModelOption[], +): Array<{ group: string; models: string[] }> { + const seen = new Set(); + for (const option of presetOptions) { + for (const selector of Object.values(option.roles ?? {})) { + // A model selector always reads provider/model; anything else (e.g. a + // profile name leaking out of config.yml) is not a selectable model. + if (typeof selector === 'string' && selector.includes('/')) seen.add(stripEffortSuffix(selector.trim())); + } + } + const groups = new Map(); + for (const model of [...seen].sort()) { + const group = providerOf(model) || 'other'; + const bucket = groups.get(group); + if (bucket) bucket.push(model); + else groups.set(group, [model]); + } + return [...groups.entries()] + .sort(([left], [right]) => providerRank(left) - providerRank(right) || left.localeCompare(right)) + .map(([group, models]) => ({ group, models })); +} + +/** + * Resolves which model id the trigger button should display: the live session + * report wins, then an explicit raw selection, then the default-role model of + * the selected (or current) preset. + */ +export function resolveDisplayModel( + value: string, + currentModel: string | undefined, + presetOptions: ProviderModelOption[], +): string | undefined { + if (currentModel?.trim()) return stripEffortSuffix(currentModel.trim()); + if (value && value !== DEFAULT_MODEL_VALUE && !value.startsWith('profile:')) return value; + const preset = presetOptions.find((option) => option.value === value) + ?? presetOptions.find((option) => option.value === DEFAULT_MODEL_VALUE); + const role = preset?.roles?.default; + if (!role) return undefined; + if (role.includes('/')) return stripEffortSuffix(role); + // A profile-name reference (no provider prefix): resolve one level through + // the referenced preset's own default role. + const referenced = presetOptions.find((option) => option.value === `profile:${role}`)?.roles?.default; + return referenced?.includes('/') ? stripEffortSuffix(referenced) : undefined; +} + +/** + * Session default-model picker. Unlike the preset picker (which swaps the + * whole five-role agent configuration), this changes only the model answering + * this session's default agent, via the same per-session active-model store. + */ +export default function SessionModelPicker({ + value, + currentModel, + presetOptions, + loading = false, + onSelect, +}: SessionModelPickerProps) { + const [open, setOpen] = useState(false); + const [selecting, setSelecting] = useState(false); + const rootRef = useRef(null); + const popupRef = useRef(null); + const [popupPosition, setPopupPosition] = useState({ bottom: 0, left: 0 }); + + const groups = useMemo(() => deriveSessionModelOptions(presetOptions), [presetOptions]); + const displayModel = resolveDisplayModel(value, currentModel, presetOptions); + const isRawSelection = value !== DEFAULT_MODEL_VALUE && !value.startsWith('profile:'); + + // The composer form clips its children (overflow-hidden rounded corners), so + // the popup must escape through a body portal with fixed positioning. + useEffect(() => { + if (!open) return; + const rect = rootRef.current?.getBoundingClientRect(); + if (rect) { + setPopupPosition({ + bottom: window.innerHeight - rect.top + 8, + left: Math.max(8, Math.min(rect.left, window.innerWidth - 288 - 8)), + }); + } + const close = (event: MouseEvent) => { + const target = event.target as Node; + if (!rootRef.current?.contains(target) && !popupRef.current?.contains(target)) setOpen(false); + }; + document.addEventListener('mousedown', close); + return () => document.removeEventListener('mousedown', close); + }, [open]); + + const choose = async (modelId: string) => { + if (modelId === value) { + setOpen(false); + return; + } + setSelecting(true); + try { + await onSelect(modelId); + setOpen(false); + } finally { + setSelecting(false); + } + }; + + return ( +
+ + + {open && createPortal( +
+
+

세션 모델

+

+ 이 세션의 기본 에이전트가 사용할 모델만 바꿉니다. +

+
+ +
+ + + {groups.map(({ group, models }) => ( +
+

+ {group} +

+ {models.map((model) => { + const isSelected = value === model; + return ( + + ); + })} +
+ ))} +
+
, + document.body, + )} +
+ ); +} diff --git a/src/components/chat/view/subcomponents/SessionStatusSummary.tsx b/src/components/chat/view/subcomponents/SessionStatusSummary.tsx deleted file mode 100644 index 0fee0e0..0000000 --- a/src/components/chat/view/subcomponents/SessionStatusSummary.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -type SessionStatusSummaryProps = { - /** Snapshot read off the live session at each turn end; null before the first turn. */ - sessionState: Record | null; -}; - -const text = (value: unknown): string | undefined => - typeof value === 'string' && value.trim() ? value : undefined; - -const finite = (value: unknown): number | undefined => - typeof value === 'number' && Number.isFinite(value) ? value : undefined; - -/** `/Users/me/repos/app` -> `~/repos/app`, then the last two segments. */ -function compactPath(path: string): string { - const home = path.replace(/^\/(?:Users|home)\/[^/]+/, '~'); - const segments = home.split('/').filter(Boolean); - if (home.startsWith('~') || segments.length <= 2) return home; - return `…/${segments.slice(-2).join('/')}`; -} - -/** Drops a provider prefix so `openai/gpt-5-codex` reads as `gpt-5-codex`. */ -const compactModel = (modelId: string): string => modelId.split('/').pop() ?? modelId; - -/** - * The facts the TUI keeps in its footer: which model is answering, at what - * reasoning level, in which directory, and how full the context is. - * - * The app showed only a raw token count, because the context window it is a - * fraction of never left the server. `12.3K` says nothing on its own — the same - * number is comfortable in a 200k window and nearly fatal in a 32k one. - * - * Each field renders only when the session actually reported it. There is no - * fallback context size: guessing one would print a confident percentage that - * is simply wrong for the model in use. - */ -export default function SessionStatusSummary({ sessionState }: SessionStatusSummaryProps) { - const { t } = useTranslation('chat'); - if (!sessionState) return null; - - const model = text(sessionState.modelId); - const thinking = text(sessionState.thinkingLevel); - const cwd = text(sessionState.cwd); - const percent = finite(sessionState.contextPercent); - const contextWindow = finite(sessionState.contextWindow); - - const parts: Array<{ key: string; label: string; title: string }> = []; - - if (model) { - parts.push({ key: 'model', label: compactModel(model), title: model }); - } - if (thinking && thinking !== 'default') { - parts.push({ - key: 'thinking', - label: thinking, - title: t('input.status.reasoning', { defaultValue: 'Reasoning effort' }), - }); - } - if (cwd) { - parts.push({ key: 'cwd', label: compactPath(cwd), title: cwd }); - } - if (percent !== undefined && contextWindow !== undefined) { - const used = finite(sessionState.contextTokens); - // Built without interpolation on purpose: these are numbers, they need no - // translation, and a `{{window}}` placeholder leaks verbatim anywhere i18n - // has not initialised. Showing both figures also beats restating the - // percentage already on screen. - parts.push({ - key: 'context', - label: `${Math.round(percent)}%`, - title: used !== undefined - ? `${used.toLocaleString()} / ${contextWindow.toLocaleString()} tokens` - : `${contextWindow.toLocaleString()} token context`, - }); - } - - if (parts.length === 0) return null; - - return ( -
- {parts.map((part, index) => ( - - {index > 0 && ·} - {part.label} - - ))} -
- ); -} diff --git a/src/components/chat/view/subcomponents/SkillPicker.tsx b/src/components/chat/view/subcomponents/SkillPicker.tsx new file mode 100644 index 0000000..0e82230 --- /dev/null +++ b/src/components/chat/view/subcomponents/SkillPicker.tsx @@ -0,0 +1,130 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Search, Sparkles } from 'lucide-react'; + +export type SelectableSkill = { + name: string; + description?: string; + path?: string; + type?: string; + metadata?: Record; +}; + +type SkillPickerProps = { + skills: SelectableSkill[]; + onSelect: (skill: SelectableSkill, index: number) => void; +}; + +const displayName = (skill: SelectableSkill): string => + String(skill.metadata?.skillName ?? skill.name.replace(/^\/skill:/, '')); + +export default function SkillPicker({ skills, onSelect }: SkillPickerProps) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const rootRef = useRef(null); + const popupRef = useRef(null); + const searchRef = useRef(null); + const [popupPosition, setPopupPosition] = useState({ bottom: 0, left: 0 }); + + // The composer form clips its children (overflow-hidden rounded corners), so + // the popup must escape through a body portal with fixed positioning. + useEffect(() => { + if (!open) return; + setQuery(''); + const rect = rootRef.current?.getBoundingClientRect(); + if (rect) { + setPopupPosition({ + bottom: window.innerHeight - rect.top + 8, + left: Math.max(8, Math.min(rect.left, window.innerWidth - 320 - 8)), + }); + } + window.requestAnimationFrame(() => searchRef.current?.focus()); + const close = (event: MouseEvent) => { + const target = event.target as Node; + if (!rootRef.current?.contains(target) && !popupRef.current?.contains(target)) setOpen(false); + }; + document.addEventListener('mousedown', close); + return () => document.removeEventListener('mousedown', close); + }, [open]); + + const filteredSkills = useMemo(() => { + const normalized = query.trim().toLowerCase(); + if (!normalized) return skills; + return skills.filter((skill) => + `${displayName(skill)} ${skill.description ?? ''}`.toLowerCase().includes(normalized), + ); + }, [query, skills]); + + return ( +
+ + + {open && createPortal( +
+
+

스킬

+

현재 프로젝트에서 사용할 스킬을 선택합니다.

+
+
+ + setQuery(event.target.value)} + placeholder="스킬 검색" + aria-label="스킬 검색" + className="h-7 w-full rounded-md border border-input bg-background pl-7 pr-2 text-xs outline-none placeholder:text-muted-foreground focus:border-ring" + /> +
+
+ {filteredSkills.length > 0 ? filteredSkills.map((skill) => { + const originalIndex = skills.indexOf(skill); + return ( + + ); + }) : ( +

+ 일치하는 스킬이 없습니다. +

+ )} +
+
, + document.body, + )} +
+ ); +} diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..eefd6a5 --- /dev/null +++ b/website/README.md @@ -0,0 +1,21 @@ +# Gajae Code App website + +가재코드앱 다운로드 랜딩입니다. 제품 UI와 분리되어 있고, 공개 아티팩트는 GitHub Release만 가리킵니다. + +## 실행 + +```bash +cd website +npm install +npm run dev +``` + +http://127.0.0.1:4173 + +## 구성 + +- 히어로 + 바로 보이는 macOS 다운로드: [paseo.sh](https://paseo.sh/) +- 번호 섹션과 큰 제품 스크린샷: [localstudio.ai](https://localstudio.ai/) +- 카피, 색, 캐릭터, 지원 범위는 Gajae Code App 기준 + +Intel Mac / Windows / Linux 데스크톱 링크는 아직 만들지 않았습니다. diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..d18abe0 --- /dev/null +++ b/website/index.html @@ -0,0 +1,32 @@ + + + + + + Gajae Code App — Run Gajae Code from your desk + + + + + + + + + + + + + +
+ + + diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 0000000..528f2c2 --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,1120 @@ +{ + "name": "gajae-code-app-website", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gajae-code-app-website", + "devDependencies": { + "vite": "^7.1.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..0bf1932 --- /dev/null +++ b/website/package.json @@ -0,0 +1,14 @@ +{ + "name": "gajae-code-app-website", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "node --test tests/*.test.js" + }, + "devDependencies": { + "vite": "^7.1.3" + } +} diff --git a/website/public/apple-touch-icon.png b/website/public/apple-touch-icon.png new file mode 100644 index 0000000..573fe69 Binary files /dev/null and b/website/public/apple-touch-icon.png differ diff --git a/website/public/brand/character.png b/website/public/brand/character.png new file mode 100644 index 0000000..1ba2b13 Binary files /dev/null and b/website/public/brand/character.png differ diff --git a/website/public/favicon.png b/website/public/favicon.png new file mode 100644 index 0000000..7ba5b5e Binary files /dev/null and b/website/public/favicon.png differ diff --git a/website/public/favicon.svg b/website/public/favicon.svg new file mode 100644 index 0000000..d8b5a93 --- /dev/null +++ b/website/public/favicon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/website/public/icon-512.png b/website/public/icon-512.png new file mode 100644 index 0000000..6205777 Binary files /dev/null and b/website/public/icon-512.png differ diff --git a/website/public/icon.svg b/website/public/icon.svg new file mode 100644 index 0000000..d8b5a93 --- /dev/null +++ b/website/public/icon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/website/public/logo.png b/website/public/logo.png new file mode 100644 index 0000000..dd72ccb Binary files /dev/null and b/website/public/logo.png differ diff --git a/website/public/screenshots/gajae-app-overview.jpg b/website/public/screenshots/gajae-app-overview.jpg new file mode 100644 index 0000000..ff5a661 Binary files /dev/null and b/website/public/screenshots/gajae-app-overview.jpg differ diff --git a/website/public/screenshots/model-presets.jpg b/website/public/screenshots/model-presets.jpg new file mode 100644 index 0000000..3721d4c Binary files /dev/null and b/website/public/screenshots/model-presets.jpg differ diff --git a/website/public/screenshots/skill-commands.jpg b/website/public/screenshots/skill-commands.jpg new file mode 100644 index 0000000..2fa57d7 Binary files /dev/null and b/website/public/screenshots/skill-commands.jpg differ diff --git a/website/src/main.js b/website/src/main.js new file mode 100644 index 0000000..f2ac7b9 --- /dev/null +++ b/website/src/main.js @@ -0,0 +1,10 @@ +import './styles.css'; +import { renderLandingPage } from './page.js'; + +function el(html) { + const template = document.createElement('template'); + template.innerHTML = html.trim(); + return template.content; +} + +document.getElementById('app').replaceChildren(el(renderLandingPage())); diff --git a/website/src/page.js b/website/src/page.js new file mode 100644 index 0000000..c1ea3b5 --- /dev/null +++ b/website/src/page.js @@ -0,0 +1,298 @@ +import { + DOWNLOADS, + DOCS_INSTALL_URL, + DOCS_SELF_HOST_URL, + GAJAE_CODE_URL, + ISSUES_URL, + LICENSE_URL, + PRODUCT_NAME, + RELEASE, + RELEASES_URL, + REPOSITORY_URL, +} from './releases.js'; + +function appleIcon() { + return ``; +} + +function globeIcon() { + return ``; +} + +function terminalIcon() { + return ``; +} + +function faqItem(question, answer, open = false) { + return ` +
+ + + + + ${question} + +

${answer}

+
+ `; +} + +export function renderLandingPage() { + return ` +
+
+ + +
+

Run Gajae Code
from your desk

+

+ A local-first workspace for Gajae Code. Projects, sessions, presets, and skills stay on your machine. +

+
+ + All download options +
+ Supports + Gajae Code + Default + Planner + Executor + Architect + Critic +
+
+
+
+ +
+
+ Gajae Code App workspace with sessions nested under a project +
+
+
+ +
+
+
+
+
+

Works with your Gajae setup

+

The app does not sell a model or a subscription. It uses the accounts, presets, and skills already configured in Gajae Code.

+
+
+
Projects
+
Sessions
+
Presets
+
Skills
+
Approvals
+
+
+ +
+
+

One surface for the work

+

Expand a project, resume a session, switch a preset, and watch tools stream in the same conversation.

+
+
+
+ Agent preset picker +
Default, Planner, Executor, Architect, and Critic in one picker.
+
+
+ Skill command menu +
Search project, user, and bundled skills with /skill:.
+
+
+
+ +
+
+

Review, continue, ship

+

Start a task from the project row, keep files beside the chat, and leave execution state on the host.

+
+
+
+ +
gajae-app / test / New Session
+
+
+
+
+ Live timeline + streaming +
+
+
src/auth/session.ts+42
+
src/auth/middleware.ts+18
+
tests/auth.test.ts+31
+
+
+
+
+ Ready to continue + local +
+
+ Preset + Skill + Send +
+
+
+
+
+ +
+
+

Download

+

v${RELEASE.version} · pinned GitHub Release artifacts with matching checksums.

+
+
+
+
${appleIcon()} macOS
+ +
+
+
${terminalIcon()} Linux server
+ +
+
+
${globeIcon()} Source
+
git clone ${REPOSITORY_URL}.git
+cd gajae-code-app
+npm ci && npm run dev
+
+
+

+ Intel Mac, Windows, and Linux desktop builds are not packaged yet. + Production installs should use the versioned files on + GitHub Releases, + not a latest URL. +

+
+ +
+
+

Install

+

Verify the checksum before opening the DMG or unpacking the server archive.

+
+
+
+

macOS

+

Apple Silicon, macOS 11+. The current beta DMG is ad-hoc signed and not notarized.

+
cd ~/Downloads
+curl -L -O ${DOWNLOADS.macosArm64.href}
+curl -L -O ${DOWNLOADS.macosArm64.checksumHref}
+${DOWNLOADS.macosArm64.verifyCommand}
+
+
+

Linux server

+

x86_64, glibc 2.35+, Node.js 22. Do not substitute a registry or container image.

+
curl --fail -LO ${DOWNLOADS.linuxServer.href}
+curl --fail -LO ${DOWNLOADS.linuxServer.checksumHref}
+${DOWNLOADS.linuxServer.verifyCommand}
+ +
+
+
+ +
+
+

FAQ

+
+
+ ${faqItem( + 'Is this free?', + 'Yes. Gajae Code App is open source under AGPL-3.0-or-later. It does not include an AI model or subscription. You need Gajae Code configured with your own credentials.', + true, + )} + ${faqItem( + 'Does my code leave my machine?', + 'The app keeps project files and execution state on the host. Agents talk to their own APIs the same way they would from the CLI. This app does not upload your repository.', + )} + ${faqItem( + 'What agents does it support?', + 'Gajae Code only. This product line does not wrap Claude Code, Codex, OpenCode, or Cursor as separate providers.', + )} + ${faqItem( + 'Do I need the desktop app?', + 'No. You can run the web UI from source or install the Linux server archive. The desktop app is the Apple Silicon beta shell around the same local server.', + )} + ${faqItem( + 'Why might macOS block the first launch?', + 'The current beta DMG is ad-hoc signed and not Apple-notarized. Verify the checksum from GitHub Releases, then Control-click the app and choose Open.', + )} +
+
+ + +
+
+ + +
+ `; +} diff --git a/website/src/releases.js b/website/src/releases.js new file mode 100644 index 0000000..7d8b103 --- /dev/null +++ b/website/src/releases.js @@ -0,0 +1,59 @@ +export const PRODUCT_NAME = 'Gajae Code App'; +export const REPOSITORY_URL = 'https://github.com/devswha/gajae-code-app'; +export const RELEASES_URL = `${REPOSITORY_URL}/releases`; +export const ISSUES_URL = `${REPOSITORY_URL}/issues`; +export const LICENSE_URL = `${REPOSITORY_URL}/blob/main/LICENSE`; +export const DOCS_INSTALL_URL = `${REPOSITORY_URL}/blob/main/docs/INSTALL.md`; +export const DOCS_SELF_HOST_URL = `${REPOSITORY_URL}/blob/main/docs/SELF-HOST.md`; +export const GAJAE_CODE_URL = 'https://github.com/devswha/gajae-code'; + +export const RELEASE = { + version: '2.0.0-beta.3', + tag: 'v2.0.0-beta.3', + channel: 'beta', + publishedLabel: '2026-07-22', +}; + +export function releaseDownloadBase(tag = RELEASE.tag) { + return `${RELEASES_URL}/download/${tag}`; +} + +export function desktopDmgName(version = RELEASE.version) { + return `gajae-app-desktop-${version}-macos-arm64.dmg`; +} + +export function serverArchiveName(version = RELEASE.version) { + return `gajae-app-server-${version}-linux-x64-node22.tar.gz`; +} + +export function checksumName(artifactName) { + return `${artifactName}.sha256`; +} + +export function downloadUrl(fileName, tag = RELEASE.tag) { + return `${releaseDownloadBase(tag)}/${fileName}`; +} + +export function buildDownloads(release = RELEASE) { + const dmg = desktopDmgName(release.version); + const server = serverArchiveName(release.version); + return { + tagUrl: `${RELEASES_URL}/tag/${release.tag}`, + macosArm64: { + label: dmg, + href: downloadUrl(dmg, release.tag), + checksumHref: downloadUrl(checksumName(dmg), release.tag), + checksumFile: checksumName(dmg), + verifyCommand: `shasum -a 256 -c ${checksumName(dmg)}`, + }, + linuxServer: { + label: server, + href: downloadUrl(server, release.tag), + checksumHref: downloadUrl(checksumName(server), release.tag), + checksumFile: checksumName(server), + verifyCommand: `sha256sum --check ${checksumName(server)}`, + }, + }; +} + +export const DOWNLOADS = buildDownloads(); diff --git a/website/src/styles.css b/website/src/styles.css new file mode 100644 index 0000000..c43f297 --- /dev/null +++ b/website/src/styles.css @@ -0,0 +1,671 @@ +:root { + color-scheme: dark; + --background: #101615; + --foreground: #fafafa; + --muted: #1a1f1e; + --muted-foreground: #a8adac; + --card: #171d1c; + --border: #252b2a; + --primary: #f04e23; + --sans: "Pretendard Variable", Pretendard, Inter, ui-sans-serif, system-ui, sans-serif; + --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; + max-width: 100%; + overflow-x: clip; + background: var(--background); + color: var(--foreground); + font-family: var(--sans); +} + +body { + font-size: 16px; + line-height: 1.55; +} + +a { + color: inherit; + text-decoration: none; +} + +img { + display: block; + max-width: 100%; + height: auto; +} + +button, +summary { + font: inherit; +} + +.hero-shell { + position: relative; +} + +.hero-inner { + width: min(80rem, calc(100% - 3rem)); + max-width: 100%; + margin: 0 auto; + padding: 1.5rem 0 2.5rem; +} + +.site-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + margin-bottom: 4rem; +} + +.brand { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.brand img { + width: 1.5rem; + height: 1.5rem; + border-radius: 0.35rem; +} + +.brand-mark { + border-radius: 0.35rem; +} + +.brand span { + font-size: 1.125rem; + font-weight: 500; +} + +.nav { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 1rem; +} + +.nav a, +.quiet-link, +.footer-col a { + color: var(--muted-foreground); + font-size: 0.875rem; + transition: color 150ms ease; +} + +.nav a:hover, +.quiet-link:hover, +.footer-col a:hover, +.plain-link:hover { + color: var(--foreground); +} + +.hero h1 { + margin: 0; + font-size: 1.875rem; + font-weight: 500; + letter-spacing: -0.03em; + line-height: 1.15; +} + +.hero .lede { + max-width: 32rem; + margin: 1.5rem 0 0; + color: rgba(255, 255, 255, 0.7); + font-size: 1.125rem; + line-height: 1.7; +} + +.cta-block { + padding-top: 2.5rem; +} + +.cta-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 2.5rem; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: 1px solid transparent; + font-size: 0.875rem; + font-weight: 500; +} + +.btn-primary { + background: var(--foreground); + color: var(--background); +} + +.btn-primary:hover { + background: rgba(250, 250, 250, 0.9); +} + +.btn-ghost { + border-color: rgba(255, 255, 255, 0.2); + color: var(--foreground); +} + +.btn-ghost:hover { + background: rgba(255, 255, 255, 0.1); +} + +.icon-btn { + width: 2.5rem; + padding-inline: 0.75rem; +} + +.all-options { + display: inline-block; + padding-top: 0.75rem; + font-size: 0.75rem; +} + +.supports { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + padding-top: 1.5rem; +} + +.supports > span { + color: var(--muted-foreground); + font-size: 0.75rem; +} + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.75rem; + padding: 0.2rem 0.55rem; + border-radius: 999px; + color: rgba(255, 255, 255, 0.6); + font-size: 0.75rem; +} + +.hero-shot-wrap { + width: min(80rem, calc(100% - 3rem)); + max-width: 100%; + margin: 0 auto; + padding: 0 0 2rem; +} + +.hero-shot { + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 1rem; + background: #0a0e0d; + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35); +} + +.hero-shot img { + width: 100%; + height: auto; +} + +.landing-content { + background: var(--background); +} + +.content { + width: min(64rem, calc(100% - 3rem)); + max-width: 100%; + margin: 0 auto; + padding: 5rem 0 2.5rem; +} + +.stack { + display: grid; + gap: 6rem; +} + +.section-head { + margin-bottom: 3rem; +} + +.section-head h2 { + margin: 0 0 0.5rem; + font-size: 1.875rem; + font-weight: 500; + letter-spacing: -0.02em; +} + +.section-head p { + max-width: 32rem; + margin: 0; + color: var(--muted-foreground); + font-size: 1rem; +} + +.grid-2, +.grid-3, +.grid-5 { + display: grid; + gap: 1rem; + min-width: 0; +} + +.grid-2 > *, +.grid-3 > *, +.grid-5 > *, +.workflow > *, +.stack > *, +.download-card, +.shot-card, +.surface, +.hero-shot, +.platform, +.review, +.ship { + min-width: 0; + max-width: 100%; +} + +.grid-2 { + grid-template-columns: 1fr; +} + +.grid-3, +.grid-5 { + grid-template-columns: 1fr; +} + +.panel, +.shot-card, +.download-card { + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 0.75rem; + background: rgba(255, 255, 255, 0.03); +} + +.panel { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + font-weight: 500; +} + +.shot-card { + overflow: hidden; +} + +.shot-card img { + width: 100%; + height: auto; +} + +.shot-card figcaption { + padding: 1rem 1.15rem 1.15rem; + color: var(--muted-foreground); + font-size: 0.875rem; +} + +.surface { + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 1rem; + background: rgba(255, 255, 255, 0.02); +} + +.chrome { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 0.8rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.03); +} + +.dots { + display: flex; + gap: 0.35rem; +} + +.dots span { + width: 0.625rem; + height: 0.625rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.18); +} + +.dots span:first-child { + background: rgba(248, 113, 113, 0.6); +} + +.dots span:nth-child(2) { + background: rgba(252, 211, 77, 0.6); +} + +.dots span:last-child { + background: rgba(74, 222, 128, 0.6); +} + +.address { + flex: 1; + min-width: 0; + padding: 0.2rem 0.5rem; + border-radius: 0.35rem; + background: rgba(0, 0, 0, 0.3); + color: rgba(255, 255, 255, 0.35); + font-family: var(--mono); + font-size: 10px; + text-align: center; +} + +.workflow { + display: grid; + gap: 1rem; + padding: 1rem; +} + +.review, +.ship { + padding: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 0.75rem; + background: rgba(255, 255, 255, 0.03); +} + +.review-head, +.ship-head, +.file-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.review-head, +.ship-head { + margin-bottom: 0.75rem; +} + +.file-list { + display: grid; + gap: 0.5rem; +} + +.file-row { + font-size: 0.75rem; +} + +.file-row code { + min-width: 0; + overflow: hidden; + color: rgba(255, 255, 255, 0.5); + font-family: var(--mono); + text-overflow: ellipsis; + white-space: nowrap; +} + +.delta { + color: rgba(134, 239, 172, 0.7); + font-family: var(--mono); +} + +.ok { + padding: 0.2rem 0.5rem; + border-radius: 999px; + background: rgba(74, 222, 128, 0.1); + color: #6ee7b7; + font-size: 0.75rem; +} + +.ship-actions { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; +} + +.ship-actions span { + padding: 0.5rem 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + font-size: 0.75rem; + text-align: center; +} + +.ship-actions span:last-child { + border-color: rgba(74, 222, 128, 0.2); + background: rgba(74, 222, 128, 0.15); + color: #bbf7d0; +} + +.download-card { + padding: 1.5rem; +} + +.platform { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1.25rem 0; + border-top: 1px solid var(--border); +} + +.platform:first-child { + border-top: 0; + padding-top: 0; +} + +.platform:last-child { + padding-bottom: 0; +} + +.platform-label { + display: flex; + align-items: center; + gap: 0.75rem; + font-weight: 500; +} + +.pills { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.pill { + display: inline-flex; + align-items: center; + min-height: 2rem; + padding: 0 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; + color: var(--foreground); + font-size: 0.8125rem; +} + +.pill:hover { + background: rgba(255, 255, 255, 0.08); +} + +.code { + display: block; + max-width: 100%; + min-width: 0; + overflow-x: auto; + margin: 0; + padding: 0.7rem 0.85rem; + border-radius: 0.5rem; + background: rgba(0, 0, 0, 0.28); + color: rgba(250, 250, 250, 0.82); + font-family: var(--mono); + font-size: 0.75rem; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.note { + margin: 2rem 0 0; + color: var(--muted-foreground); + font-size: 0.75rem; + text-align: center; +} + +.faq-list { + display: grid; + gap: 1.25rem; + padding-left: 1rem; +} + +.faq summary { + display: flex; + align-items: flex-start; + gap: 0.5rem; + margin-left: -1rem; + cursor: pointer; + list-style: none; + font-size: 0.875rem; + font-weight: 500; +} + +.faq summary::-webkit-details-marker { + display: none; +} + +.faq-mark { + flex-shrink: 0; + color: rgba(255, 255, 255, 0.4); + font-family: var(--mono); +} + +.faq[open] .faq-plus, +.faq:not([open]) .faq-minus { + display: none; +} + +.faq p { + max-width: 42rem; + margin: 0.5rem 0 0 0.25rem; + color: var(--muted-foreground); + font-size: 0.875rem; +} + +.sponsor { + max-width: 36rem; +} + +.sponsor p { + color: var(--muted-foreground); +} + +.site-footer { + width: min(64rem, calc(100% - 3rem)); + max-width: 100%; + margin: 0 auto; + padding: 0 0 5rem; +} + +.footer-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + font-size: 0.875rem; +} + +.footer-col { + display: grid; + gap: 0.5rem; +} + +.footer-col p { + margin: 0 0 0.25rem; + color: rgba(255, 255, 255, 0.6); + font-weight: 500; +} + +@media (min-width: 640px) { + .grid-2, + .grid-3 { + grid-template-columns: 1fr 1fr; + } + + .grid-5 { + grid-template-columns: repeat(3, 1fr); + } + + .platform { + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .footer-grid { + grid-template-columns: repeat(4, 1fr); + } +} + +@media (min-width: 768px) { + .hero-inner { + padding: 5rem 0 3rem; + } + + .site-header { + flex-direction: row; + justify-content: space-between; + } + + .hero h1 { + font-size: 3rem; + } + + .content { + padding-top: 10rem; + } + + .grid-3, + .grid-5 { + grid-template-columns: repeat(3, 1fr); + } + + .workflow { + grid-template-columns: 1.1fr 0.9fr; + } + + .download-card { + padding: 2rem; + } +} + +@media (min-width: 1024px) { + .grid-5 { + grid-template-columns: repeat(5, 1fr); + } +} + +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + } +} + +.docs-links { + margin-top: 1rem; + text-align: left; +} diff --git a/website/tests/page.test.js b/website/tests/page.test.js new file mode 100644 index 0000000..8ad59fe --- /dev/null +++ b/website/tests/page.test.js @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { renderLandingPage } from '../src/page.js'; +import { DOWNLOADS, RELEASE } from '../src/releases.js'; + +test('landing page exposes the pinned GitHub download buttons', () => { + const html = renderLandingPage(); + assert.match(html, /id="download"/); + assert.match(html, new RegExp(DOWNLOADS.macosArm64.href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + assert.match(html, new RegExp(DOWNLOADS.linuxServer.href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + assert.match(html, /Download for Mac/); + assert.equal(html.includes('Windows용 내려받기'), false); + assert.equal(html.includes('Download for Windows'), false); + assert.equal(html.includes('/latest/download/'), false); + assert.match(html, new RegExp(RELEASE.tag.replace('.', '\\.'))); +}); diff --git a/website/tests/releases.test.js b/website/tests/releases.test.js new file mode 100644 index 0000000..b8274c6 --- /dev/null +++ b/website/tests/releases.test.js @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + DOWNLOADS, + RELEASE, + RELEASES_URL, + buildDownloads, + checksumName, + desktopDmgName, + downloadUrl, + serverArchiveName, +} from '../src/releases.js'; + +test('pins the current public beta and GitHub download URLs', () => { + assert.equal(RELEASE.version, '2.0.0-beta.3'); + assert.equal(RELEASE.tag, 'v2.0.0-beta.3'); + assert.equal(desktopDmgName(), 'gajae-app-desktop-2.0.0-beta.3-macos-arm64.dmg'); + assert.equal(serverArchiveName(), 'gajae-app-server-2.0.0-beta.3-linux-x64-node22.tar.gz'); + assert.equal( + downloadUrl(desktopDmgName()), + `${RELEASES_URL}/download/v2.0.0-beta.3/gajae-app-desktop-2.0.0-beta.3-macos-arm64.dmg`, + ); + assert.equal( + DOWNLOADS.macosArm64.checksumHref, + `${RELEASES_URL}/download/v2.0.0-beta.3/${checksumName(desktopDmgName())}`, + ); + assert.match(DOWNLOADS.macosArm64.verifyCommand, /shasum -a 256 -c /); +}); + +test('does not invent Windows or Intel desktop artifacts', () => { + const downloads = buildDownloads(); + assert.equal('windows' in downloads, false); + assert.equal('macosIntel' in downloads, false); + assert.ok(downloads.linuxServer.href.includes('linux-x64-node22')); +}); diff --git a/website/vite.config.js b/website/vite.config.js new file mode 100644 index 0000000..c5fd9fc --- /dev/null +++ b/website/vite.config.js @@ -0,0 +1,22 @@ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; + +const rootDir = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + root: rootDir, + publicDir: 'public', + server: { + host: '127.0.0.1', + port: 4173, + }, + preview: { + host: '127.0.0.1', + port: 4174, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + }, +});