diff --git a/.changeset/project-default-agent.md b/.changeset/project-default-agent.md new file mode 100644 index 00000000..6ff8437c --- /dev/null +++ b/.changeset/project-default-agent.md @@ -0,0 +1,5 @@ +--- +'@inkeep/open-knowledge': patch +--- + +Use `agents.defaultAgent` in a project config to choose Codex or Pi for the Ask AI composer on machines that have no locally remembered agent choice. A user-selected agent continues to take precedence. diff --git a/packages/app/src/components/BottomComposer.tsx b/packages/app/src/components/BottomComposer.tsx index 14c43230..47247fac 100644 --- a/packages/app/src/components/BottomComposer.tsx +++ b/packages/app/src/components/BottomComposer.tsx @@ -54,6 +54,7 @@ import { } from '@/editor/selection-context'; import type { EditorSurface } from '@/editor/selection-stats'; import { useSelectionContext } from '@/hooks/use-selection-context'; +import { useConfigContextOptional } from '@/lib/config-context'; import { resolveDefaultCli } from '@/lib/default-cli-resolver'; import { VISIBLE_TARGETS } from '@/lib/handoff/targets'; import { matchesKeyboardShortcut } from '@/lib/keyboard-shortcuts'; @@ -61,6 +62,7 @@ import { recordOnboardingAskedAi } from '@/lib/onboarding-signals'; import { loadStickyAgent, parseStickyCliId, + resolveInitialAgentPreference, resolveStickyAgent, saveStickyAgent, terminalCliId, @@ -199,6 +201,7 @@ export function BottomComposer({ const effectiveSurface: EditorSurface = surface ?? 'wysiwyg'; const reduced = useReducedMotion(); const workspace = useWorkspace(); + const projectDefaultAgent = useConfigContextOptional()?.projectConfig?.agents?.defaultAgent; const { pageMeta } = usePageList(); const { states } = useInstalledAgents(); const { dispatch } = useHandoffDispatch(); @@ -465,7 +468,7 @@ export function BottomComposer({ // sentinel (`terminal-cli:`) only resolves to terminal mode when the // launcher is available, so a sticky CLI pick degrades to the first app target // on the web host. - const effectiveId = selectedId ?? stickyId; + const effectiveId = selectedId ?? resolveInitialAgentPreference(stickyId, projectDefaultAgent); // An explicit CLI pick (sticky or this session) launches that CLI regardless of // install state — a missing binary just prints "command not found". const explicitCli: TerminalCli | null = diff --git a/packages/app/src/lib/unified-agent-store.test.ts b/packages/app/src/lib/unified-agent-store.test.ts index a149a21e..e9a4e2fc 100644 --- a/packages/app/src/lib/unified-agent-store.test.ts +++ b/packages/app/src/lib/unified-agent-store.test.ts @@ -14,6 +14,7 @@ import { import { loadStickyAgent, parseStickyCliId, + resolveInitialAgentPreference, resolveStickyAgent, saveStickyAgent, TERMINAL_CLI_ID, @@ -50,6 +51,16 @@ function states( ) as Record; } +describe('resolveInitialAgentPreference', () => { + test('uses the project default when this machine has no local selection', () => { + expect(resolveInitialAgentPreference(null, 'terminal-cli:pi')).toBe('terminal-cli:pi'); + }); + + test('keeps the local user selection over a project default', () => { + expect(resolveInitialAgentPreference('codex', 'terminal-cli:pi')).toBe('codex'); + }); +}); + describe('unified-agent-store — load/save round-trip', () => { test('round-trips through the unified key', () => { const storage = makeStorage(); diff --git a/packages/app/src/lib/unified-agent-store.ts b/packages/app/src/lib/unified-agent-store.ts index 75de091f..f50f9678 100644 --- a/packages/app/src/lib/unified-agent-store.ts +++ b/packages/app/src/lib/unified-agent-store.ts @@ -135,6 +135,14 @@ export function saveStickyAgent(id: HandoffTarget | string, storage?: StickyAgen } } +/** Browser-local user choice wins over an installer-provided workspace default. */ +export function resolveInitialAgentPreference( + stickyId: string | null, + projectDefault: string | null | undefined, +): string | null { + return stickyId ?? projectDefault ?? null; +} + /** * Resolve which app agent the composer should target, given live install state * and the sticky preference: diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index ca830d88..1b0a1cd1 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -375,6 +375,17 @@ export const ConfigSchema = z.looseObject({ // config paths are a user-facing `~/.ok/global.yml` contract and hard to rename. agents: z .looseObject({ + // A workspace-provided initial handoff preference. It is intentionally a + // PROJECT field so an installer can select the runtime that owns this + // knowledge base without modifying a user's browser-local last-used + // choice. The app uses it only when no local preference exists. + defaultAgent: z.enum(['codex', 'pi', 'terminal-cli:pi']).optional().register(fieldRegistry, { + scope: 'project', + agentSettable: false, + defaultScope: 'project', + description: + 'Initial agent for this workspace when this machine has no locally remembered choice. Allowed values are codex, pi, and terminal-cli:pi. A local user selection always takes precedence.', + }), autoApproveOkTools: z .boolean() .register(fieldRegistry, {