Skip to content

Commit 4da9bfa

Browse files
author
IM.codes
committed
Refine shared context processing form defaults
1 parent 23870b8 commit 4da9bfa

File tree

13 files changed

+258
-64
lines changed

13 files changed

+258
-64
lines changed

shared/shared-context-runtime-config.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { QWEN_MODEL_IDS } from './qwen-models.js';
55

66
export const SHARED_CONTEXT_RUNTIME_BACKENDS = ['claude-code-sdk', 'codex-sdk', 'qwen', 'openclaw'] as const satisfies readonly SharedContextRuntimeBackend[];
77
export const DEFAULT_PRIMARY_CONTEXT_BACKEND: SharedContextRuntimeBackend = 'claude-code-sdk';
8+
export const DEFAULT_CONTEXT_MODEL_BY_BACKEND: Record<SharedContextRuntimeBackend, string> = {
9+
'claude-code-sdk': DEFAULT_PRIMARY_CONTEXT_MODEL,
10+
'codex-sdk': CODEX_MODEL_IDS[0],
11+
qwen: 'qwen3-coder-plus',
12+
openclaw: DEFAULT_PRIMARY_CONTEXT_MODEL,
13+
};
814

915
export const SHARED_CONTEXT_RUNTIME_CONFIG_MSG = {
1016
APPLY: 'shared_context.runtime_config.apply',
@@ -26,7 +32,7 @@ export interface SharedContextRuntimeConfigSnapshot {
2632
export function defaultSharedContextRuntimeConfig(): ContextModelConfig {
2733
return {
2834
primaryContextBackend: DEFAULT_PRIMARY_CONTEXT_BACKEND,
29-
primaryContextModel: DEFAULT_PRIMARY_CONTEXT_MODEL,
35+
primaryContextModel: DEFAULT_CONTEXT_MODEL_BY_BACKEND[DEFAULT_PRIMARY_CONTEXT_BACKEND],
3036
backupContextBackend: undefined,
3137
backupContextModel: undefined,
3238
};
@@ -49,6 +55,25 @@ export function inferSharedContextRuntimeBackend(model: string | null | undefine
4955
return undefined;
5056
}
5157

58+
export function getDefaultSharedContextModelForBackend(backend: SharedContextRuntimeBackend): string {
59+
return DEFAULT_CONTEXT_MODEL_BY_BACKEND[backend];
60+
}
61+
62+
export function isKnownSharedContextModelForBackend(backend: SharedContextRuntimeBackend, model: string | null | undefined): boolean {
63+
const trimmed = model?.trim();
64+
if (!trimmed) return false;
65+
switch (backend) {
66+
case 'claude-code-sdk':
67+
return CLAUDE_CODE_MODEL_IDS.includes(trimmed as typeof CLAUDE_CODE_MODEL_IDS[number]);
68+
case 'codex-sdk':
69+
return CODEX_MODEL_IDS.includes(trimmed as typeof CODEX_MODEL_IDS[number]);
70+
case 'qwen':
71+
return QWEN_MODEL_IDS.includes(trimmed as typeof QWEN_MODEL_IDS[number]);
72+
case 'openclaw':
73+
return true;
74+
}
75+
}
76+
5277
function trimModelValue(value: string | undefined): string | undefined {
5378
const trimmed = value?.trim();
5479
return trimmed ? trimmed : undefined;
@@ -57,19 +82,27 @@ function trimModelValue(value: string | undefined): string | undefined {
5782
export function normalizeSharedContextRuntimeConfig(
5883
input: Partial<ContextModelConfig> | null | undefined,
5984
): ContextModelConfig {
60-
const primaryContextModel = trimModelValue(input?.primaryContextModel) ?? DEFAULT_PRIMARY_CONTEXT_MODEL;
61-
const backupContextModel = trimModelValue(input?.backupContextModel);
85+
const normalizedPrimaryBackend = normalizeSharedContextRuntimeBackend(input?.primaryContextBackend)
86+
?? inferSharedContextRuntimeBackend(input?.primaryContextModel)
87+
?? DEFAULT_PRIMARY_CONTEXT_BACKEND;
88+
const rawPrimaryContextModel = trimModelValue(input?.primaryContextModel);
89+
const primaryContextModel = rawPrimaryContextModel && isKnownSharedContextModelForBackend(normalizedPrimaryBackend, rawPrimaryContextModel)
90+
? rawPrimaryContextModel
91+
: getDefaultSharedContextModelForBackend(normalizedPrimaryBackend);
92+
const normalizedBackupBackendCandidate = normalizeSharedContextRuntimeBackend(input?.backupContextBackend)
93+
?? inferSharedContextRuntimeBackend(input?.backupContextModel)
94+
?? normalizedPrimaryBackend;
95+
const rawBackupContextModel = trimModelValue(input?.backupContextModel);
96+
const backupContextModel = rawBackupContextModel
97+
? (isKnownSharedContextModelForBackend(normalizedBackupBackendCandidate, rawBackupContextModel)
98+
? rawBackupContextModel
99+
: getDefaultSharedContextModelForBackend(normalizedBackupBackendCandidate))
100+
: undefined;
62101
return {
63-
primaryContextBackend: normalizeSharedContextRuntimeBackend(input?.primaryContextBackend)
64-
?? inferSharedContextRuntimeBackend(primaryContextModel)
65-
?? DEFAULT_PRIMARY_CONTEXT_BACKEND,
102+
primaryContextBackend: normalizedPrimaryBackend,
66103
primaryContextModel,
67104
backupContextBackend: backupContextModel
68-
? (normalizeSharedContextRuntimeBackend(input?.backupContextBackend)
69-
?? inferSharedContextRuntimeBackend(backupContextModel)
70-
?? normalizeSharedContextRuntimeBackend(input?.primaryContextBackend)
71-
?? inferSharedContextRuntimeBackend(primaryContextModel)
72-
?? DEFAULT_PRIMARY_CONTEXT_BACKEND)
105+
? normalizedBackupBackendCandidate
73106
: undefined,
74107
backupContextModel,
75108
};
@@ -85,6 +118,6 @@ export function buildSharedContextRuntimeConfigSnapshot(
85118
envPrimaryOverrideActive: false,
86119
envBackupOverrideActive: false,
87120
defaultPrimaryContextBackend: DEFAULT_PRIMARY_CONTEXT_BACKEND,
88-
defaultPrimaryContextModel: DEFAULT_PRIMARY_CONTEXT_MODEL,
121+
defaultPrimaryContextModel: DEFAULT_CONTEXT_MODEL_BY_BACKEND[DEFAULT_PRIMARY_CONTEXT_BACKEND],
89122
};
90123
}

test/daemon/context-model-config.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ describe('context-model-config', () => {
2626
vi.stubEnv('IMCODES_PRIMARY_CONTEXT_MODEL', 'env-model');
2727
setContextModelRuntimeConfig({
2828
primaryContextBackend: 'codex-sdk',
29-
primaryContextModel: 'synced-model',
29+
primaryContextModel: 'gpt-5.4-mini',
3030
});
31-
expect(getContextModelConfig().primaryContextModel).toBe('synced-model');
31+
expect(getContextModelConfig().primaryContextModel).toBe('gpt-5.4-mini');
3232
expect(getContextModelConfig().primaryContextBackend).toBe('codex-sdk');
3333
});
3434
});

test/daemon/materialization-coordinator.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ describe('MaterializationCoordinator', () => {
117117
primaryContextBackend: 'codex-sdk',
118118
primaryContextModel: 'gpt-5.2',
119119
backupContextBackend: 'qwen',
120-
backupContextModel: 'qwen',
120+
backupContextModel: 'qwen3-coder-plus',
121121
});
122122
});
123123
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
getDefaultSharedContextModelForBackend,
4+
normalizeSharedContextRuntimeConfig,
5+
} from '../shared/shared-context-runtime-config.js';
6+
7+
describe('shared-context-runtime-config', () => {
8+
it('uses backend-specific defaults when model is missing', () => {
9+
expect(normalizeSharedContextRuntimeConfig({
10+
primaryContextBackend: 'qwen',
11+
})).toEqual({
12+
primaryContextBackend: 'qwen',
13+
primaryContextModel: getDefaultSharedContextModelForBackend('qwen'),
14+
backupContextBackend: undefined,
15+
backupContextModel: undefined,
16+
});
17+
});
18+
19+
it('replaces incompatible saved models with the backend default', () => {
20+
expect(normalizeSharedContextRuntimeConfig({
21+
primaryContextBackend: 'qwen',
22+
primaryContextModel: 'sonnet',
23+
backupContextBackend: 'codex-sdk',
24+
backupContextModel: 'haiku',
25+
})).toEqual({
26+
primaryContextBackend: 'qwen',
27+
primaryContextModel: getDefaultSharedContextModelForBackend('qwen'),
28+
backupContextBackend: 'codex-sdk',
29+
backupContextModel: getDefaultSharedContextModelForBackend('codex-sdk'),
30+
});
31+
});
32+
});

