Skip to content

Commit 03012d8

Browse files
committed
feat(vibe): add per‑vibe dependenciesUserOverride flag; persist with Libraries chooser; gate prompt module selection on override\n\n- Extend VibeDocument with dependenciesUserOverride:boolean\n- AppSettings: saving Libraries sets override=true and persists via useSession\n- useSession.updateDependencies now normalizes (validate, de‑dupe, canonical order) and saves flag\n- prompts: when override=true use saved deps; otherwise use deterministic DEFAULT_DEPENDENCIES (picker bypass pending clarification)\n- tests: update UI + prompt tests for override gating and keep defaults for non‑override path\n- use ALLOWED_DEPENDENCY_NAMES as single source of truth
1 parent c761ab7 commit 03012d8

File tree

7 files changed

+66
-21
lines changed

7 files changed

+66
-21
lines changed

app/components/ResultPreview/AppSettingsView.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ type AppSettingsViewProps = {
66
onUpdateTitle: (next: string, isManual?: boolean) => Promise<void> | void;
77
onDownloadHtml: () => void;
88
selectedDependencies?: string[];
9-
onUpdateDependencies: (deps: string[]) => Promise<void> | void;
9+
// When saving a manual selection, we set `userOverride` true
10+
onUpdateDependencies: (deps: string[], userOverride: boolean) => Promise<void> | void;
1011
};
1112

