Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
71 changes: 42 additions & 29 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -10017,6 +10017,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
result = {
provider: config.model_provider || '未设置',
model: config.model || '未设置',
currentModels: readCurrentModels(),
serviceTier,
modelReasoningEffort,
modelContextWindow,
Expand Down Expand Up @@ -12360,35 +12361,47 @@ function buildMcpProviderListPayload() {
configReady: !listConfigResult.isVirtual,
configErrorType: listConfigResult.errorType || '',
configNotice: listConfigResult.reason || '',
providers: Object.entries(providers).map(([name, p]) => ({
name,
url: p.base_url || '',
key: maskKey(p.preferred_auth_method || ''),
hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
models: Array.isArray(p.models)
? p.models
.filter((model) => model && typeof model === 'object' && !Array.isArray(model))
.map((model) => ({
id: typeof model.id === 'string' ? model.id : '',
name: typeof model.name === 'string' ? model.name : '',
cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
? {
input: model.cost.input,
output: model.cost.output,
cacheRead: model.cost.cacheRead,
cacheWrite: model.cost.cacheWrite
}
: null,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens
}))
.filter((model) => model.id)
: [],
current: name === current,
readOnly: isBuiltinManagedProvider(name),
nonDeletable: isNonDeletableProvider(name),
nonEditable: isNonEditableProvider(name)
}))
providers: Object.entries(providers).map(([name, p]) => {
const bridge = typeof p.codexmate_bridge === 'string' ? p.codexmate_bridge.trim() : '';
let upstreamUrl = '';
if (bridge === 'openai') {
const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
if (upstream && !upstream.error && typeof upstream.baseUrl === 'string') {
upstreamUrl = upstream.baseUrl.trim();
}
}
return {
name,
url: p.base_url || '',
upstreamUrl,
codexmate_bridge: bridge,
key: maskKey(p.preferred_auth_method || ''),
hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
models: Array.isArray(p.models)
? p.models
.filter((model) => model && typeof model === 'object' && !Array.isArray(model))
.map((model) => ({
id: typeof model.id === 'string' ? model.id : '',
name: typeof model.name === 'string' ? model.name : '',
cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
? {
input: model.cost.input,
output: model.cost.output,
cacheRead: model.cost.cacheRead,
cacheWrite: model.cost.cacheWrite
}
: null,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens
}))
.filter((model) => model.id)
: [],
current: name === current,
readOnly: isBuiltinManagedProvider(name),
nonDeletable: isNonDeletableProvider(name),
nonEditable: isNonEditableProvider(name)
};
})
Comment on lines +12364 to +12404

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Support legacy bridge detection when computing upstreamUrl.

At Line [12367], upstream resolution only runs when codexmate_bridge === 'openai'. Legacy transform providers that only have bridge-form base_url won’t expose upstreamUrl, so the new UI indicator can disappear for existing users.

🔧 Proposed fix
-        providers: Object.entries(providers).map(([name, p]) => {
-            const bridge = typeof p.codexmate_bridge === 'string' ? p.codexmate_bridge.trim() : '';
+        providers: Object.entries(providers).map(([name, p]) => {
+            const bridge = typeof p.codexmate_bridge === 'string' ? p.codexmate_bridge.trim() : '';
+            const isOpenaiBridgeProvider = bridge === 'openai'
+                || (typeof p.base_url === 'string' && p.base_url.includes('/bridge/openai/'));
             let upstreamUrl = '';
-            if (bridge === 'openai') {
+            if (isOpenaiBridgeProvider) {
                 const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
                 if (upstream && !upstream.error && typeof upstream.baseUrl === 'string') {
                     upstreamUrl = upstream.baseUrl.trim();
                 }
             }
             return {
                 name,
                 url: p.base_url || '',
                 upstreamUrl,
-                codexmate_bridge: bridge,
+                codexmate_bridge: bridge || (isOpenaiBridgeProvider ? 'openai' : ''),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
providers: Object.entries(providers).map(([name, p]) => {
const bridge = typeof p.codexmate_bridge === 'string' ? p.codexmate_bridge.trim() : '';
let upstreamUrl = '';
if (bridge === 'openai') {
const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
if (upstream && !upstream.error && typeof upstream.baseUrl === 'string') {
upstreamUrl = upstream.baseUrl.trim();
}
}
return {
name,
url: p.base_url || '',
upstreamUrl,
codexmate_bridge: bridge,
key: maskKey(p.preferred_auth_method || ''),
hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
models: Array.isArray(p.models)
? p.models
.filter((model) => model && typeof model === 'object' && !Array.isArray(model))
.map((model) => ({
id: typeof model.id === 'string' ? model.id : '',
name: typeof model.name === 'string' ? model.name : '',
cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
? {
input: model.cost.input,
output: model.cost.output,
cacheRead: model.cost.cacheRead,
cacheWrite: model.cost.cacheWrite
}
: null,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens
}))
.filter((model) => model.id)
: [],
current: name === current,
readOnly: isBuiltinManagedProvider(name),
nonDeletable: isNonDeletableProvider(name),
nonEditable: isNonEditableProvider(name)
};
})
providers: Object.entries(providers).map(([name, p]) => {
const bridge = typeof p.codexmate_bridge === 'string' ? p.codexmate_bridge.trim() : '';
const isOpenaiBridgeProvider = bridge === 'openai'
|| (typeof p.base_url === 'string' && p.base_url.includes('/bridge/openai/'));
let upstreamUrl = '';
if (isOpenaiBridgeProvider) {
const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
if (upstream && !upstream.error && typeof upstream.baseUrl === 'string') {
upstreamUrl = upstream.baseUrl.trim();
}
}
return {
name,
url: p.base_url || '',
upstreamUrl,
codexmate_bridge: bridge || (isOpenaiBridgeProvider ? 'openai' : ''),
key: maskKey(p.preferred_auth_method || ''),
hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
models: Array.isArray(p.models)
? p.models
.filter((model) => model && typeof model === 'object' && !Array.isArray(model))
.map((model) => ({
id: typeof model.id === 'string' ? model.id : '',
name: typeof model.name === 'string' ? model.name : '',
cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
? {
input: model.cost.input,
output: model.cost.output,
cacheRead: model.cost.cacheRead,
cacheWrite: model.cost.cacheWrite
}
: null,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens
}))
.filter((model) => model.id)
: [],
current: name === current,
readOnly: isBuiltinManagedProvider(name),
nonDeletable: isNonDeletableProvider(name),
nonEditable: isNonEditableProvider(name)
};
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 12364 - 12404, The upstreamUrl is only computed when
codexmate_bridge === 'openai', so legacy providers that only expose a
bridge-style base_url never get an upstreamUrl; update the conditional that
calls resolveOpenaiBridgeUpstream to also detect legacy bridge-form base_url
values (e.g., when p.base_url is a non-empty string that looks like a bridge
URL) and call resolveOpenaiBridgeUpstream in that case. In practice, modify the
branch around resolveOpenaiBridgeUpstream (referencing
OPENAI_BRIDGE_SETTINGS_FILE, resolveOpenaiBridgeUpstream, p.base_url, bridge and
upstreamUrl) to run when bridge === 'openai' OR when p.base_url indicates a
legacy bridge, then preserve the same upstream.error/baseUrl checks and trim
logic to set upstreamUrl.

};
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"test:ci": "node tools/ci/run-check.js all",
"test:unit": "node tests/unit/run.mjs",
"test:e2e": "node tests/e2e/run.js",
"setup:git": "git remote set-url origin https://github.com/SakuraByteCore/codexmate.git && gh auth setup-git",
"reset:dev": "node tools/dev/reset-and-dev.js",
"pretest": "node tools/ci/ensure-test-deps.js"
},
"dependencies": {
Expand Down
27 changes: 0 additions & 27 deletions plugins/prompt-templates/issue-inject/index.mjs

This file was deleted.

5 changes: 2 additions & 3 deletions plugins/prompt-templates/overview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from './storage.mjs';
import { buildBuiltinCommentPolishTemplate } from './comment-polish/index.mjs';
import { buildBuiltinRuleAckTemplate } from './rule-ack/index.mjs';
import { buildBuiltinIssueInjectTemplate } from './issue-inject/index.mjs';


function ensureBuiltinTemplates(rawList, builtins) {
const list = Array.isArray(rawList) ? rawList.filter(Boolean) : [];
Expand All @@ -33,8 +33,7 @@ export async function loadPromptTemplatesOverview(ctx, options = {}) {
const rawList = readPromptTemplatesFromStorage(localStorage);
const normalized = ensureBuiltinTemplates(rawList, [
buildBuiltinCommentPolishTemplate(t),
buildBuiltinRuleAckTemplate(t),
buildBuiltinIssueInjectTemplate(t)
buildBuiltinRuleAckTemplate(t)
]);
app.promptTemplatesListRaw = normalized;
persistPromptTemplatesToStorage(normalized, localStorage);
Expand Down
5 changes: 0 additions & 5 deletions plugins/prompt-templates/ownership.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ export const templateOwnershipById = {
templateId: 'builtin_rule_ack',
createdBy: 'ymkiux',
maintainers: ['ymkiux']
},
builtin_issue_inject: {
templateId: 'builtin_issue_inject',
createdBy: 'ymkiux',
maintainers: ['ymkiux']
}
};

2 changes: 1 addition & 1 deletion tests/e2e/test-web-ui-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module.exports = async function testWebUiAssets(ctx) {
/^text\/html\b/.test(String(bundledIndex.headers['content-type'] || '')),
'/web-ui/index.html should return html content type'
);
assert(bundledIndex.body.includes('id="settings-panel-trash"'), '/web-ui/index.html should inline settings partials');
assert(bundledIndex.body.includes('id="settings-panel-data"'), '/web-ui/index.html should inline settings partials');
assert(bundledIndex.body.includes('src="/web-ui/app.js"'), '/web-ui/index.html should point to the absolute app entry');
assert(
bundledIndex.body.includes('src="/res/vue.global.prod.js"'),
Expand Down
56 changes: 24 additions & 32 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,10 @@ test('config template keeps expected config tabs in top and side navigation', ()
/<button[^>]*@click="resetCodexContextBudgetDefaults"[^>]*>[\s\S]*?t\('config\.reset'\)[\s\S]*?<\/button>/
);
assert.match(html, /class="codex-config-grid"/);
assert.match(html, /onSettingsTabClick\('backup'\)/);
assert.match(html, /onSettingsTabClick\('trash'\)/);
assert.match(html, /onSettingsTabClick\('device'\)/);
assert.match(html, /settingsTab === 'backup'/);
assert.match(html, /settingsTab === 'trash'/);
assert.match(html, /settingsTab === 'device'/);
assert.match(html, /onSettingsTabClick\('general'\)/);
assert.match(html, /onSettingsTabClick\('data'\)/);
assert.match(html, /settingsTab === 'general'/);
assert.match(html, /settingsTab === 'data'/);
assert.match(html, /setConfigTemplateDiffConfirmEnabled/);
assert.match(html, /configTemplateDiffConfirmEnabled/);
assert.match(html, /sessionTrashCount/);
Expand Down Expand Up @@ -205,31 +203,25 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.doesNotMatch(html, /resetOnlineSkillsMarketSearch/);
assert.doesNotMatch(html, /class="market-online-list"/);
assert.doesNotMatch(html, /class="market-ecosystem-grid"/);
assert.match(html, /id="settings-tab-backup"/);
assert.match(html, /id="settings-tab-trash"/);
assert.match(html, /id="settings-tab-device"/);
assert.match(html, /id="settings-tab-general"/);
assert.match(html, /id="settings-tab-data"/);
assert.match(html, /role="tab"/);
assert.match(html, /aria-controls="settings-panel-backup"/);
assert.match(html, /aria-controls="settings-panel-trash"/);
assert.match(html, /aria-controls="settings-panel-device"/);
assert.match(html, /:aria-selected="settingsTab === 'backup'"/);
assert.match(html, /:aria-selected="settingsTab === 'trash'"/);
assert.match(html, /:aria-selected="settingsTab === 'device'"/);
assert.match(html, /id="settings-tab-backup"[\s\S]*:tabindex="settingsTab === 'backup' \? 0 : -1"/);
assert.match(html, /id="settings-tab-trash"[\s\S]*:tabindex="settingsTab === 'trash' \? 0 : -1"/);
assert.match(html, /id="settings-tab-device"[\s\S]*:tabindex="settingsTab === 'device' \? 0 : -1"/);
assert.match(html, /id="settings-panel-backup"/);
assert.match(html, /id="settings-panel-trash"/);
assert.match(html, /id="settings-panel-device"/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'backup'"[\s\S]*id="settings-panel-backup"[\s\S]*aria-labelledby="settings-tab-backup">/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'trash'"[\s\S]*id="settings-panel-trash"[\s\S]*aria-labelledby="settings-tab-trash">/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'device'"[\s\S]*id="settings-panel-device"[\s\S]*aria-labelledby="settings-tab-device">/);
assert.match(html, /id="settings-panel-backup"[\s\S]*?<div class="settings-card-title">\{\{\s*t\('settings\.sharePrefix\.title'\)\s*\}\}<\/div>/);
assert.match(html, /aria-controls="settings-panel-general"/);
assert.match(html, /aria-controls="settings-panel-data"/);
assert.match(html, /:aria-selected="settingsTab === 'general'"/);
assert.match(html, /:aria-selected="settingsTab === 'data'"/);
assert.match(html, /id="settings-tab-general"[\s\S]*:tabindex="settingsTab === 'general' \? 0 : -1"/);
assert.match(html, /id="settings-tab-data"[\s\S]*:tabindex="settingsTab === 'data' \? 0 : -1"/);
assert.match(html, /id="settings-panel-general"/);
assert.match(html, /id="settings-panel-data"/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'general'"[\s\S]*id="settings-panel-general"[\s\S]*aria-labelledby="settings-tab-general">/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'data'"[\s\S]*id="settings-panel-data"[\s\S]*aria-labelledby="settings-tab-data">/);
assert.match(html, /id="settings-panel-general"[\s\S]*?<div class="settings-card-title">\{\{\s*t\('settings\.sharePrefix\.title'\)\s*\}\}<\/div>/);
assert.match(html, /id="settings-share-prefix"[\s\S]*class="model-select"[\s\S]*:value="shareCommandPrefix"[\s\S]*@change="setShareCommandPrefix\(\$event\.target\.value\)"/);
assert.match(html, /<option value="npm start">npm start<\/option>/);
assert.match(html, /<option value="codexmate">codexmate<\/option>/);
assert.match(html, /id="settings-panel-device"[\s\S]*?<div class="settings-card-title">\{\{\s*t\('settings\.reset\.title'\)\s*\}\}<\/div>/);
assert.match(html, /id="settings-panel-device"[\s\S]*?@click="resetConfig"/);
assert.match(html, /id="settings-panel-data"[\s\S]*?<div class="settings-card-title">\{\{\s*t\('settings\.reset\.title'\)\s*\}\}<\/div>/);
assert.match(html, /id="settings-panel-data"[\s\S]*?@click="resetConfig"/);
assert.doesNotMatch(
html.match(/id="panel-config-provider"[\s\S]*?<\/template>/)?.[0] || '',
/<span class="selector-title">配置重置<\/span>/
Expand All @@ -242,7 +234,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(html, /<button class="btn-tool btn-tool-compact" @click="clearSessionTrash"/);
assert.doesNotMatch(html, /<span class="selector-title">会话回收站<\/span>/);
assert.match(html, /role="tabpanel"/);
assert.doesNotMatch(html, /v-if="settingsTab === 'backup'"/);
assert.doesNotMatch(html, /v-if="settingsTab === 'general'"/);
assert.match(html, /class="trash-item session-item session-card"/);
assert.match(html, /class="trash-item-mainline"/);
assert.match(html, /class="trash-item-side"/);
Expand Down Expand Up @@ -472,7 +464,7 @@ test('web ui script defines provider mode metadata for codex only', () => {
assert.match(appScript, /normalizePositiveIntegerInput\(/);
assert.match(constantsSource, /export const SESSION_TRASH_LIST_LIMIT = 500;/);
assert.match(constantsSource, /export const SESSION_TRASH_PAGE_SIZE = 200;/);
assert.match(appScript, /settingsTab:\s*'backup'/);
assert.match(appScript, /settingsTab:\s*'general'/);
assert.match(appScript, /skillsTargetApp:\s*'codex'/);
assert.match(appScript, /skillsMarketLoading:\s*false/);
assert.match(appScript, /skillsMarketLocalLoadedOnce:\s*false/);
Expand All @@ -492,7 +484,7 @@ test('web ui script defines provider mode metadata for codex only', () => {
assert.match(appScript, /sessionTrashHasMoreItems\(\)/);
assert.match(appScript, /sessionTrashHiddenCount\(\)/);
assert.match(appScript, /normalizeSettingsTab\(tab\)/);
assert.match(appScript, /tab === 'trash' \|\| tab === 'device'/);
assert.match(appScript, /tab === 'general' \|\| tab === 'data'/);
assert.match(appScript, /switchSettingsTab\(tab,\s*options = \{\}\)/);
assert.match(appScript, /loadSessionTrash\(options = \{\}\)/);
assert.match(appScript, /loadMoreSessionTrashItems\(\)/);
Expand Down Expand Up @@ -526,10 +518,10 @@ test('session helper deferred claude refresh validates live tab and mode before
assert.match(helperScript, /const expectedConfigMode = this\.configMode;/);
assert.match(helperScript, /if \(this\.mainTab !== expectedTab \|\| this\.configMode !== expectedConfigMode\) return;/);
assert.match(helperScript, /const shouldLoadTrashListOnSettingsEnter = nextTab === 'settings'/);
assert.match(helperScript, /this\.settingsTab === 'trash'/);
assert.match(helperScript, /this\.settingsTab === 'data'/);
assert.match(helperScript, /forceRefresh: !!this\.sessionTrashLoadedOnce/);
assert.match(helperScript, /const shouldPrimeTrashCountOnSettingsEnter = nextTab === 'settings'/);
assert.match(helperScript, /this\.settingsTab !== 'trash'/);
assert.match(helperScript, /this\.settingsTab !== 'data'/);
assert.match(helperScript, /this\.sessionTrashLoadedOnce = false;/);
assert.match(helperScript, /this\.loadSessionTrashCount\(\{ silent: true \}\);/);
assert.match(helperScript, /const shouldLoadSkillsMarketOnEnter = nextTab === 'market'/);
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/provider-share-command.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ test('status api case does not synthesize budget defaults after config load erro
}),
hasConfigLoadError,
readPositiveIntegerConfigValue,
readCurrentModels: () => ({}),
consumeInitNotice: () => ''
}
);
Expand All @@ -632,6 +633,7 @@ test('status api case does not synthesize budget defaults after config load erro
assert.strictEqual(result.modelAutoCompactTokenLimit, '');
assert.strictEqual(result.configErrorType, 'parse');
assert.strictEqual(result.configNotice, 'config.toml 解析失败');
assert.deepStrictEqual(result.currentModels, {});
});

test('status api case keeps lexical declarations scoped to the switch branch', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/providers-validation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test('addProvider normalizes trimmed values and submits sanitized payload', asyn
}
}]);
assert.strictEqual(context.showAddModal, false);
assert.deepStrictEqual(context.newProvider, { name: '', url: '', key: '', useTransform: false });
assert.deepStrictEqual(context.newProvider, { name: '', url: '', key: '', useTransform: false, _suggestedModel: '' });
assert.deepStrictEqual(loadAllCalls, ['loadAll']);
assert.strictEqual(messages.length, 1);
assert.deepStrictEqual(messages[0], {
Expand Down
28 changes: 10 additions & 18 deletions tests/unit/session-resume-command.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,37 @@ test('isResumeCommandAvailable supports codex and codebuddy with sessionId', ()
assert.strictEqual(methods.isResumeCommandAvailable({ source: 'claude', sessionId: '', filePath: '/home/user/.claude/projects/demo/sess-3.jsonl' }), true);
});

test('buildResumeCommand generates codex resume with optional --yolo, codebuddy -r, gemini -r, and claude -r', () => {
test('buildResumeCommand generates codex --yolo resume, codebuddy -r, gemini -r, and claude --dangerously-skip-permissions -r', () => {
const methods = createSessionActionMethods();
const contextBase = {
...methods,
sessionResumeWithYolo: false
};
const ctx = { ...methods, quoteShellArg: methods.quoteShellArg };

assert.strictEqual(
methods.buildResumeCommand.call(contextBase, { source: 'codex', sessionId: 'sess-1' }),
'codex resume sess-1'
);

assert.strictEqual(
methods.buildResumeCommand.call({ ...contextBase, sessionResumeWithYolo: true }, { source: 'codex', sessionId: 'sess-1' }),
methods.buildResumeCommand.call(ctx, { source: 'codex', sessionId: 'sess-1' }),
'codex --yolo resume sess-1'
);

assert.strictEqual(
methods.buildResumeCommand.call({ ...contextBase, sessionResumeWithYolo: true }, { source: 'codebuddy', sessionId: 'abc123' }),
methods.buildResumeCommand.call(ctx, { source: 'codebuddy', sessionId: 'abc123' }),
'codebuddy -r abc123'
);

assert.strictEqual(
methods.buildResumeCommand.call({ ...contextBase, sessionResumeWithYolo: true }, { source: 'gemini', sessionId: 'gm-123' }),
methods.buildResumeCommand.call(ctx, { source: 'gemini', sessionId: 'gm-123' }),
'gemini -r gm-123'
);

assert.strictEqual(
methods.buildResumeCommand.call({ ...contextBase, sessionResumeWithYolo: true }, { source: 'claude', sessionId: 'sess-2' }),
'claude -r sess-2'
methods.buildResumeCommand.call(ctx, { source: 'claude', sessionId: 'sess-2' }),
'claude --dangerously-skip-permissions -r sess-2'
);

assert.strictEqual(
methods.buildResumeCommand.call({ ...contextBase, sessionResumeWithYolo: true }, { source: 'claude', sessionId: '', filePath: '/home/user/.claude/projects/demo/sess-3.jsonl' }),
'claude -r sess-3'
methods.buildResumeCommand.call(ctx, { source: 'claude', sessionId: '', filePath: '/home/user/.claude/projects/demo/sess-3.jsonl' }),
'claude --dangerously-skip-permissions -r sess-3'
);

assert.strictEqual(
methods.buildResumeCommand.call({ ...contextBase, sessionResumeWithYolo: true }, { source: 'gemini', sessionId: '', filePath: '/home/user/.gemini/tmp/abc/chats/gm-456.json' }),
methods.buildResumeCommand.call(ctx, { source: 'gemini', sessionId: '', filePath: '/home/user/.gemini/tmp/abc/chats/gm-456.json' }),
'gemini -r gm-456'
);
});
Loading
Loading