@cloudcli-ai/cloudcli 1.37.0 → 1.37.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/dist/assets/index-D3jfzuKN.css +32 -0
- package/dist/assets/{index-eGqPeIEu.js → index-DTMWjJj8.js} +324 -305
- package/dist/index.html +2 -2
- package/dist-server/server/modules/commands/commands.routes.js +3 -3
- package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
- package/dist-server/server/modules/commands/tests/commands.test.js +2 -7
- package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -1
- package/dist-server/server/modules/database/index.js +2 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +18 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +63 -7
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +28 -3
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +22 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js +5 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +50 -18
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +83 -7
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +46 -14
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +8 -15
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +58 -71
- 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 +1 -54
- 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 +7 -7
- 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 +32 -669
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +105 -328
- 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 +0 -12
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +139 -31
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +167 -199
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +2 -2
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +12 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +62 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +53 -17
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +15 -126
- 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 +5 -5
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +175 -292
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +2 -6
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +1 -0
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +126 -0
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +87 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/package.json +3 -3
- package/server/modules/commands/commands.routes.ts +3 -3
- package/server/modules/commands/tests/commands.test.ts +2 -7
- package/server/modules/database/index.ts +2 -0
- package/server/modules/database/migrations.ts +20 -0
- package/server/modules/database/repositories/provider-models.ts +168 -0
- package/server/modules/database/repositories/sessions.db.ts +85 -8
- package/server/modules/database/schema.ts +29 -3
- package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -1
- package/server/modules/database/tests/sessions.db.integration.test.ts +32 -0
- package/server/modules/file-tree/file-tree.module.ts +5 -1
- package/server/modules/file-tree/file-tree.service.ts +68 -20
- package/server/modules/file-tree/tests/file-tree.service.test.ts +104 -7
- package/server/modules/providers/list/claude/claude-models.provider.ts +46 -14
- package/server/modules/providers/list/claude/claude-runtime.provider.js +8 -16
- package/server/modules/providers/list/codex/codex-models.provider.ts +58 -99
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +1 -60
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +7 -7
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +32 -710
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +105 -413
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +0 -13
- package/server/modules/providers/provider.routes.ts +185 -33
- package/server/modules/providers/services/provider-models.service.ts +223 -288
- package/server/modules/providers/services/provider-runtime.service.ts +2 -3
- package/server/modules/providers/services/session-conversations-search.service.ts +12 -4
- package/server/modules/providers/services/sessions.service.ts +87 -3
- package/server/modules/providers/tests/codex-sessions.test.ts +57 -18
- package/server/modules/providers/tests/opencode-models.test.ts +34 -135
- package/server/modules/providers/tests/opencode-sessions.test.ts +5 -5
- package/server/modules/providers/tests/provider-models.service.test.ts +353 -450
- package/server/modules/providers/tests/provider-runtime.service.test.ts +2 -6
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +1 -0
- package/server/modules/providers/tests/provider.routes.test.ts +183 -0
- package/server/modules/providers/tests/sessions.service.test.ts +127 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +5 -1
- package/server/shared/interfaces.ts +4 -6
- package/server/shared/types.ts +29 -17
- package/dist/assets/index-DSlds633.css +0 -32
|
@@ -787,11 +787,18 @@ async function parseClaudeSessionMatches(
|
|
|
787
787
|
? matchedSessionsForFile
|
|
788
788
|
: [session];
|
|
789
789
|
|
|
790
|
-
|
|
790
|
+
// Transcript lines are tagged with the provider session id (e.g. Claude's own
|
|
791
|
+
// conversation UUID), which is not necessarily the app-facing `session_id`.
|
|
792
|
+
// Match and attribute by the provider id, but map results back to the
|
|
793
|
+
// internal `session_id` the rest of the app uses to identify sessions.
|
|
794
|
+
const providerToInternalId = new Map<string, string>();
|
|
791
795
|
const customNameBySessionId = new Map<string, string | null>();
|
|
792
796
|
for (const candidate of targetSessions) {
|
|
793
|
-
|
|
797
|
+
const providerId = candidate.provider_session_id || candidate.session_id;
|
|
798
|
+
providerToInternalId.set(providerId, candidate.session_id);
|
|
799
|
+
customNameBySessionId.set(providerId, candidate.custom_name ?? null);
|
|
794
800
|
}
|
|
801
|
+
const targetSessionIds = new Set(providerToInternalId.keys());
|
|
795
802
|
|
|
796
803
|
type ClaudeSessionSearchState = {
|
|
797
804
|
matches: SessionConversationMatch[];
|
|
@@ -912,8 +919,9 @@ async function parseClaudeSessionMatches(
|
|
|
912
919
|
continue;
|
|
913
920
|
}
|
|
914
921
|
|
|
915
|
-
|
|
916
|
-
|
|
922
|
+
const internalSessionId = providerToInternalId.get(sessionId) ?? sessionId;
|
|
923
|
+
fileResults.set(internalSessionId, {
|
|
924
|
+
sessionId: internalSessionId,
|
|
917
925
|
provider: 'claude',
|
|
918
926
|
sessionSummary: toSummaryText(
|
|
919
927
|
customNameBySessionId.get(sessionId) ?? null,
|
|
@@ -17,6 +17,7 @@ type CreateAppSessionResult = {
|
|
|
17
17
|
sessionId: string;
|
|
18
18
|
provider: LLMProvider;
|
|
19
19
|
projectPath: string;
|
|
20
|
+
sessionName: string;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
type ArchivedSessionListItem = {
|
|
@@ -32,6 +33,17 @@ type ArchivedSessionListItem = {
|
|
|
32
33
|
isProjectArchived: boolean;
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
type RecentSessionListItem = Pick<
|
|
37
|
+
ArchivedSessionListItem,
|
|
38
|
+
'sessionId' | 'provider' | 'projectId' | 'projectDisplayName' | 'sessionTitle' | 'lastActivity'
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
type RecentSessionsPage = {
|
|
42
|
+
conversations: RecentSessionListItem[];
|
|
43
|
+
total: number;
|
|
44
|
+
hasMore: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
35
47
|
type SessionDetails = {
|
|
36
48
|
/** Canonical app-facing session id (may differ from the looked-up id when a provider-native id was given). */
|
|
37
49
|
sessionId: string;
|
|
@@ -51,6 +63,13 @@ type SessionDetails = {
|
|
|
51
63
|
} | null;
|
|
52
64
|
};
|
|
53
65
|
|
|
66
|
+
const MAX_CLOUDCLI_SESSION_NAME_WORDS = 4;
|
|
67
|
+
|
|
68
|
+
function buildCloudCliSessionName(initialMessage: string): string {
|
|
69
|
+
const words = initialMessage.trim().split(/\s+/).filter(Boolean);
|
|
70
|
+
return words.slice(0, MAX_CLOUDCLI_SESSION_NAME_WORDS).join(' ') || 'Untitled Session';
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
/**
|
|
55
74
|
* Removes one file if it exists.
|
|
56
75
|
*/
|
|
@@ -119,6 +138,40 @@ export const sessionsService = {
|
|
|
119
138
|
return chatRunRegistry.listRunningRuns();
|
|
120
139
|
},
|
|
121
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Returns the active conversation feed in true global activity order.
|
|
143
|
+
*/
|
|
144
|
+
listRecentSessions(limit: number, offset: number): RecentSessionsPage {
|
|
145
|
+
const page = sessionsDb.getRecentSessionsPage(limit, offset);
|
|
146
|
+
const projectCache = new Map<string, ReturnType<typeof projectsDb.getProjectPath>>();
|
|
147
|
+
const conversations = page.sessions.map((session) => {
|
|
148
|
+
const projectPath = session.project_path?.trim() ? session.project_path : null;
|
|
149
|
+
let project = null;
|
|
150
|
+
|
|
151
|
+
if (projectPath) {
|
|
152
|
+
if (!projectCache.has(projectPath)) {
|
|
153
|
+
projectCache.set(projectPath, projectsDb.getProjectPath(projectPath));
|
|
154
|
+
}
|
|
155
|
+
project = projectCache.get(projectPath) ?? null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
sessionId: session.session_id,
|
|
160
|
+
provider: session.provider as LLMProvider,
|
|
161
|
+
projectId: project?.project_id ?? null,
|
|
162
|
+
projectDisplayName: resolveProjectDisplayName(projectPath, project?.custom_project_name),
|
|
163
|
+
sessionTitle: session.custom_name?.trim() || session.session_id,
|
|
164
|
+
lastActivity: session.updated_at ?? session.created_at ?? null,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
conversations,
|
|
170
|
+
total: page.total,
|
|
171
|
+
hasMore: offset + conversations.length < page.total,
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
|
|
122
175
|
/**
|
|
123
176
|
* Resolves the provider-native session id a runtime needs for resume.
|
|
124
177
|
*
|
|
@@ -154,9 +207,15 @@ export const sessionsService = {
|
|
|
154
207
|
* (via `POST /api/providers/sessions`) when the user starts a brand-new
|
|
155
208
|
* chat, navigates to the returned id immediately, and the id never changes
|
|
156
209
|
* for the lifetime of the conversation. The provider-native id is mapped to
|
|
157
|
-
* this row later, when the provider runtime announces it mid-run.
|
|
210
|
+
* this row later, when the provider runtime announces it mid-run. Its title
|
|
211
|
+
* comes directly from the first visible CloudCLI message and is limited to
|
|
212
|
+
* four whole words before any provider-owned storage exists.
|
|
158
213
|
*/
|
|
159
|
-
createAppSession(
|
|
214
|
+
createAppSession(
|
|
215
|
+
provider: LLMProvider,
|
|
216
|
+
projectPath: string,
|
|
217
|
+
initialMessage: string,
|
|
218
|
+
): CreateAppSessionResult {
|
|
160
219
|
const normalizedProjectPath = projectPath.trim();
|
|
161
220
|
if (!normalizedProjectPath) {
|
|
162
221
|
throw new AppError('projectPath is required.', {
|
|
@@ -166,15 +225,40 @@ export const sessionsService = {
|
|
|
166
225
|
}
|
|
167
226
|
|
|
168
227
|
const sessionId = randomUUID();
|
|
169
|
-
|
|
228
|
+
const sessionName = buildCloudCliSessionName(initialMessage);
|
|
229
|
+
sessionsDb.createAppSession(sessionId, provider, normalizedProjectPath, sessionName);
|
|
170
230
|
|
|
171
231
|
return {
|
|
172
232
|
sessionId,
|
|
173
233
|
provider,
|
|
174
234
|
projectPath: normalizedProjectPath,
|
|
235
|
+
sessionName,
|
|
175
236
|
};
|
|
176
237
|
},
|
|
177
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Resolves the provider-native id only for an explicit user copy action.
|
|
241
|
+
* Normal session payloads continue to expose only the stable app id.
|
|
242
|
+
*/
|
|
243
|
+
getProviderSessionId(sessionId: string): string {
|
|
244
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
245
|
+
if (!session) {
|
|
246
|
+
throw new AppError(`Session "${sessionId}" was not found.`, {
|
|
247
|
+
code: 'SESSION_NOT_FOUND',
|
|
248
|
+
statusCode: 404,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!session.provider_session_id) {
|
|
253
|
+
throw new AppError('This session ID is not available yet.', {
|
|
254
|
+
code: 'PROVIDER_SESSION_ID_NOT_AVAILABLE',
|
|
255
|
+
statusCode: 409,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return session.provider_session_id;
|
|
260
|
+
},
|
|
261
|
+
|
|
178
262
|
/**
|
|
179
263
|
* Fetches persisted history by app session id.
|
|
180
264
|
*
|
|
@@ -64,24 +64,24 @@ const writeCodexTranscript = async (
|
|
|
64
64
|
return filePath;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
test('Codex synchronizer
|
|
67
|
+
test('Codex synchronizer preserves the title assigned when CloudCLI creates a session', { concurrency: false }, async () => {
|
|
68
68
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-session-sync-app-'));
|
|
69
69
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
70
70
|
await mkdir(workspacePath, { recursive: true });
|
|
71
71
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
await writeCodexTranscript(tempRoot, 'codex-app-1', workspacePath, '
|
|
74
|
+
await writeCodexTranscript(tempRoot, 'codex-app-1', workspacePath, 'Provider transcript title must not win');
|
|
75
75
|
await withIsolatedDatabase(async () => {
|
|
76
76
|
// The app allocates its own id and later maps the provider id onto it,
|
|
77
77
|
// exactly as a message sent from cloudcli does.
|
|
78
|
-
sessionsDb.createAppSession('app-1', 'codex', workspacePath);
|
|
78
|
+
sessionsDb.createAppSession('app-1', 'codex', workspacePath, 'Fix the login redirect');
|
|
79
79
|
sessionsDb.assignProviderSessionId('app-1', 'codex-app-1');
|
|
80
80
|
|
|
81
81
|
const synchronizer = new CodexSessionSynchronizer();
|
|
82
82
|
await synchronizer.synchronize();
|
|
83
83
|
|
|
84
|
-
assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the login redirect
|
|
84
|
+
assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the login redirect');
|
|
85
85
|
});
|
|
86
86
|
} finally {
|
|
87
87
|
restoreHomeDir();
|
|
@@ -152,7 +152,7 @@ test('Codex synchronizer leaves indexed sessions untitled when no name is availa
|
|
|
152
152
|
}
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
test('Codex history
|
|
155
|
+
test('Codex history preserves wrapped exec tool calls and results', { concurrency: false }, async () => {
|
|
156
156
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-exec-history-'));
|
|
157
157
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
158
158
|
await mkdir(workspacePath, { recursive: true });
|
|
@@ -161,15 +161,46 @@ test('Codex history renders Promise.all shell wrappers as Bash activity', { conc
|
|
|
161
161
|
try {
|
|
162
162
|
const providerSessionId = 'codex-exec-1';
|
|
163
163
|
const transcriptPath = await writeCodexTranscript(tempRoot, providerSessionId, workspacePath);
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
const wrappedCalls = [
|
|
165
|
+
{
|
|
166
|
+
callId: 'shell-command-1',
|
|
167
|
+
input: 'const cmds = ["echo one", "echo two"]; await Promise.all(cmds.map(command => tools.shell_command({ command })));',
|
|
168
|
+
expectedToolName: 'Bash',
|
|
169
|
+
expectedToolInput: JSON.stringify({ command: 'echo one\necho two' }),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
callId: 'json-shell-command-1',
|
|
173
|
+
input: 'const r = await tools.shell_command({"command":"Get-Content -Raw README.md","workdir":"C:\\\\workspace","timeout_ms":10000}); text(r)',
|
|
174
|
+
expectedToolName: 'Bash',
|
|
175
|
+
expectedToolInput: JSON.stringify({ command: 'Get-Content -Raw README.md' }),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
callId: 'exec-command-1',
|
|
179
|
+
input: 'await tools.exec_command({"command":"echo current"});',
|
|
180
|
+
expectedToolName: 'Bash',
|
|
181
|
+
expectedToolInput: JSON.stringify({ command: 'echo current' }),
|
|
182
|
+
},
|
|
183
|
+
{ callId: 'apply-patch-1', input: 'await tools.apply_patch("*** Begin Patch\\n*** End Patch");' },
|
|
184
|
+
{ callId: 'web-run-1', input: 'await tools.web__run({ search_query: [{ q: "Codex" }] });' },
|
|
185
|
+
{ callId: 'update-plan-1', input: 'await tools.update_plan({ plan: [] });' },
|
|
186
|
+
{ callId: 'unknown-1', input: 'await tools.unknown_wrapper({ value: true });' },
|
|
187
|
+
];
|
|
188
|
+
const transcriptLines = [
|
|
167
189
|
JSON.stringify({ type: 'session_meta', payload: { id: providerSessionId, cwd: workspacePath } }),
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
];
|
|
191
|
+
for (const call of wrappedCalls) {
|
|
192
|
+
transcriptLines.push(
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
type: 'response_item',
|
|
195
|
+
payload: { type: 'custom_tool_call', name: 'exec', call_id: call.callId, input: call.input },
|
|
196
|
+
}),
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
type: 'response_item',
|
|
199
|
+
payload: { type: 'custom_tool_call_output', call_id: call.callId, output: `result:${call.callId}` },
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
await writeFile(transcriptPath, `${transcriptLines.join('\n')}\n`, 'utf8');
|
|
173
204
|
|
|
174
205
|
await withIsolatedDatabase(async () => {
|
|
175
206
|
sessionsDb.createAppSession('app-exec-1', 'codex', workspacePath);
|
|
@@ -179,11 +210,19 @@ test('Codex history renders Promise.all shell wrappers as Bash activity', { conc
|
|
|
179
210
|
const history = await new CodexSessionsProvider().fetchHistory('app-exec-1');
|
|
180
211
|
const toolUses = history.messages.filter((message) => message.kind === 'tool_use');
|
|
181
212
|
const toolResults = history.messages.filter((message) => message.kind === 'tool_result');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
assert.equal(toolUses
|
|
186
|
-
assert.equal(toolResults.
|
|
213
|
+
const toolUsesById = new Map(toolUses.map((message) => [message.toolId, message]));
|
|
214
|
+
const toolResultsById = new Map(toolResults.map((message) => [message.toolId, message]));
|
|
215
|
+
|
|
216
|
+
assert.equal(toolUses.length, wrappedCalls.length);
|
|
217
|
+
assert.equal(toolResults.length, wrappedCalls.length);
|
|
218
|
+
for (const call of wrappedCalls) {
|
|
219
|
+
const toolUse = toolUsesById.get(call.callId);
|
|
220
|
+
assert.ok(toolUse);
|
|
221
|
+
assert.equal(toolUse.toolName, call.expectedToolName || 'exec');
|
|
222
|
+
assert.equal(toolUse.toolInput, call.expectedToolInput || call.input);
|
|
223
|
+
assert.equal(toolUse.toolResult?.content, `result:${call.callId}`);
|
|
224
|
+
assert.equal(toolResultsById.get(call.callId)?.content, `result:${call.callId}`);
|
|
225
|
+
}
|
|
187
226
|
});
|
|
188
227
|
} finally {
|
|
189
228
|
restoreHomeDir();
|
|
@@ -2,141 +2,40 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
parseOpenCodeModelsStdout,
|
|
8
|
-
parseOpenCodeVerboseModelsStdout,
|
|
5
|
+
OpenCodeProviderModels,
|
|
6
|
+
OPENCODE_PREDEFINED_MODELS,
|
|
9
7
|
} from '@/modules/providers/list/opencode/opencode-models.provider.js';
|
|
10
8
|
|
|
11
|
-
test('OpenCode
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
description: 'opencode - opencode/deepseek-v4-flash-free',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
value: 'opencode/nemotron-3-super-free',
|
|
47
|
-
label: 'Nemotron 3 Super Free',
|
|
48
|
-
description: 'opencode - opencode/nemotron-3-super-free',
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
value: 'anthropic/claude-3-5-sonnet-20241022',
|
|
52
|
-
label: 'Claude 3.5 Sonnet (2024-10-22)',
|
|
53
|
-
description: 'anthropic - anthropic/claude-3-5-sonnet-20241022',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
value: 'anthropic/claude-opus-4-7-fast',
|
|
57
|
-
label: 'Claude Opus 4.7 Fast',
|
|
58
|
-
description: 'anthropic - anthropic/claude-opus-4-7-fast',
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
value: 'openai/gpt-5.4-mini-fast',
|
|
62
|
-
label: 'GPT-5.4 Mini Fast',
|
|
63
|
-
description: 'openai - openai/gpt-5.4-mini-fast',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
value: 'openai/gpt-5.5-pro',
|
|
67
|
-
label: 'GPT-5.5 Pro',
|
|
68
|
-
description: 'openai - openai/gpt-5.5-pro',
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
value: 'newprovider/alpha-v12-special-20261231',
|
|
72
|
-
label: 'Alpha V12 Special (2026-12-31)',
|
|
73
|
-
description: 'newprovider - newprovider/alpha-v12-special-20261231',
|
|
74
|
-
},
|
|
75
|
-
]);
|
|
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
|
-
]);
|
|
9
|
+
test('OpenCode exposes only the curated predefined catalog', async () => {
|
|
10
|
+
const adapter = new OpenCodeProviderModels();
|
|
11
|
+
|
|
12
|
+
assert.deepEqual(await adapter.getSupportedModels(), OPENCODE_PREDEFINED_MODELS);
|
|
13
|
+
assert.equal(
|
|
14
|
+
(await adapter.getCurrentActiveModel()).model,
|
|
15
|
+
OPENCODE_PREDEFINED_MODELS.DEFAULT,
|
|
16
|
+
);
|
|
17
|
+
// OpenCode routes by `<providerID>/<modelID>`, so every option has to carry a
|
|
18
|
+
// provider prefix that `opencode models --verbose` reports.
|
|
19
|
+
const providerIds = new Set(
|
|
20
|
+
OPENCODE_PREDEFINED_MODELS.OPTIONS.map((option) => option.value.split('/')[0]),
|
|
21
|
+
);
|
|
22
|
+
assert.deepEqual([...providerIds].sort(), ['anthropic', 'opencode', 'openai'].sort());
|
|
23
|
+
assert.equal(
|
|
24
|
+
OPENCODE_PREDEFINED_MODELS.OPTIONS.every((option) => /^[a-z0-9-]+\/.+/.test(option.value)),
|
|
25
|
+
true,
|
|
26
|
+
);
|
|
27
|
+
assert.equal(
|
|
28
|
+
new Set(OPENCODE_PREDEFINED_MODELS.OPTIONS.map((option) => option.value)).size,
|
|
29
|
+
OPENCODE_PREDEFINED_MODELS.OPTIONS.length,
|
|
30
|
+
);
|
|
31
|
+
assert.equal(OPENCODE_PREDEFINED_MODELS.DEFAULT, 'opencode/gpt-5.6-terra');
|
|
32
|
+
assert.ok(
|
|
33
|
+
OPENCODE_PREDEFINED_MODELS.OPTIONS.some((option) => option.value === 'opencode/claude-opus-5'),
|
|
34
|
+
);
|
|
35
|
+
assert.ok(
|
|
36
|
+
OPENCODE_PREDEFINED_MODELS.OPTIONS.some((option) => option.value === 'anthropic/claude-opus-5'),
|
|
37
|
+
);
|
|
38
|
+
assert.ok(
|
|
39
|
+
OPENCODE_PREDEFINED_MODELS.OPTIONS.some((option) => option.value === 'openai/gpt-5.6'),
|
|
40
|
+
);
|
|
142
41
|
});
|
|
@@ -470,22 +470,22 @@ const seedOpenCodeSession = async (
|
|
|
470
470
|
}
|
|
471
471
|
};
|
|
472
472
|
|
|
473
|
-
test('OpenCode synchronizer
|
|
473
|
+
test('OpenCode synchronizer preserves the title assigned when CloudCLI creates a session', { concurrency: false }, async () => {
|
|
474
474
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-app-'));
|
|
475
475
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
476
476
|
await mkdir(workspacePath, { recursive: true });
|
|
477
477
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
478
478
|
|
|
479
479
|
try {
|
|
480
|
-
//
|
|
481
|
-
//
|
|
480
|
+
// Both provider-owned values differ from the CloudCLI title so either one
|
|
481
|
+
// leaking through would change the assertion below.
|
|
482
482
|
await seedOpenCodeSession(tempRoot, workspacePath, {
|
|
483
483
|
sessionId: 'oc-app-1',
|
|
484
484
|
title: 'OpenCode generated title',
|
|
485
|
-
firstUserText: '
|
|
485
|
+
firstUserText: 'OpenCode first user prompt',
|
|
486
486
|
});
|
|
487
487
|
await withIsolatedDatabase(async () => {
|
|
488
|
-
sessionsDb.createAppSession('app-1', 'opencode', workspacePath);
|
|
488
|
+
sessionsDb.createAppSession('app-1', 'opencode', workspacePath, 'Fix the checkout crash');
|
|
489
489
|
sessionsDb.assignProviderSessionId('app-1', 'oc-app-1');
|
|
490
490
|
|
|
491
491
|
await new OpenCodeSessionSynchronizer().synchronize();
|