1213
const AppSettingsView: React.FC<AppSettingsViewProps> = ({
@@ -89,7 +90,7 @@ const AppSettingsView: React.FC<AppSettingsViewProps> = ({
8990
setSaveDepsErr(null);
9091
try {
9192
const valid = deps.filter((n) => allowedNames.has(n));
92-
await onUpdateDependencies(valid.length ? valid : DEFAULT_DEPENDENCIES);
93+
await onUpdateDependencies(valid.length ? valid : DEFAULT_DEPENDENCIES, true);
9394
setHasUnsavedDeps(false);
9495
setSaveDepsOk(true);
9596
setTimeout(() => setSaveDepsOk(false), 2000);

app/hooks/useSession.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
import { getSessionDatabaseName } from '../utils/databaseManager';
99
import { useLazyFireproof } from './useLazyFireproof';
1010
import { encodeTitle } from '../components/SessionSidebar/utils';
11-
import { llmsCatalog } from '../llms/catalog';
11+
import { ALLOWED_DEPENDENCY_NAMES, llmsCatalog } from '../llms/catalog';
1212

1313
export function useSession(routedSessionId?: string) {
1414
const [generatedSessionId] = useState(
@@ -138,14 +138,22 @@ export function useSession(routedSessionId?: string) {
138138

139139
// Update per‑vibe dependency selection using the vibe document
140140
const updateDependencies = useCallback(
141-
async (deps: string[]) => {
142-
const allowed = new Set(llmsCatalog.map((l) => l.name));
143-
const validDeps = (Array.isArray(deps) ? deps : [])
144-
.filter((n): n is string => typeof n === 'string')
145-
.filter((n) => allowed.has(n));
141+
async (deps: string[], userOverride: boolean = true) => {
142+
const input = Array.isArray(deps)
143+
? deps.filter((n): n is string => typeof n === 'string')
144+
: [];
145+
// Validate and de‑dupe by allowed names
146+
const deduped = Array.from(new Set(input.filter((n) => ALLOWED_DEPENDENCY_NAMES.has(n))));
147+
// Canonicalize order by catalog order
148+
const order = new Map(llmsCatalog.map((l, i) => [l.name, i] as const));
149+
const validDeps = deduped.sort((a, b) => order.get(a)! - order.get(b)!);
146150

147151
const base = vibeRef.current;
148-
const updatedDoc = { ...base, dependencies: validDeps } as VibeDocument;
152+
const updatedDoc = {
153+
...base,
154+
dependencies: validDeps,
155+
dependenciesUserOverride: !!userOverride,
156+
} as VibeDocument;
149157
mergeRef.current(updatedDoc);
150158
await sessionDatabase.put(updatedDoc);
151159
},

app/prompts.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import callaiTxt from './llms/callai.txt?raw';
33
import fireproofTxt from './llms/fireproof.txt?raw';
44
import imageGenTxt from './llms/image-gen.txt?raw';
5-
import { DEFAULT_DEPENDENCIES, llmsCatalog, type LlmsCatalogEntry } from './llms/catalog';
5+
import {
6+
DEFAULT_DEPENDENCIES,
7+
llmsCatalog,
8+
type LlmsCatalogEntry,
9+
ALLOWED_DEPENDENCY_NAMES,
10+
} from './llms/catalog';
611

712
// Static mapping of LLM text content
813
const llmsTextContent: Record<string, string> = {
@@ -62,19 +67,22 @@ export function generateImportStatements(llms: LlmsCatalogEntry[]) {
6267
export async function makeBaseSystemPrompt(model: string, sessionDoc?: any) {
6368
// Inputs for module selection
6469
const userPrompt = sessionDoc?.userPrompt || '';
65-
// Deterministic dependency selection
66-
let selectedNames: string[] | undefined = undefined;
67-
if (Array.isArray(sessionDoc?.dependencies)) {
68-
// Validate against catalog
69-
const allowed = new Set(llmsCatalog.map((l) => l.name));
70+
// Deterministic dependency selection with user override gating
71+
const useOverride = !!sessionDoc?.dependenciesUserOverride;
72+
let selectedNames: string[] | undefined;
73+
if (useOverride && Array.isArray(sessionDoc?.dependencies)) {
7074
selectedNames = (sessionDoc.dependencies as unknown[])
7175
.filter((v): v is string => typeof v === 'string')
72-
.filter((name) => allowed.has(name));
76+
.filter((name) => ALLOWED_DEPENDENCY_NAMES.has(name));
77+
// Note: allow empty [] when override is true? Clarified in PR discussion; for now,
78+
// we keep defaults for empty only when override is not set.
7379
}
74-
// Apply clear default when not provided or empty
75-
if (!selectedNames || selectedNames.length === 0) {
80+
// Non-override path: use deterministic defaults (pending clarification on re-enabling picker)
81+
if (!useOverride) {
7682
selectedNames = [...DEFAULT_DEPENDENCIES];
7783
}
84+
// Final safety: ensure array is defined
85+
if (!selectedNames) selectedNames = [...DEFAULT_DEPENDENCIES];
7886
const chosenLlms = llmsCatalog.filter((l) => selectedNames!.includes(l.name));
7987

8088
// 3) Concatenate docs for chosen modules

app/types/chat.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export interface VibeDocument {
2020
* These control which helper libraries and docs are injected into prompts.
2121
*/
2222
dependencies?: string[];
23+
/**
24+
* When true, treat `dependencies` as a user override and bypass any
25+
* automatic/catalog-based module selection.
26+
*/
27+
dependenciesUserOverride?: boolean;
2328
}
2429

2530
// ===== Content Segment Types =====

tests/appSettings-dependencies.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('AppSettingsView Libraries (per‑vibe dependency chooser)', () => {
5353
expect(save).not.toBeDisabled();
5454

5555
await act(async () => fireEvent.click(save));
56-
expect(onUpdateDependencies).toHaveBeenCalledWith(['fireproof']);
56+
expect(onUpdateDependencies).toHaveBeenCalledWith(['fireproof'], true);
5757

5858
// After save, button should disable again briefly
5959
expect(save).toBeDisabled();

tests/prompt-builder.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,14 @@ describe('prompt builder (real implementation)', () => {
123123
// User prompt appears verbatim
124124
expect(prompt).toContain('hello');
125125
});
126+
127+
it('makeBaseSystemPrompt: honors explicit dependencies only when override=true', async () => {
128+
await preloadLlmsText();
129+
const prompt = await makeBaseSystemPrompt('test-model', {
130+
dependencies: ['fireproof'],
131+
dependenciesUserOverride: true,
132+
});
133+
expect(prompt).toContain('<useFireproof-docs>');
134+
expect(prompt).not.toContain('<callAI-docs>');
135+
});
126136
});

tests/prompts-dependencies.integration.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ beforeAll(async () => {
1414
});
1515

1616
describe('makeBaseSystemPrompt dependency selection', () => {
17-
it('uses default dependencies when none provided', async () => {
17+
it('uses default dependencies when override is false/absent', async () => {
1818
await preloadLlmsText();
1919
const prompt = await makeBaseSystemPrompt('anthropic/claude-sonnet-4', {
2020
_id: 'user_settings',
@@ -27,16 +27,29 @@ describe('makeBaseSystemPrompt dependency selection', () => {
2727
expect(prompt).toMatch(/import\s+\{\s*callAI\s*\}\s+from\s+"call-ai"/);
2828
});
2929

30-
it('honors explicit dependencies from settings', async () => {
30+
it('honors explicit dependencies only when override=true', async () => {
3131
await preloadLlmsText();
3232
const prompt = await makeBaseSystemPrompt('anthropic/claude-sonnet-4', {
3333
_id: 'user_settings',
3434
dependencies: ['fireproof'],
35+
dependenciesUserOverride: true,
3536
});
3637
expect(prompt).toMatch(/<useFireproof-docs>/);
3738
expect(prompt).not.toMatch(/<callAI-docs>/);
3839
// Import statements reflect chosen modules only
3940
expect(prompt).toMatch(/import\s+\{\s*useFireproof\s*\}\s+from\s+"use-fireproof"/);
4041
expect(prompt).not.toMatch(/from\s+"call-ai"/);
4142
});
43+
44+
it('ignores explicit dependencies when override=false', async () => {
45+
await preloadLlmsText();
46+
const prompt = await makeBaseSystemPrompt('anthropic/claude-sonnet-4', {
47+
_id: 'user_settings',
48+
dependencies: ['fireproof'],
49+
dependenciesUserOverride: false,
50+
});
51+
// Default includes both core libs
52+
expect(prompt).toMatch(/<useFireproof-docs>/);
53+
expect(prompt).toMatch(/<callAI-docs>/);
54+
});
4255
});

0 commit comments

Comments
 (0)