web/src/components/SharedContextManagementPanel.tsx

Lines changed: 135 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { SharedContextRuntimeBackend } from '@shared/context-types.js';
66
import { QWEN_MODEL_IDS } from '@shared/qwen-models.js';
77
import {
88
DEFAULT_PRIMARY_CONTEXT_BACKEND,
9+
getDefaultSharedContextModelForBackend,
10+
isKnownSharedContextModelForBackend,
911
SHARED_CONTEXT_RUNTIME_BACKENDS,
1012
type SharedContextRuntimeConfigSnapshot,
1113
} from '@shared/shared-context-runtime-config.js';
@@ -140,6 +142,45 @@ const fieldInputStyle = {
140142
width: '100%',
141143
} as const;
142144

145+
const processingGridStyle = {
146+
display: 'grid',
147+
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
148+
gap: 12,
149+
alignItems: 'start',
150+
} as const;
151+
152+
const processingCardStyle = {
153+
border: '1px solid #334155',
154+
borderRadius: 12,
155+
padding: 12,
156+
background: '#0f172a',
157+
display: 'flex',
158+
flexDirection: 'column',
159+
gap: 10,
160+
} as const;
161+
162+
const backendChipRowStyle = {
163+
display: 'flex',
164+
gap: 8,
165+
flexWrap: 'wrap',
166+
} as const;
167+
168+
function processingChipStyle(active: boolean) {
169+
return active
170+
? {
171+
...buttonStyle,
172+
padding: '6px 10px',
173+
fontSize: 12,
174+
fontWeight: 700,
175+
}
176+
: {
177+
...subtleButtonStyle,
178+
padding: '6px 10px',
179+
fontSize: 12,
180+
fontWeight: 600,
181+
};
182+
}
183+
143184
const defaultPolicyState: SharedProjectPolicy = {
144185
enrollmentId: '',
145186
enterpriseId: '',
@@ -162,6 +203,22 @@ const PROCESSING_MODEL_OPTIONS_BY_BACKEND: Record<SharedContextRuntimeBackend, r
162203
openclaw: [],
163204
};
164205

