Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions scripts/v3-agent-profiles/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,47 @@ for (const invalid of ['', '0', '-1', '1.5', 'Infinity', '9007199254740992']) {
assert.throws(() => parseModelLimits({ context: invalid, input: '', output: '8192' }));
}
console.log('Token limit inheritance, optional input, required context/output and safe integer checks passed');

const previewSource = await readFile(resolve(projectRoot, 'src/components/agent-profiles/effectiveConfig.ts'), 'utf8');
const previewCode = ts.transpileModule(previewSource, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020 } }).outputText;
const { knownConfiguration, launchCommand } = await import(`data:text/javascript;base64,${Buffer.from(previewCode).toString('base64')}`);
const previewProfile = {
agent: 'opencode', display_name: 'Demo', source: { path: { native: "/tmp/A B/quote'$(printf injected).jsonc", platform: 'linux' }, scope: 'user' }, default_state: {},
managed: { default_provider_id: 'wrong', default_model_id: 'p/org/m', providers: [
{ provider_id: 'wrong', models: [], protocol: { native_protocol: 'unknown' } },
{ provider_id: 'p', credential: { secret: 'SECRET_SENTINEL' }, protocol: { native_protocol: 'anthropic_messages' }, models: [{ model_id: 'org/m',
thinking: { reasoning_effort: 'low', thinking_mode: null, thinking_budget: null, selected: 'high', variant_values: { high: { effort: 'high', apiKey: 'SECRET_SENTINEL', thinking: { type: 'adaptive' } } } },
modalities: { input: ['text', 'image'], output: ['text'] }, limits: { context: 128000, input: null, output: 8192 }
}] }
] }
};
const preview = knownConfiguration(previewProfile);
assert.equal(preview.find(r => r.key === 'provider').value, 'p');
assert.deepEqual(preview.find(r => r.key === 'reasoningEffort'), { key: 'reasoningEffort', value: 'high', source: 'variant', variant: 'high', overridden: 'low' });
assert.equal(preview.find(r => r.key === 'thinkingMode').source, 'variant');
assert.equal(preview.find(r => r.key === 'inputLimit').source, 'inherited');
assert.ok(!JSON.stringify(preview).includes('SECRET_SENTINEL'));
previewProfile.managed.providers[1].models[0].thinking.variant_values.high.disabled = true;
assert.equal(knownConfiguration(previewProfile).find(r => r.key === 'reasoningEffort').value, 'low');
const { mkdtempSync, writeFileSync, chmodSync, rmSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const { execFileSync } = await import('node:child_process');
const probeDir = mkdtempSync(resolve(tmpdir(), 'vibehub-command-'));
try {
const probe = resolve(probeDir, 'opencode');
writeFileSync(probe, '#!/bin/sh\nprintf \'%s\' "$OPENCODE_CONFIG"\n'); chmodSync(probe, 0o755);
const command = launchCommand(previewProfile, null).command;
const output = execFileSync('/bin/sh', ['-c', command], { env: { ...process.env, PATH: probeDir + ':' + process.env.PATH }, encoding: 'utf8' });
assert.equal(output, previewProfile.source.path.native, 'quoted POSIX path is passed literally to the child');
} finally { rmSync(probeDir, { recursive: true }); }
const windowsProfile = structuredClone(previewProfile);
windowsProfile.source.path = { native: "C:\\Users\\A B\\it's.jsonc", platform: 'windows' };
const windowsCommand = launchCommand(windowsProfile, { kind: 'host', platform: 'windows' });
assert.equal(windowsCommand.shell, 'powershell');
assert.ok(windowsCommand.command.includes("it''s.jsonc"));
assert.ok(windowsCommand.command.includes('finally { $env:OPENCODE_CONFIG = $previousOpenCodeConfig }'));
windowsProfile.source.path.native = '\\\\wsl.localhost\\Ubuntu\\home\\Case User\\opencode.jsonc';
const wslCommand = launchCommand(windowsProfile, { kind: 'wsl', platform: 'linux', distribution: 'Ubuntu' });
assert.ok(wslCommand.command.includes("'OPENCODE_CONFIG=/home/Case User/opencode.jsonc'"));
assert.equal(launchCommand(windowsProfile, { kind: 'wsl', platform: 'linux', distribution: 'Debian' }).command, null);
console.log('Preview source/override/credential exclusion and Linux child execution, Windows quoting and WSL path checks passed');
27 changes: 6 additions & 21 deletions src/components/agent-profiles/AgentProfilesPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { EffectiveConfigPreview } from './EffectiveConfigPreview';
import { launchCommand } from './effectiveConfig';
import { ModelCapabilitySummary } from './ModelCapabilitySummary';
import { ModelLimitsEditor } from './ModelLimitsEditor';
import { parseModelLimits, type ModelLimitDraft } from './modelLimits';
Expand Down Expand Up @@ -285,26 +287,6 @@ function statusTone(compatibility: string): string {
return 'border-destructive/30 bg-destructive/10 text-destructive';
}

function computeLaunchCommand(profile: AgentProfileDocument): string {
if (profile.agent === 'opencode') {
return 'opencode';
}
if (profile.agent === 'claude_code') {
if (profile.source.scope === 'user') {
return 'claude';
}
return `claude --setting-sources "" --settings "${profile.source.path.native}"`;
}
if (profile.agent === 'codex') {
if (profile.default_state.is_default || profile.source.scope === 'user') {
return 'codex';
}
const profileName = profile.source.profile_name || profile.display_name;
return `codex --profile-v2 "${profileName}"`;
}
return profile.launch.executable || 'agent';
}

function defaultProtocol(agent: AgentKind): ProtocolCapability {
const protocol = agent === 'claude_code' ? 'anthropic_messages' : 'openai_responses';
return {
Expand Down Expand Up @@ -685,7 +667,8 @@ export function AgentProfilesPanel() {

const copyLaunchCommand = async () => {
if (!profileForEdit) return;
const command = computeLaunchCommand(profileForEdit);
const command = launchCommand(profileForEdit, selectedTarget).command;
if (!command) { setNotice({ kind: 'error', text: t('agentProfiles.preview.runtimePathMismatch') }); return; }
try {
await navigator.clipboard.writeText(command);
setNotice({ kind: 'info', text: t('agentProfiles.notices.commandCopied', { command }) });
Expand Down Expand Up @@ -1265,6 +1248,8 @@ export function AgentProfilesPanel() {
<span>{t('agentProfiles.models.count', { count: profileForEdit.managed.providers.reduce((count, provider) => count + provider.models.length, 0) })}</span>
</div>

<EffectiveConfigPreview profile={profileForEdit} saved={profile} target={selectedTarget} dirty={dirty} />

<section className="mt-5" aria-labelledby="providers-title">
<div className="flex items-end justify-between gap-3 px-1 pb-3 sm:px-2"><div><h3 id="providers-title" className="text-base font-semibold">{t('agentProfiles.providers.title')}</h3><p className="mt-1 text-xs text-muted-foreground">{t('agentProfiles.providers.description')}</p></div><Button type="button" variant="outline" size="sm" onClick={() => openProviderEditor()} disabled={!canCreateProvider || busyAction !== null}><Plus className="mr-1.5 h-3.5 w-3.5" />{t('agentProfiles.providers.add')}</Button></div>
{profileForEdit.managed.providers.length === 0 && <div className="py-8 text-sm text-muted-foreground">{t('agentProfiles.providers.empty')}</div>}
Expand Down
28 changes: 28 additions & 0 deletions src/components/agent-profiles/EffectiveConfigPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useTranslation } from 'react-i18next';
import type { AgentProfileDocument, RuntimeTarget } from '@/v3/contracts/generated/agent-profile';
import { knownConfiguration, launchCommand } from './effectiveConfig';

export function EffectiveConfigPreview({ profile, saved, target, dirty }: { profile: AgentProfileDocument; saved: AgentProfileDocument | null; target: RuntimeTarget | null; dirty: boolean }) {
const { t } = useTranslation();
const rows = knownConfiguration(profile);
const savedRows = saved ? knownConfiguration(saved) : [];
const launch = launchCommand(profile, target);
const display = (value: unknown) => value === null || value === undefined ? t('agentProfiles.preview.inherited') : typeof value === 'string' ? value : JSON.stringify(value);
return <details className="mt-4 rounded-lg border p-4">
<summary className="cursor-pointer text-sm font-medium">{t('agentProfiles.preview.title')}{dirty ? ` · ${t('agentProfiles.preview.draft')}` : ''}</summary>
<p className="mt-3 text-xs text-muted-foreground">{t('agentProfiles.preview.scope')}</p>
<p className="mt-2 break-all font-mono text-xs">{profile.source.path.display}</p>
<div className="mt-3 overflow-x-auto"><table className="w-full text-left text-xs"><thead><tr>{['field', 'value', 'source'].map(key => <th key={key} className="border-b p-2">{t(`agentProfiles.preview.${key}`)}</th>)}</tr></thead><tbody>
{rows.map(row => {
const changed = dirty && JSON.stringify(row) !== JSON.stringify(savedRows.find(savedRow => savedRow.key === row.key));
return <tr key={row.key}><td className="border-b p-2">{t(`agentProfiles.preview.fields.${row.key}`)}{changed && <span className="ml-1 text-amber-600">{t('agentProfiles.preview.draft')}</span>}</td>
<td className="max-w-80 break-all border-b p-2 font-mono">{display(row.value)}{row.source === 'variant' && <div className="mt-1 text-muted-foreground">{t('agentProfiles.preview.overrides', { value: display(row.overridden) })}</div>}</td>
<td className="border-b p-2">{t(`agentProfiles.preview.origins.${row.source}`)}{row.variant ? `: ${row.variant}` : ''}</td></tr>;
})}
</tbody></table></div>
{profile.agent === 'opencode' && <p className="mt-3 text-xs text-muted-foreground">{t('agentProfiles.preview.layers')}</p>}
<p className="mt-3 text-xs font-medium">{t('agentProfiles.preview.launch', { shell: launch.shell === 'powershell' ? 'PowerShell' : 'sh / bash / zsh' })}</p>
<pre className="mt-1 select-text overflow-auto whitespace-pre-wrap break-all rounded bg-muted/40 p-2 text-xs">{launch.command || t('agentProfiles.preview.runtimePathMismatch')}</pre>
{dirty && <p className="mt-2 text-xs text-amber-600">{t('agentProfiles.preview.saveFirst')}</p>}
</details>;
}
69 changes: 69 additions & 0 deletions src/components/agent-profiles/effectiveConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { AgentProfileDocument, RuntimeTarget } from '@/v3/contracts/generated/agent-profile';

export type PreviewRow = { key: string; value: unknown; source: 'file' | 'model' | 'variant' | 'inherited'; variant?: string; overridden?: unknown };
const record = (value: unknown): Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};

export function knownConfiguration(profile: AgentProfileDocument): PreviewRow[] {
const managed = profile.managed;
const selected = managed.default_model_id;
const provider = profile.agent === 'opencode'
? managed.providers.find(p => p.provider_id === selected?.split('/')[0])
: managed.providers.find(p => p.provider_id === managed.default_provider_id);
const model = provider?.models.find(m => selected === m.model_id || selected === `${provider.provider_id}/${m.model_id}`);
const rows: PreviewRow[] = [];
const add = (key: string, value: unknown, source: PreviewRow['source'] = 'model') => rows.push({ key, value: value ?? null, source: value == null ? 'inherited' : source });
add('provider', provider?.provider_id, 'file');
add('model', selected, 'file');
add('protocol', provider?.protocol.native_protocol, 'file');
add('smallModel', managed.small_model_id, 'file');
add('variant', profile.agent === 'opencode' ? model?.thinking.selected : null, 'file');
add('reasoningEffort', profile.agent === 'opencode' ? model?.thinking.reasoning_effort : model?.thinking.selected);
add('thinkingMode', model?.thinking.thinking_mode);
add('thinkingBudget', model?.thinking.thinking_budget);
add('inputModalities', model?.modalities?.input);
add('outputModalities', model?.modalities?.output);
add('context', model?.limits?.context);
add('inputLimit', model?.limits?.input);
add('outputLimit', model?.limits?.output);
// Preview only managed, non-credential values. Never spread provider options,
// credentials or arbitrary variant keys into the preview.
const variantName = profile.agent === 'opencode' ? model?.thinking.selected : null;
const variant = record(variantName ? model?.thinking.variant_values?.[variantName] : null);
if (variantName && variant.disabled !== true) {
const thinking = record(variant.thinking);
const effortKey = provider?.protocol.native_protocol === 'anthropic_messages' ? 'effort' : 'reasoningEffort';
for (const [key, value] of [['reasoningEffort', variant[effortKey]], ['thinkingMode', thinking.type], ['thinkingBudget', thinking.budgetTokens]] as const) {
if (key === 'thinkingBudget' ? typeof value !== 'number' : typeof value !== 'string') continue;
const row = rows.find(row => row.key === key)!;
row.overridden = row.value; row.value = value; row.source = 'variant'; row.variant = variantName;
}
}
return rows;
}

export function launchCommand(profile: AgentProfileDocument, target: RuntimeTarget | null): { command: string | null; shell: 'powershell' | 'posix'; error?: string } {
const wsl = target?.kind === 'wsl';
const windows = wsl || (target?.platform ?? profile.source.path.platform) === 'windows';
const shell = windows ? 'powershell' : 'posix';
const quote = (value: string) => windows ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\\''")}'`;
let path = profile.source.path.native;
if (wsl) {
const unc = path.replace(/^\\\\\?\\UNC\\/i, '\\\\').match(/^\\\\(?:wsl\$|wsl\.localhost)\\([^\\]+)(\\.*)?$/i);
if (unc && unc[1].toLowerCase() === target.distribution?.toLowerCase()) path = (unc[2] || '/').replace(/\\/g, '/');
else if (!path.startsWith('/') || path.startsWith('//')) return { command: null, shell, error: 'runtimePathMismatch' };
if (!target.distribution) return { command: null, shell, error: 'runtimePathMismatch' };
}
const executable = profile.agent === 'opencode' ? 'opencode' : profile.agent === 'claude_code' ? 'claude' : 'codex';
const args = profile.agent === 'claude_code' && profile.source.scope !== 'user' ? ['--setting-sources', '', '--settings', path]
: profile.agent === 'codex' && !profile.default_state.is_default && profile.source.scope !== 'user' ? ['--profile-v2', profile.source.profile_name || profile.display_name] : [];
if (wsl) {
const command = ['wsl.exe', '--distribution', target.distribution!, '--exec', ...(profile.agent === 'opencode' ? ['env', `OPENCODE_CONFIG=${path}`] : []), executable, ...args];
return { command: '& ' + command.map(quote).join(' '), shell };
}
if (profile.agent === 'opencode') {
return { command: windows
? `& { $previousOpenCodeConfig = $env:OPENCODE_CONFIG; try { $env:OPENCODE_CONFIG = ${quote(path)}; & 'opencode' } finally { $env:OPENCODE_CONFIG = $previousOpenCodeConfig } }`
: `env ${quote(`OPENCODE_CONFIG=${path}`)} opencode`, shell };
}
return { command: (windows ? '& ' : '') + [executable, ...args].map(quote).join(' '), shell };
}
35 changes: 35 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,41 @@
"yes": "Supported",
"no": "Unsupported",
"hint": "Declarations are not an API test. Missing streaming/usage metadata prevents a complete compatibility claim."
},
"preview": {
"title": "Configuration values and sources",
"draft": "Unsaved",
"scope": "Known managed values in the selected file, with selected variant overrides. This is a partial preview, not the complete merged runtime configuration. Credentials are excluded.",
"inherited": "Inherited / source not read",
"field": "Setting",
"value": "Value",
"source": "Source",
"overrides": "Overrides: {{value}}",
"layers": "Other layers not evaluated: remote defaults → global configuration → selected OPENCODE_CONFIG → project configuration → configuration directories → OPENCODE_CONFIG_CONTENT → managed policy. Later layers may override these values.",
"launch": "Launch command ({{shell}})",
"saveFirst": "Save before running this command; terminal commands read the saved file.",
"runtimePathMismatch": "The configuration path cannot be mapped to the selected runtime.",
"origins": {
"file": "Selected file",
"model": "Model configuration",
"variant": "Selected variant",
"inherited": "External defaults (unknown)"
},
"fields": {
"provider": "Provider",
"model": "Default model",
"protocol": "Protocol",
"smallModel": "Small model",
"variant": "Default variant",
"reasoningEffort": "Reasoning effort",
"thinkingMode": "Thinking mode",
"thinkingBudget": "Thinking budget",
"inputModalities": "Input types",
"outputModalities": "Output types",
"context": "Context window",
"inputLimit": "Maximum input",
"outputLimit": "Maximum output"
}
}
},
"update": {
Expand Down
35 changes: 35 additions & 0 deletions src/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -1883,6 +1883,41 @@
"yes": "支援",
"no": "不支援",
"hint": "宣告不等於介面實測。串流輸出和用量資訊缺少能力中繼資料時,不會標記為完全相容。"
},
"preview": {
"title": "設定值與來源預覽",
"draft": "未儲存",
"scope": "顯示所選檔案中已讀取的受管理設定及所選變體覆寫。這是部分預覽,不代表完整合併後的執行設定;憑證不會顯示。",
"inherited": "繼承 / 來源未讀取",
"field": "設定項目",
"value": "值",
"source": "來源",
"overrides": "覆寫:{{value}}",
"layers": "尚未計算的其他層:遠端預設設定 → 全域設定 → 所選 OPENCODE_CONFIG → 專案設定 → 設定目錄 → OPENCODE_CONFIG_CONTENT → 管理原則。後續層可能覆寫目前值。",
"launch": "啟動命令({{shell}})",
"saveFirst": "執行命令前請先儲存;終端讀取的是已儲存檔案。",
"runtimePathMismatch": "無法將設定路徑對應到所選執行環境。",
"origins": {
"file": "所選設定檔案",
"model": "模型設定",
"variant": "所選變體",
"inherited": "外部預設值(未知)"
},
"fields": {
"provider": "供應商",
"model": "預設模型",
"protocol": "協定",
"smallModel": "小模型",
"variant": "預設變體",
"reasoningEffort": "思考強度",
"thinkingMode": "思考模式",
"thinkingBudget": "思考預算",
"inputModalities": "輸入類型",
"outputModalities": "輸出類型",
"context": "上下文視窗",
"inputLimit": "最大輸入",
"outputLimit": "最大輸出"
}
}
},
"update": {
Expand Down
Loading
Loading