@cloudcli-ai/cloudcli 1.35.1 → 1.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +17 -18
- package/README.ja.md +16 -17
- package/README.ko.md +17 -18
- package/README.md +19 -20
- package/README.ru.md +17 -18
- package/README.tr.md +17 -18
- package/README.zh-CN.md +17 -18
- package/README.zh-TW.md +17 -18
- package/dist/api-docs.html +8 -3
- package/dist/assets/{index-D1IdCTT6.js → index-Bz9vXV2m.js} +265 -260
- package/dist/assets/index-DLmhQ15L.css +32 -0
- package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
- package/dist/index.html +3 -3
- package/dist-server/server/claude-sdk.js +62 -116
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +1 -4
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/cursor-cli.js +12 -11
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/index.js +10 -141
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/assets/assets.routes.js +89 -0
- package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
- package/dist-server/server/modules/assets/index.js +4 -0
- package/dist-server/server/modules/assets/index.js.map +1 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +70 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +52 -11
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +145 -14
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.registry.js +0 -2
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +0 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +11 -13
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +2 -2
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +65 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
- package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/openai-codex.js +16 -9
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +62 -7
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-cli.test.js +103 -3
- package/dist-server/server/opencode-cli.test.js.map +1 -1
- package/dist-server/server/routes/agent.js +22 -21
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -2
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/git.js +179 -53
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/git.test.js +80 -0
- package/dist-server/server/routes/git.test.js.map +1 -0
- package/dist-server/server/routes/taskmaster.js +3 -1
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +2 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/shared/image-attachments.js +270 -0
- package/dist-server/server/shared/image-attachments.js.map +1 -0
- package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
- package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +43 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/gitConfig.js +2 -1
- package/dist-server/server/utils/gitConfig.js.map +1 -1
- package/dist-server/server/utils/plugin-process-manager.js +2 -1
- package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
- package/electron/launcher/launcher.js +1 -1
- package/package.json +3 -3
- package/public/api-docs.html +8 -3
- package/server/claude-sdk.js +69 -131
- package/server/cli.js +1 -4
- package/server/cursor-cli.js +13 -12
- package/server/index.js +11 -159
- package/server/modules/assets/assets.routes.ts +103 -0
- package/server/modules/assets/index.ts +3 -0
- package/server/modules/assets/services/image-assets.service.ts +82 -0
- package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
- package/server/modules/browser-use/browser-use.service.ts +7 -3
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
- package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/server/modules/projects/services/project-clone.service.ts +3 -1
- package/server/modules/providers/README.md +1 -7
- package/server/modules/providers/list/claude/claude-models.provider.ts +73 -1
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +59 -11
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +193 -18
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
- package/server/modules/providers/provider.registry.ts +0 -2
- package/server/modules/providers/provider.routes.ts +0 -1
- package/server/modules/providers/services/provider-capabilities.service.ts +13 -13
- package/server/modules/providers/services/provider-models.service.ts +2 -2
- package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
- package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
- package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
- package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
- package/server/modules/providers/tests/mcp.test.ts +3 -34
- package/server/modules/providers/tests/opencode-models.test.ts +69 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
- package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
- package/server/modules/providers/tests/skills.test.ts +3 -42
- package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
- package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
- package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
- package/server/openai-codex.js +18 -8
- package/server/opencode-cli.js +64 -7
- package/server/opencode-cli.test.js +112 -3
- package/server/routes/agent.js +22 -21
- package/server/routes/commands.js +1 -2
- package/server/routes/git.js +201 -60
- package/server/routes/git.test.js +106 -0
- package/server/routes/taskmaster.js +3 -1
- package/server/routes/user.js +2 -1
- package/server/shared/image-attachments.ts +341 -0
- package/server/shared/tests/image-attachments.test.ts +298 -0
- package/server/shared/types.ts +8 -1
- package/server/shared/utils.ts +45 -0
- package/server/utils/gitConfig.js +2 -1
- package/server/utils/plugin-process-manager.js +3 -1
- package/dist/assets/index-BvX30NW5.css +0 -32
- package/dist/icons/gemini-ai-icon.svg +0 -1
- package/dist-server/server/gemini-cli.js +0 -551
- package/dist-server/server/gemini-cli.js.map +0 -1
- package/dist-server/server/gemini-response-handler.js +0 -106
- package/dist-server/server/gemini-response-handler.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
- package/dist-server/server/routes/gemini.js +0 -21
- package/dist-server/server/routes/gemini.js.map +0 -1
- package/dist-server/server/sessionManager.js +0 -194
- package/dist-server/server/sessionManager.js.map +0 -1
- package/server/gemini-cli.js +0 -638
- package/server/gemini-response-handler.js +0 -117
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
- package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
- package/server/routes/gemini.js +0 -25
- package/server/sessionManager.js +0 -226
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
8
|
+
import { CodexSessionSynchronizer } from '@/modules/providers/list/codex/codex-session-synchronizer.provider.js';
|
|
9
|
+
|
|
10
|
+
const patchHomeDir = (nextHomeDir: string) => {
|
|
11
|
+
const original = os.homedir;
|
|
12
|
+
(os as any).homedir = () => nextHomeDir;
|
|
13
|
+
return () => {
|
|
14
|
+
(os as any).homedir = original;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
19
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
20
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'codex-provider-db-'));
|
|
21
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
22
|
+
|
|
23
|
+
closeConnection();
|
|
24
|
+
process.env.DATABASE_PATH = databasePath;
|
|
25
|
+
await initializeDatabase();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await runTest();
|
|
29
|
+
} finally {
|
|
30
|
+
closeConnection();
|
|
31
|
+
if (previousDatabasePath === undefined) {
|
|
32
|
+
delete process.env.DATABASE_PATH;
|
|
33
|
+
} else {
|
|
34
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
35
|
+
}
|
|
36
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Writes one Codex rollout transcript. `firstUserMessage` mirrors the
|
|
42
|
+
* `event_msg`/`user_message` payload the runtime records for the prompt the
|
|
43
|
+
* user typed; omitting it produces a transcript with no user turn.
|
|
44
|
+
*/
|
|
45
|
+
const writeCodexTranscript = async (
|
|
46
|
+
homeDir: string,
|
|
47
|
+
codexSessionId: string,
|
|
48
|
+
workspacePath: string,
|
|
49
|
+
firstUserMessage?: string,
|
|
50
|
+
): Promise<string> => {
|
|
51
|
+
const sessionsDir = path.join(homeDir, '.codex', 'sessions', '2026', '07', '07');
|
|
52
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
const lines: string[] = [
|
|
55
|
+
JSON.stringify({ type: 'session_meta', payload: { id: codexSessionId, cwd: workspacePath } }),
|
|
56
|
+
];
|
|
57
|
+
if (firstUserMessage !== undefined) {
|
|
58
|
+
lines.push(JSON.stringify({ type: 'event_msg', payload: { type: 'user_message', message: firstUserMessage } }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const filePath = path.join(sessionsDir, `rollout-${codexSessionId}.jsonl`);
|
|
62
|
+
await writeFile(filePath, `${lines.join('\n')}\n`, 'utf8');
|
|
63
|
+
return filePath;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
test('Codex synchronizer titles app-created sessions from the first user message', { concurrency: false }, async () => {
|
|
67
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-session-sync-app-'));
|
|
68
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
69
|
+
await mkdir(workspacePath, { recursive: true });
|
|
70
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await writeCodexTranscript(tempRoot, 'codex-app-1', workspacePath, 'Fix the login redirect bug');
|
|
74
|
+
await withIsolatedDatabase(async () => {
|
|
75
|
+
// The app allocates its own id and later maps the provider id onto it,
|
|
76
|
+
// exactly as a message sent from cloudcli does.
|
|
77
|
+
sessionsDb.createAppSession('app-1', 'codex', workspacePath);
|
|
78
|
+
sessionsDb.assignProviderSessionId('app-1', 'codex-app-1');
|
|
79
|
+
|
|
80
|
+
const synchronizer = new CodexSessionSynchronizer();
|
|
81
|
+
await synchronizer.synchronize();
|
|
82
|
+
|
|
83
|
+
assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the login redirect bug');
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
restoreHomeDir();
|
|
87
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('Codex synchronizer leaves indexed sessions untitled when no name is available', { concurrency: false }, async () => {
|
|
92
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-session-sync-indexed-'));
|
|
93
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
94
|
+
await mkdir(workspacePath, { recursive: true });
|
|
95
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// A CLI-created session has no app row; its first user message must NOT be
|
|
99
|
+
// used as the title, preserving the existing indexing behavior.
|
|
100
|
+
await writeCodexTranscript(tempRoot, 'codex-indexed-1', workspacePath, 'This prompt should be ignored');
|
|
101
|
+
await withIsolatedDatabase(async () => {
|
|
102
|
+
const synchronizer = new CodexSessionSynchronizer();
|
|
103
|
+
await synchronizer.synchronize();
|
|
104
|
+
|
|
105
|
+
assert.equal(sessionsDb.getSessionById('codex-indexed-1')?.custom_name, 'Untitled Codex Session');
|
|
106
|
+
});
|
|
107
|
+
} finally {
|
|
108
|
+
restoreHomeDir();
|
|
109
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
@@ -257,34 +257,15 @@ test('providerMcpService handles opencode MCP config and capability validation',
|
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
|
-
* This test covers
|
|
260
|
+
* This test covers Cursor MCP JSON format and user/project scope persistence.
|
|
261
261
|
*/
|
|
262
|
-
test('providerMcpService handles
|
|
262
|
+
test('providerMcpService handles cursor MCP JSON config formats', { concurrency: false }, async () => {
|
|
263
263
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-gc-'));
|
|
264
264
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
265
265
|
await fs.mkdir(workspacePath, { recursive: true });
|
|
266
266
|
|
|
267
267
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
268
268
|
try {
|
|
269
|
-
await providerMcpService.upsertProviderMcpServer('gemini', {
|
|
270
|
-
name: 'gemini-stdio',
|
|
271
|
-
scope: 'user',
|
|
272
|
-
transport: 'stdio',
|
|
273
|
-
command: 'node',
|
|
274
|
-
args: ['server.js'],
|
|
275
|
-
env: { TOKEN: '$TOKEN' },
|
|
276
|
-
cwd: './server',
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
await providerMcpService.upsertProviderMcpServer('gemini', {
|
|
280
|
-
name: 'gemini-http',
|
|
281
|
-
scope: 'project',
|
|
282
|
-
transport: 'http',
|
|
283
|
-
url: 'https://gemini.example.com/mcp',
|
|
284
|
-
headers: { Authorization: 'Bearer token' },
|
|
285
|
-
workspacePath,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
269
|
await providerMcpService.upsertProviderMcpServer('cursor', {
|
|
289
270
|
name: 'cursor-stdio',
|
|
290
271
|
scope: 'project',
|
|
@@ -303,15 +284,6 @@ test('providerMcpService handles gemini and cursor MCP JSON config formats', { c
|
|
|
303
284
|
headers: { API_KEY: 'value' },
|
|
304
285
|
});
|
|
305
286
|
|
|
306
|
-
const geminiUserConfig = await readJson(path.join(tempRoot, '.gemini', 'settings.json'));
|
|
307
|
-
const geminiUserServer = (geminiUserConfig.mcpServers as Record<string, unknown>)['gemini-stdio'] as Record<string, unknown>;
|
|
308
|
-
assert.equal(geminiUserServer.command, 'node');
|
|
309
|
-
assert.equal(geminiUserServer.type, undefined);
|
|
310
|
-
|
|
311
|
-
const geminiProjectConfig = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
|
312
|
-
const geminiProjectServer = (geminiProjectConfig.mcpServers as Record<string, unknown>)['gemini-http'] as Record<string, unknown>;
|
|
313
|
-
assert.equal(geminiProjectServer.type, 'http');
|
|
314
|
-
|
|
315
287
|
const cursorUserConfig = await readJson(path.join(tempRoot, '.cursor', 'mcp.json'));
|
|
316
288
|
const cursorHttpServer = (cursorUserConfig.mcpServers as Record<string, unknown>)['cursor-http'] as Record<string, unknown>;
|
|
317
289
|
assert.equal(cursorHttpServer.url, 'http://localhost:3333/mcp');
|
|
@@ -341,7 +313,7 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
|
|
|
341
313
|
workspacePath,
|
|
342
314
|
});
|
|
343
315
|
|
|
344
|
-
assert.equal(globalResult.length,
|
|
316
|
+
assert.equal(globalResult.length, 4);
|
|
345
317
|
assert.ok(globalResult.every((entry) => entry.created === true));
|
|
346
318
|
|
|
347
319
|
const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
|
|
@@ -350,9 +322,6 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
|
|
|
350
322
|
const codexProject = TOML.parse(await fs.readFile(path.join(workspacePath, '.codex', 'config.toml'), 'utf8')) as Record<string, unknown>;
|
|
351
323
|
assert.ok((codexProject.mcp_servers as Record<string, unknown>)['global-http']);
|
|
352
324
|
|
|
353
|
-
const geminiProject = await readJson(path.join(workspacePath, '.gemini', 'settings.json'));
|
|
354
|
-
assert.ok((geminiProject.mcpServers as Record<string, unknown>)['global-http']);
|
|
355
|
-
|
|
356
325
|
const opencodeProject = await readJson(path.join(workspacePath, 'opencode.json'));
|
|
357
326
|
assert.ok((opencodeProject.mcp as Record<string, unknown>)['global-http']);
|
|
358
327
|
|
|
@@ -2,8 +2,10 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
buildOpenCodeDefinitionFromVerboseModels,
|
|
5
6
|
buildOpenCodeDefinitionFromIds,
|
|
6
7
|
parseOpenCodeModelsStdout,
|
|
8
|
+
parseOpenCodeVerboseModelsStdout,
|
|
7
9
|
} from '@/modules/providers/list/opencode/opencode-models.provider.js';
|
|
8
10
|
|
|
9
11
|
test('OpenCode models provider parses plain CLI output and removes duplicates', () => {
|
|
@@ -28,6 +30,7 @@ test('OpenCode models provider formats frontend labels from provider-prefixed id
|
|
|
28
30
|
'opencode/nemotron-3-super-free',
|
|
29
31
|
'anthropic/claude-3-5-sonnet-20241022',
|
|
30
32
|
'anthropic/claude-opus-4-7-fast',
|
|
33
|
+
'google/model-alpha',
|
|
31
34
|
'openai/gpt-5.4-mini-fast',
|
|
32
35
|
'openai/gpt-5.5-pro',
|
|
33
36
|
'newprovider/alpha-v12-special-20261231',
|
|
@@ -71,3 +74,69 @@ test('OpenCode models provider formats frontend labels from provider-prefixed id
|
|
|
71
74
|
},
|
|
72
75
|
]);
|
|
73
76
|
});
|
|
77
|
+
|
|
78
|
+
test('OpenCode models provider maps verbose model variants to effort options', () => {
|
|
79
|
+
const models = parseOpenCodeVerboseModelsStdout(`
|
|
80
|
+
opencode/deepseek-v4-flash-free
|
|
81
|
+
{
|
|
82
|
+
"id": "deepseek-v4-flash-free",
|
|
83
|
+
"providerID": "opencode",
|
|
84
|
+
"name": "DeepSeek V4 Flash Free",
|
|
85
|
+
"variants": {
|
|
86
|
+
"low": {
|
|
87
|
+
"reasoningEffort": "low"
|
|
88
|
+
},
|
|
89
|
+
"high": {
|
|
90
|
+
"reasoningEffort": "high"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
anthropic/claude-sonnet-5
|
|
95
|
+
{
|
|
96
|
+
"id": "claude-sonnet-5",
|
|
97
|
+
"providerID": "anthropic",
|
|
98
|
+
"name": "Claude Sonnet 5",
|
|
99
|
+
"variants": {
|
|
100
|
+
"low": {
|
|
101
|
+
"effort": "low"
|
|
102
|
+
},
|
|
103
|
+
"max": {
|
|
104
|
+
"effort": "max"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
google/model-alpha
|
|
109
|
+
{
|
|
110
|
+
"id": "model-alpha",
|
|
111
|
+
"providerID": "google",
|
|
112
|
+
"name": "Model Alpha"
|
|
113
|
+
}
|
|
114
|
+
`);
|
|
115
|
+
|
|
116
|
+
const definition = buildOpenCodeDefinitionFromVerboseModels(models);
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(definition.OPTIONS, [
|
|
119
|
+
{
|
|
120
|
+
value: 'opencode/deepseek-v4-flash-free',
|
|
121
|
+
label: 'DeepSeek V4 Flash Free',
|
|
122
|
+
description: 'opencode - opencode/deepseek-v4-flash-free',
|
|
123
|
+
effort: {
|
|
124
|
+
values: [
|
|
125
|
+
{ value: 'low' },
|
|
126
|
+
{ value: 'high' },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
value: 'anthropic/claude-sonnet-5',
|
|
132
|
+
label: 'Claude Sonnet 5',
|
|
133
|
+
description: 'anthropic - anthropic/claude-sonnet-5',
|
|
134
|
+
effort: {
|
|
135
|
+
values: [
|
|
136
|
+
{ value: 'low' },
|
|
137
|
+
{ value: 'max' },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
@@ -9,6 +9,7 @@ import Database from 'better-sqlite3';
|
|
|
9
9
|
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
10
10
|
import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
|
|
11
11
|
import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
|
|
12
|
+
import { appendImagesInputTag } from '@/shared/image-attachments.js';
|
|
12
13
|
|
|
13
14
|
const patchHomeDir = (nextHomeDir: string) => {
|
|
14
15
|
const original = os.homedir;
|
|
@@ -321,6 +322,41 @@ test('OpenCode session synchronizer adopts the pending app session before watche
|
|
|
321
322
|
}
|
|
322
323
|
});
|
|
323
324
|
|
|
325
|
+
test('OpenCode sessions provider strips <images_input> from user turns and exposes attachments', { concurrency: false }, async () => {
|
|
326
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-images-'));
|
|
327
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
328
|
+
await mkdir(workspacePath, { recursive: true });
|
|
329
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
await createOpenCodeDatabase(tempRoot, workspacePath);
|
|
333
|
+
|
|
334
|
+
// Rewrite the user text part with the tagged prompt the runtime sends.
|
|
335
|
+
const taggedPrompt = appendImagesInputTag('Look at this screenshot.', [
|
|
336
|
+
{ path: 'C:/Users/x/.cloudcli/assets/shot.png' },
|
|
337
|
+
]);
|
|
338
|
+
const db = new Database(path.join(tempRoot, '.local', 'share', 'opencode', 'opencode.db'));
|
|
339
|
+
try {
|
|
340
|
+
db.prepare('UPDATE part SET data = ? WHERE id = ?').run(
|
|
341
|
+
JSON.stringify({ type: 'text', text: taggedPrompt }),
|
|
342
|
+
'part-user-text',
|
|
343
|
+
);
|
|
344
|
+
} finally {
|
|
345
|
+
db.close();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const provider = new OpenCodeSessionsProvider();
|
|
349
|
+
const history = await provider.fetchHistory('open-session-1');
|
|
350
|
+
const userMessage = history.messages.find((message) => message.kind === 'text' && message.role === 'user');
|
|
351
|
+
|
|
352
|
+
assert.equal(userMessage?.content, 'Look at this screenshot.');
|
|
353
|
+
assert.deepEqual(userMessage?.images, [{ path: 'C:/Users/x/.cloudcli/assets/shot.png' }]);
|
|
354
|
+
} finally {
|
|
355
|
+
restoreHomeDir();
|
|
356
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
324
360
|
test('OpenCode sessions provider normalizes quoted live text and skips user echoes', () => {
|
|
325
361
|
const provider = new OpenCodeSessionsProvider();
|
|
326
362
|
const normalized = provider.normalizeMessage({
|
|
@@ -381,3 +417,106 @@ test('OpenCode sessions provider reads sqlite history and token usage', { concur
|
|
|
381
417
|
await rm(tempRoot, { recursive: true, force: true });
|
|
382
418
|
}
|
|
383
419
|
});
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Seeds a single OpenCode session with a controllable stored title and first
|
|
423
|
+
* user message. Uses a minimal schema (only the columns the synchronizer reads)
|
|
424
|
+
* with a plain-text user part so the derived name is unambiguous.
|
|
425
|
+
*/
|
|
426
|
+
const seedOpenCodeSession = async (
|
|
427
|
+
homeDir: string,
|
|
428
|
+
workspacePath: string,
|
|
429
|
+
options: { sessionId: string; title: string | null; firstUserText: string },
|
|
430
|
+
): Promise<void> => {
|
|
431
|
+
const dataDir = path.join(homeDir, '.local', 'share', 'opencode');
|
|
432
|
+
await mkdir(dataDir, { recursive: true });
|
|
433
|
+
|
|
434
|
+
const db = new Database(path.join(dataDir, 'opencode.db'));
|
|
435
|
+
try {
|
|
436
|
+
db.exec(`
|
|
437
|
+
CREATE TABLE project (id TEXT PRIMARY KEY, worktree TEXT);
|
|
438
|
+
CREATE TABLE session (
|
|
439
|
+
id TEXT PRIMARY KEY,
|
|
440
|
+
project_id TEXT,
|
|
441
|
+
directory TEXT,
|
|
442
|
+
title TEXT,
|
|
443
|
+
time_created INTEGER,
|
|
444
|
+
time_updated INTEGER,
|
|
445
|
+
time_archived INTEGER
|
|
446
|
+
);
|
|
447
|
+
CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
|
|
448
|
+
CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT);
|
|
449
|
+
`);
|
|
450
|
+
|
|
451
|
+
db.prepare('INSERT INTO project (id, worktree) VALUES (?, ?)').run('project-1', workspacePath);
|
|
452
|
+
db.prepare(`
|
|
453
|
+
INSERT INTO session (id, project_id, directory, title, time_created, time_updated, time_archived)
|
|
454
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL)
|
|
455
|
+
`).run(options.sessionId, 'project-1', workspacePath, options.title, 1_700_000_000_000, 1_700_000_001_000);
|
|
456
|
+
db.prepare('INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)')
|
|
457
|
+
.run('message-user', options.sessionId, 1_700_000_001_000, JSON.stringify({ role: 'user' }));
|
|
458
|
+
db.prepare('INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)')
|
|
459
|
+
.run(
|
|
460
|
+
'part-user',
|
|
461
|
+
'message-user',
|
|
462
|
+
options.sessionId,
|
|
463
|
+
1_700_000_001_000,
|
|
464
|
+
// OpenCode persists the prompt as a JSON string literal inside the text
|
|
465
|
+
// field, so double-encode it here to exercise the unwrap on read.
|
|
466
|
+
JSON.stringify({ type: 'text', text: JSON.stringify(options.firstUserText) }),
|
|
467
|
+
);
|
|
468
|
+
} finally {
|
|
469
|
+
db.close();
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
test('OpenCode synchronizer titles app-created sessions from the first user message', { concurrency: false }, async () => {
|
|
474
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-app-'));
|
|
475
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
476
|
+
await mkdir(workspacePath, { recursive: true });
|
|
477
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
// Stored title differs from the first message so we can prove the first
|
|
481
|
+
// message wins for sessions started from cloudcli.
|
|
482
|
+
await seedOpenCodeSession(tempRoot, workspacePath, {
|
|
483
|
+
sessionId: 'oc-app-1',
|
|
484
|
+
title: 'OpenCode generated title',
|
|
485
|
+
firstUserText: 'Fix the checkout crash',
|
|
486
|
+
});
|
|
487
|
+
await withIsolatedDatabase(async () => {
|
|
488
|
+
sessionsDb.createAppSession('app-1', 'opencode', workspacePath);
|
|
489
|
+
sessionsDb.assignProviderSessionId('app-1', 'oc-app-1');
|
|
490
|
+
|
|
491
|
+
await new OpenCodeSessionSynchronizer().synchronize();
|
|
492
|
+
|
|
493
|
+
assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the checkout crash');
|
|
494
|
+
});
|
|
495
|
+
} finally {
|
|
496
|
+
restoreHomeDir();
|
|
497
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('OpenCode synchronizer keeps the stored title for indexed sessions', { concurrency: false }, async () => {
|
|
502
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-indexed-'));
|
|
503
|
+
const workspacePath = path.join(tempRoot, 'workspace');
|
|
504
|
+
await mkdir(workspacePath, { recursive: true });
|
|
505
|
+
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
await seedOpenCodeSession(tempRoot, workspacePath, {
|
|
509
|
+
sessionId: 'oc-indexed-1',
|
|
510
|
+
title: 'OpenCode generated title',
|
|
511
|
+
firstUserText: 'This prompt should be ignored',
|
|
512
|
+
});
|
|
513
|
+
await withIsolatedDatabase(async () => {
|
|
514
|
+
await new OpenCodeSessionSynchronizer().synchronize();
|
|
515
|
+
|
|
516
|
+
assert.equal(sessionsDb.getSessionById('oc-indexed-1')?.custom_name, 'OpenCode generated title');
|
|
517
|
+
});
|
|
518
|
+
} finally {
|
|
519
|
+
restoreHomeDir();
|
|
520
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
|
|
5
|
+
import { CodexSessionsProvider, extractCodexUserImages } from '@/modules/providers/list/codex/codex-sessions.provider.js';
|
|
6
|
+
import { CursorSessionsProvider } from '@/modules/providers/list/cursor/cursor-sessions.provider.js';
|
|
7
|
+
import { appendImagesInputTag } from '@/shared/image-attachments.js';
|
|
8
|
+
|
|
9
|
+
const SESSION_ID = 'session-1';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------- Claude
|
|
12
|
+
|
|
13
|
+
test('claude history: base64 image blocks surface as user message images', () => {
|
|
14
|
+
const provider = new ClaudeSessionsProvider();
|
|
15
|
+
const entry = {
|
|
16
|
+
uuid: 'u1',
|
|
17
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
18
|
+
message: {
|
|
19
|
+
role: 'user',
|
|
20
|
+
content: [
|
|
21
|
+
{ type: 'text', text: 'What is in this screenshot?' },
|
|
22
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'QUJD' } },
|
|
23
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: 'REVG' } },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const messages = provider.normalizeMessage(entry, SESSION_ID);
|
|
29
|
+
|
|
30
|
+
assert.equal(messages.length, 1);
|
|
31
|
+
assert.equal(messages[0].kind, 'text');
|
|
32
|
+
assert.equal(messages[0].role, 'user');
|
|
33
|
+
assert.equal(messages[0].content, 'What is in this screenshot?');
|
|
34
|
+
assert.deepEqual(messages[0].images, [
|
|
35
|
+
{ data: 'data:image/png;base64,QUJD' },
|
|
36
|
+
{ data: 'data:image/jpeg;base64,REVG' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('claude history: image-only user turns still produce a bubble', () => {
|
|
41
|
+
const provider = new ClaudeSessionsProvider();
|
|
42
|
+
const entry = {
|
|
43
|
+
uuid: 'u2',
|
|
44
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
45
|
+
message: {
|
|
46
|
+
role: 'user',
|
|
47
|
+
content: [
|
|
48
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'QUJD' } },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const messages = provider.normalizeMessage(entry, SESSION_ID);
|
|
54
|
+
|
|
55
|
+
assert.equal(messages.length, 1);
|
|
56
|
+
assert.equal(messages[0].role, 'user');
|
|
57
|
+
assert.equal(messages[0].content, '');
|
|
58
|
+
assert.deepEqual(messages[0].images, [{ data: 'data:image/png;base64,QUJD' }]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('claude history: plain text user turns carry no images field', () => {
|
|
62
|
+
const provider = new ClaudeSessionsProvider();
|
|
63
|
+
const entry = {
|
|
64
|
+
uuid: 'u3',
|
|
65
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
66
|
+
message: { role: 'user', content: [{ type: 'text', text: 'hello' }] },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const messages = provider.normalizeMessage(entry, SESSION_ID);
|
|
70
|
+
assert.equal(messages.length, 1);
|
|
71
|
+
assert.equal(messages[0].images, undefined);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------- Codex
|
|
75
|
+
|
|
76
|
+
test('codex history: user_message payload images become path attachments', () => {
|
|
77
|
+
// Real rollout shape: local_image input items land in `local_images`,
|
|
78
|
+
// while `images` stays an empty array.
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
extractCodexUserImages({
|
|
81
|
+
type: 'user_message',
|
|
82
|
+
message: 'can u see attached image?',
|
|
83
|
+
images: [],
|
|
84
|
+
local_images: ['C:\\proj\\.cloudcli\\assets\\a.png'],
|
|
85
|
+
}),
|
|
86
|
+
[{ path: 'C:/proj/.cloudcli/assets/a.png' }],
|
|
87
|
+
);
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
extractCodexUserImages({ type: 'user_message', message: 'hi', images: ['/proj/b.jpg'] }),
|
|
90
|
+
[{ path: '/proj/b.jpg' }],
|
|
91
|
+
);
|
|
92
|
+
assert.equal(extractCodexUserImages({ type: 'user_message', message: 'hi' }), undefined);
|
|
93
|
+
assert.equal(extractCodexUserImages({ type: 'user_message', message: 'hi', images: [], local_images: [] }), undefined);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('codex history: base64 data URLs pass through as inline data attachments', () => {
|
|
97
|
+
const dataUrl = 'data:image/png;base64,QUJD';
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
extractCodexUserImages({
|
|
100
|
+
type: 'user_message',
|
|
101
|
+
message: 'look',
|
|
102
|
+
images: [dataUrl],
|
|
103
|
+
local_images: ['C:\\proj\\a.png'],
|
|
104
|
+
}),
|
|
105
|
+
[{ path: 'C:/proj/a.png' }, { data: dataUrl }],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('codex history: normalized user entries keep their images', () => {
|
|
110
|
+
const provider = new CodexSessionsProvider();
|
|
111
|
+
const messages = provider.normalizeMessage(
|
|
112
|
+
{
|
|
113
|
+
timestamp: '2026-07-03T10:00:00.000Z',
|
|
114
|
+
message: { role: 'user', content: 'Look at this' },
|
|
115
|
+
images: [{ path: '.cloudcli/assets/a.png' }],
|
|
116
|
+
},
|
|
117
|
+
SESSION_ID,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
assert.equal(messages.length, 1);
|
|
121
|
+
assert.equal(messages[0].role, 'user');
|
|
122
|
+
assert.equal(messages[0].content, 'Look at this');
|
|
123
|
+
assert.deepEqual(messages[0].images, [{ path: '.cloudcli/assets/a.png' }]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------- Cursor
|
|
127
|
+
|
|
128
|
+
test('cursor history: <images_input> inside user_query is stripped and attached', () => {
|
|
129
|
+
const provider = new CursorSessionsProvider();
|
|
130
|
+
const taggedPrompt = appendImagesInputTag('Fix the layout bug', [{ path: '.cloudcli/assets/shot.png' }]);
|
|
131
|
+
const blobs = [
|
|
132
|
+
{
|
|
133
|
+
id: 'blob1',
|
|
134
|
+
sequence: 1,
|
|
135
|
+
rowid: 1,
|
|
136
|
+
content: {
|
|
137
|
+
role: 'user',
|
|
138
|
+
content: `<timestamp>2026-07-03</timestamp>\n<user_query>${taggedPrompt}</user_query>`,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'blob2',
|
|
143
|
+
sequence: 2,
|
|
144
|
+
rowid: 2,
|
|
145
|
+
content: {
|
|
146
|
+
role: 'assistant',
|
|
147
|
+
content: [{ type: 'text', text: 'Done — the flex container was wrong.' }],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const messages = provider.normalizeCursorBlobs(blobs, SESSION_ID);
|
|
153
|
+
|
|
154
|
+
assert.equal(messages.length, 2);
|
|
155
|
+
assert.equal(messages[0].role, 'user');
|
|
156
|
+
assert.equal(messages[0].content, 'Fix the layout bug');
|
|
157
|
+
assert.deepEqual(messages[0].images, [{ path: '.cloudcli/assets/shot.png' }]);
|
|
158
|
+
assert.equal(messages[1].role, 'assistant');
|
|
159
|
+
assert.equal(messages[1].images, undefined);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('cursor history: user text without a tag keeps existing behavior', () => {
|
|
163
|
+
const provider = new CursorSessionsProvider();
|
|
164
|
+
const blobs = [
|
|
165
|
+
{
|
|
166
|
+
id: 'blob1',
|
|
167
|
+
sequence: 1,
|
|
168
|
+
rowid: 1,
|
|
169
|
+
content: {
|
|
170
|
+
role: 'user',
|
|
171
|
+
content: '<timestamp>2026-07-03</timestamp>\n<user_query>plain question</user_query>',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const messages = provider.normalizeCursorBlobs(blobs, SESSION_ID);
|
|
177
|
+
assert.equal(messages.length, 1);
|
|
178
|
+
assert.equal(messages[0].content, 'plain question');
|
|
179
|
+
assert.equal(messages[0].images, undefined);
|
|
180
|
+
});
|
|
@@ -170,13 +170,13 @@ test('provider model cache is persisted across service instances', async () => {
|
|
|
170
170
|
cachePath,
|
|
171
171
|
resolveProvider: () => ({
|
|
172
172
|
models: {
|
|
173
|
-
getSupportedModels: async () => createModels('
|
|
174
|
-
getCurrentActiveModel: async () => createCurrentActiveModel('
|
|
175
|
-
changeActiveModel: async (input) => createSessionActiveModelChange('
|
|
173
|
+
getSupportedModels: async () => createModels('cursor-cached'),
|
|
174
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'),
|
|
175
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input),
|
|
176
176
|
},
|
|
177
177
|
}),
|
|
178
178
|
});
|
|
179
|
-
await writer.getProviderModels('
|
|
179
|
+
await writer.getProviderModels('cursor');
|
|
180
180
|
|
|
181
181
|
const reader = createProviderModelsService({
|
|
182
182
|
cachePath,
|
|
@@ -185,13 +185,13 @@ test('provider model cache is persisted across service instances', async () => {
|
|
|
185
185
|
getSupportedModels: async () => {
|
|
186
186
|
throw new Error('loader should not be called for persisted cache hits');
|
|
187
187
|
},
|
|
188
|
-
getCurrentActiveModel: async () => createCurrentActiveModel('
|
|
189
|
-
changeActiveModel: async (input) => createSessionActiveModelChange('
|
|
188
|
+
getCurrentActiveModel: async () => createCurrentActiveModel('cursor-active'),
|
|
189
|
+
changeActiveModel: async (input) => createSessionActiveModelChange('cursor', input),
|
|
190
190
|
},
|
|
191
191
|
}),
|
|
192
192
|
});
|
|
193
|
-
const models = await reader.getProviderModels('
|
|
194
|
-
assert.equal(models.models.DEFAULT, '
|
|
193
|
+
const models = await reader.getProviderModels('cursor');
|
|
194
|
+
assert.equal(models.models.DEFAULT, 'cursor-cached');
|
|
195
195
|
assert.equal(models.cache.source, 'disk');
|
|
196
196
|
} finally {
|
|
197
197
|
await rm(tempRoot, { recursive: true, force: true });
|