206+
function resolveProcessingModelForBackend(
207+
nextBackend: SharedContextRuntimeBackend,
208+
currentModel: string,
209+
previousBackend?: SharedContextRuntimeBackend,
210+
): string {
211+
const trimmed = currentModel.trim();
212+
if (!trimmed) return getDefaultSharedContextModelForBackend(nextBackend);
213+
if (previousBackend && trimmed === getDefaultSharedContextModelForBackend(previousBackend)) {
214+
return getDefaultSharedContextModelForBackend(nextBackend);
215+
}
216+
if (!isKnownSharedContextModelForBackend(nextBackend, trimmed)) {
217+
return getDefaultSharedContextModelForBackend(nextBackend);
218+
}
219+
return trimmed;
220+
}
221+
165222
type KindOption = SharedDocument['kind'];
166223
type ManagementTab = 'enterprise' | 'members' | 'projects' | 'knowledge' | 'processing';
167224

@@ -417,6 +474,23 @@ export function SharedContextManagementPanel({ enterpriseId: initialEnterpriseId
417474
void reloadProcessingConfig();
418475
}, [activeTab, reloadProcessingConfig]);
419476

477+
const handleProcessingPrimaryBackendChange = useCallback((nextBackend: SharedContextRuntimeBackend) => {
478+
setProcessingPrimaryBackend((prevBackend) => {
479+
setProcessingPrimaryModel((prevModel) => resolveProcessingModelForBackend(nextBackend, prevModel, prevBackend));
480+
return nextBackend;
481+
});
482+
}, []);
483+
484+
const handleProcessingBackupBackendChange = useCallback((nextBackend: SharedContextRuntimeBackend) => {
485+
setProcessingBackupBackend((prevBackend) => {
486+
setProcessingBackupModel((prevModel) => {
487+
if (!prevModel.trim()) return '';
488+
return resolveProcessingModelForBackend(nextBackend, prevModel, prevBackend);
489+
});
490+
return nextBackend;
491+
});
492+
}, []);
493+
420494
return (
421495
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 8, color: '#e2e8f0', overflow: 'auto' }}>
422496
<div style={sectionStyle}>
@@ -848,51 +922,67 @@ export function SharedContextManagementPanel({ enterpriseId: initialEnterpriseId
848922
<InfoCard title={t('sharedContext.management.processingModelTitle')}>
849923
{serverId ? (
850924
<>
851-
<div style={rowStyle}>
852-
<label style={{ ...fieldLabelStyle, flex: '1 1 220px' }}>
853-
<span>{t('sharedContext.management.processingPrimaryBackend')}</span>
854-
<select
855-
value={processingPrimaryBackend}
856-
onChange={(e) => setProcessingPrimaryBackend((e.currentTarget as HTMLSelectElement).value as SharedContextRuntimeBackend)}
857-
style={fieldInputStyle}
858-
>
859-
{SHARED_CONTEXT_RUNTIME_BACKENDS.map((backend) => (
860-
<option key={backend} value={backend}>{backend}</option>
861-
))}
862-
</select>
863-
</label>
864-
<label style={{ ...fieldLabelStyle, flex: '1 1 220px' }}>
865-
<span>{t('sharedContext.management.processingPrimaryModel')}</span>
866-
<input
867-
list={`shared-context-model-options-${processingPrimaryBackend}`}
868-
value={processingPrimaryModel}
869-
onInput={(e) => setProcessingPrimaryModel((e.currentTarget as HTMLInputElement).value)}
870-
placeholder={DEFAULT_PRIMARY_CONTEXT_MODEL}
871-
style={fieldInputStyle}
872-
/>
873-
</label>
874-
<label style={{ ...fieldLabelStyle, flex: '1 1 220px' }}>
875-
<span>{t('sharedContext.management.processingBackupBackend')}</span>
876-
<select
877-
value={processingBackupBackend}
878-
onChange={(e) => setProcessingBackupBackend((e.currentTarget as HTMLSelectElement).value as SharedContextRuntimeBackend)}
879-
style={fieldInputStyle}
880-
>
881-
{SHARED_CONTEXT_RUNTIME_BACKENDS.map((backend) => (
882-
<option key={backend} value={backend}>{backend}</option>
883-
))}
884-
</select>
885-
</label>
886-
<label style={{ ...fieldLabelStyle, flex: '1 1 220px' }}>
887-
<span>{t('sharedContext.management.processingBackupModel')}</span>
888-
<input
889-
list={`shared-context-model-options-${processingBackupBackend}`}
890-
value={processingBackupModel}
891-
onInput={(e) => setProcessingBackupModel((e.currentTarget as HTMLInputElement).value)}
892-
placeholder={t('sharedContext.management.processingBackupPlaceholder')}
893-
style={fieldInputStyle}
894-
/>
895-
</label>
925+
<div style={processingGridStyle}>
926+
<div style={processingCardStyle}>
927+
<strong>{t('sharedContext.management.processingPrimaryCardTitle')}</strong>
928+
<label style={fieldLabelStyle}>
929+
<span>{t('sharedContext.management.processingPrimaryBackend')}</span>
930+
<div style={backendChipRowStyle}>
931+
{SHARED_CONTEXT_RUNTIME_BACKENDS.map((backend) => (
932+
<button
933+
key={`primary:${backend}`}
934+
type="button"
935+
aria-label={`${t('sharedContext.management.processingPrimaryBackend')}: ${backend}`}
936+
style={processingChipStyle(processingPrimaryBackend === backend)}
937+
onClick={() => handleProcessingPrimaryBackendChange(backend)}
938+
>
939+
{backend}
940+
</button>
941+
))}
942+
</div>
943+
</label>
944+
<label style={fieldLabelStyle}>
945+
<span>{t('sharedContext.management.processingPrimaryModel')}</span>
946+
<input
947+
aria-label={t('sharedContext.management.processingPrimaryModel')}
948+
list={`shared-context-model-options-${processingPrimaryBackend}`}
949+
value={processingPrimaryModel}
950+
onInput={(e) => setProcessingPrimaryModel((e.currentTarget as HTMLInputElement).value)}
951+
placeholder={DEFAULT_PRIMARY_CONTEXT_MODEL}
952+
style={fieldInputStyle}
953+
/>
954+
</label>
955+
</div>
956+
<div style={processingCardStyle}>
957+
<strong>{t('sharedContext.management.processingBackupCardTitle')}</strong>
958+
<label style={fieldLabelStyle}>
959+
<span>{t('sharedContext.management.processingBackupBackend')}</span>
960+
<div style={backendChipRowStyle}>
961+
{SHARED_CONTEXT_RUNTIME_BACKENDS.map((backend) => (
962+
<button
963+
key={`backup:${backend}`}
964+
type="button"
965+
aria-label={`${t('sharedContext.management.processingBackupBackend')}: ${backend}`}
966+
style={processingChipStyle(processingBackupBackend === backend)}
967+
onClick={() => handleProcessingBackupBackendChange(backend)}
968+
>
969+
{backend}
970+
</button>
971+
))}
972+
</div>
973+
</label>
974+
<label style={fieldLabelStyle}>
975+
<span>{t('sharedContext.management.processingBackupModel')}</span>
976+
<input
977+
aria-label={t('sharedContext.management.processingBackupModel')}
978+
list={`shared-context-model-options-${processingBackupBackend}`}
979+
value={processingBackupModel}
980+
onInput={(e) => setProcessingBackupModel((e.currentTarget as HTMLInputElement).value)}
981+
placeholder={t('sharedContext.management.processingBackupPlaceholder')}
982+
style={fieldInputStyle}
983+
/>
984+
</label>
985+
</div>
896986
</div>
897987
{SHARED_CONTEXT_RUNTIME_BACKENDS.map((backend) => (
898988
<datalist id={`shared-context-model-options-${backend}`} key={backend}>

web/src/i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,14 +905,17 @@
905905
"policyExplainLine2": "Use the stricter options for team-owned codebases. Use the more permissive options only when you need continuity over strict consistency.",
906906
"allowDegradedHelp": "Allow providers that can only inject shared context in a reduced or message-side form. Turn this off if the team requires consistent behavior across providers.",
907907
"allowLocalFallbackHelp": "Allow this machine to keep using its local processed context when shared remote context is stale or temporarily unavailable.",
908+
"requireFullSupportHelp": "Only allow providers that fully support normalized shared-context injection. This is the strictest setting.",
908909
"requireFullProviderSupportHelp": "Only allow providers that fully support normalized shared-context injection. This is the strictest setting.",
909910
"processingSummaryTitle": "Processing flow",
910911
"processingSummaryLine1": "Raw activity is staged locally, then materialized into processed local context on delayed jobs.",
911912
"processingSummaryLine2": "Shared projects can replicate processed context to the remote shared store after local materialization succeeds.",
912913
"processingSummaryLine3": "This panel manages enterprise policy and authored knowledge. It does not directly run the materializer.",
913914
"processingModelTitle": "Materialization model selection",
915+
"processingPrimaryCardTitle": "Primary processing path",
914916
"processingPrimaryBackend": "Primary SDK / backend",
915917
"processingPrimaryModel": "Default primary model",
918+
"processingBackupCardTitle": "Backup processing path",
916919
"processingBackupBackend": "Backup SDK / backend",
917920
"processingBackupModel": "Backup model",
918921
"processingBackupPlaceholder": "Optional fallback model",

0 commit comments

Comments
 (0)