@cloudcli-ai/cloudcli 1.36.0 → 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.
Files changed (217) hide show
  1. package/README.de.md +17 -18
  2. package/README.ja.md +16 -17
  3. package/README.ko.md +17 -18
  4. package/README.md +19 -20
  5. package/README.ru.md +17 -18
  6. package/README.tr.md +17 -18
  7. package/README.zh-CN.md +17 -18
  8. package/README.zh-TW.md +17 -18
  9. package/dist/api-docs.html +2 -3
  10. package/dist/assets/{index-S6cIy7hj.js → index-Bz9vXV2m.js} +264 -259
  11. package/dist/assets/index-DLmhQ15L.css +32 -0
  12. package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
  13. package/dist/index.html +3 -3
  14. package/dist-server/server/claude-sdk.js +37 -90
  15. package/dist-server/server/claude-sdk.js.map +1 -1
  16. package/dist-server/server/cli.js +1 -4
  17. package/dist-server/server/cli.js.map +1 -1
  18. package/dist-server/server/cursor-cli.js +12 -11
  19. package/dist-server/server/cursor-cli.js.map +1 -1
  20. package/dist-server/server/index.js +10 -141
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/assets/assets.routes.js +89 -0
  23. package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
  24. package/dist-server/server/modules/assets/index.js +4 -0
  25. package/dist-server/server/modules/assets/index.js.map +1 -0
  26. package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
  27. package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
  28. package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
  29. package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
  30. package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
  31. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  32. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
  33. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
  34. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  35. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  36. package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
  37. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
  39. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
  41. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +36 -1
  43. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
  45. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
  47. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +49 -13
  49. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
  51. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +8 -14
  53. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  54. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
  55. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
  57. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  58. package/dist-server/server/modules/providers/provider.registry.js +0 -2
  59. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  60. package/dist-server/server/modules/providers/provider.routes.js +0 -1
  61. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  62. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +7 -14
  63. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  64. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  65. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  66. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
  67. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +0 -1
  69. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +0 -13
  71. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  72. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
  73. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
  74. package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
  75. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  76. package/dist-server/server/modules/providers/tests/opencode-models.test.js +7 -0
  77. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  78. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
  79. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  80. package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
  81. package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
  82. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
  83. package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
  84. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  85. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
  86. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  87. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
  88. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
  90. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
  92. package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
  93. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
  94. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  95. package/dist-server/server/openai-codex.js +8 -2
  96. package/dist-server/server/openai-codex.js.map +1 -1
  97. package/dist-server/server/opencode-cli.js +43 -6
  98. package/dist-server/server/opencode-cli.js.map +1 -1
  99. package/dist-server/server/opencode-cli.test.js +103 -3
  100. package/dist-server/server/opencode-cli.test.js.map +1 -1
  101. package/dist-server/server/routes/agent.js +9 -19
  102. package/dist-server/server/routes/agent.js.map +1 -1
  103. package/dist-server/server/routes/commands.js +1 -2
  104. package/dist-server/server/routes/commands.js.map +1 -1
  105. package/dist-server/server/routes/git.js +179 -53
  106. package/dist-server/server/routes/git.js.map +1 -1
  107. package/dist-server/server/routes/git.test.js +80 -0
  108. package/dist-server/server/routes/git.test.js.map +1 -0
  109. package/dist-server/server/routes/taskmaster.js +3 -1
  110. package/dist-server/server/routes/taskmaster.js.map +1 -1
  111. package/dist-server/server/routes/user.js +2 -1
  112. package/dist-server/server/routes/user.js.map +1 -1
  113. package/dist-server/server/shared/image-attachments.js +270 -0
  114. package/dist-server/server/shared/image-attachments.js.map +1 -0
  115. package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
  116. package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
  117. package/dist-server/server/shared/utils.js +43 -0
  118. package/dist-server/server/shared/utils.js.map +1 -1
  119. package/dist-server/server/utils/gitConfig.js +2 -1
  120. package/dist-server/server/utils/gitConfig.js.map +1 -1
  121. package/dist-server/server/utils/plugin-process-manager.js +2 -1
  122. package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
  123. package/electron/launcher/launcher.js +1 -1
  124. package/package.json +3 -3
  125. package/public/api-docs.html +2 -3
  126. package/server/claude-sdk.js +37 -105
  127. package/server/cli.js +1 -4
  128. package/server/cursor-cli.js +13 -12
  129. package/server/index.js +11 -159
  130. package/server/modules/assets/assets.routes.ts +103 -0
  131. package/server/modules/assets/index.ts +3 -0
  132. package/server/modules/assets/services/image-assets.service.ts +82 -0
  133. package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
  134. package/server/modules/browser-use/browser-use.service.ts +7 -3
  135. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
  136. package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
  137. package/server/modules/projects/services/project-clone.service.ts +3 -1
  138. package/server/modules/providers/README.md +1 -7
  139. package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
  140. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
  141. package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
  142. package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
  143. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
  144. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
  145. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
  146. package/server/modules/providers/list/opencode/opencode-models.provider.ts +15 -19
  147. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
  148. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
  149. package/server/modules/providers/provider.registry.ts +0 -2
  150. package/server/modules/providers/provider.routes.ts +0 -1
  151. package/server/modules/providers/services/provider-capabilities.service.ts +7 -14
  152. package/server/modules/providers/services/provider-models.service.ts +1 -1
  153. package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
  154. package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
  155. package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
  156. package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
  157. package/server/modules/providers/tests/mcp.test.ts +3 -34
  158. package/server/modules/providers/tests/opencode-models.test.ts +7 -0
  159. package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
  160. package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
  161. package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
  162. package/server/modules/providers/tests/skills.test.ts +3 -42
  163. package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
  164. package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
  165. package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
  166. package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
  167. package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
  168. package/server/openai-codex.js +9 -1
  169. package/server/opencode-cli.js +44 -6
  170. package/server/opencode-cli.test.js +112 -3
  171. package/server/routes/agent.js +9 -19
  172. package/server/routes/commands.js +1 -2
  173. package/server/routes/git.js +201 -60
  174. package/server/routes/git.test.js +106 -0
  175. package/server/routes/taskmaster.js +3 -1
  176. package/server/routes/user.js +2 -1
  177. package/server/shared/image-attachments.ts +341 -0
  178. package/server/shared/tests/image-attachments.test.ts +298 -0
  179. package/server/shared/types.ts +1 -1
  180. package/server/shared/utils.ts +45 -0
  181. package/server/utils/gitConfig.js +2 -1
  182. package/server/utils/plugin-process-manager.js +3 -1
  183. package/dist/assets/index-uLUE6Qt1.css +0 -32
  184. package/dist/icons/gemini-ai-icon.svg +0 -1
  185. package/dist-server/server/gemini-cli.js +0 -551
  186. package/dist-server/server/gemini-cli.js.map +0 -1
  187. package/dist-server/server/gemini-response-handler.js +0 -106
  188. package/dist-server/server/gemini-response-handler.js.map +0 -1
  189. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
  190. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
  191. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
  192. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
  193. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
  194. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
  195. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
  196. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
  197. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
  198. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
  199. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
  200. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
  201. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
  202. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
  203. package/dist-server/server/routes/gemini.js +0 -21
  204. package/dist-server/server/routes/gemini.js.map +0 -1
  205. package/dist-server/server/sessionManager.js +0 -194
  206. package/dist-server/server/sessionManager.js.map +0 -1
  207. package/server/gemini-cli.js +0 -638
  208. package/server/gemini-response-handler.js +0 -117
  209. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
  210. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
  211. package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
  212. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
  213. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
  214. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
  215. package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
  216. package/server/routes/gemini.js +0 -25
  217. package/server/sessionManager.js +0 -226
@@ -1,5 +1,3 @@
1
- import { spawn } from 'node:child_process';
2
-
3
1
  import Database from 'better-sqlite3';
4
2
  import crossSpawn from 'cross-spawn';
5
3
 
@@ -51,23 +49,15 @@ export const OPENCODE_FALLBACK_MODELS: ProviderModelsDefinition = {
51
49
  label: 'GPT-5.4 Mini',
52
50
  description: 'openai - openai/gpt-5.4-mini',
53
51
  },
54
- {
55
- value: 'google/gemini-2.5-pro',
56
- label: 'Gemini 2.5 Pro',
57
- description: 'google - google/gemini-2.5-pro',
58
- },
59
- {
60
- value: 'google/gemini-2.5-flash',
61
- label: 'Gemini 2.5 Flash',
62
- description: 'google - google/gemini-2.5-flash',
63
- },
64
52
  ],
65
53
  DEFAULT: 'anthropic/claude-sonnet-4-5',
66
54
  };
67
55
 
68
56
  const OPEN_CODE_MODELS_TIMEOUT_MS = 20_000;
69
57
  const MODEL_ID_LINE = /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/i;
70
- const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
58
+ // cross-spawn resolves .cmd shims/PATHEXT on Windows and delegates to
59
+ // child_process.spawn everywhere else.
60
+ const spawnFunction = crossSpawn;
71
61
  const DATE_TOKEN = /^\d{8}$/;
72
62
  const SIMPLE_NUMBER_TOKEN = /^\d$/;
73
63
  const VERSION_TOKEN = /^[a-z]\d+$/i;
@@ -239,6 +229,10 @@ const readOpenCodeModelParts = (id: string): { upstreamProvider: string; slug: s
239
229
  };
240
230
  };
241
231
 
232
+ const isSupportedOpenCodeModelId = (id: string): boolean => (
233
+ readOpenCodeModelParts(id).upstreamProvider.toLowerCase() !== 'google'
234
+ );
235
+
242
236
  const readOpenCodeVerboseModelId = (model: OpenCodeVerboseModel): string | null => {
243
237
  const id = readOptionalString(model.id);
244
238
  if (!id) {
@@ -296,7 +290,7 @@ const readOpenCodeEffortValues = (
296
290
 
297
291
  const mapOpenCodeVerboseModel = (model: OpenCodeVerboseModel): ProviderModelOption | null => {
298
292
  const value = readOpenCodeVerboseModelId(model);
299
- if (!value) {
293
+ if (!value || !isSupportedOpenCodeModelId(value)) {
300
294
  return null;
301
295
  }
302
296
 
@@ -315,11 +309,13 @@ const mapOpenCodeVerboseModel = (model: OpenCodeVerboseModel): ProviderModelOpti
315
309
  };
316
310
 
317
311
  export const buildOpenCodeDefinitionFromIds = (ids: string[]): ProviderModelsDefinition => {
318
- const options: ProviderModelOption[] = ids.map((value) => ({
319
- value,
320
- label: labelForOpenCodeModelId(value),
321
- description: descriptionForOpenCodeModelId(value),
322
- }));
312
+ const options: ProviderModelOption[] = ids
313
+ .filter(isSupportedOpenCodeModelId)
314
+ .map((value) => ({
315
+ value,
316
+ label: labelForOpenCodeModelId(value),
317
+ description: descriptionForOpenCodeModelId(value),
318
+ }));
323
319
 
324
320
  const defaultValue = options.find((option) => option.value === OPENCODE_FALLBACK_MODELS.DEFAULT)?.value
325
321
  ?? options[0]?.value
@@ -11,6 +11,7 @@ import {
11
11
  normalizeSessionName,
12
12
  readJsonRecord,
13
13
  readOptionalString,
14
+ unwrapJsonStringLiteral,
14
15
  } from '@/shared/utils.js';
15
16
 
16
17
  type OpenCodeSessionRow = {
@@ -128,9 +129,26 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
128
129
  const existingSession = sessionsDb.getSessionByProviderSessionId(sessionId)
129
130
  ?? sessionsDb.getSessionById(sessionId);
130
131
  const existingName = existingSession?.custom_name;
131
- const nextName = existingName && existingName !== fallbackTitle
132
- ? existingName
133
- : readOptionalString(row.title) ?? this.readFirstUserText(db, sessionId);
132
+
133
+ // Sessions started by sending a message from cloudcli carry a distinct
134
+ // app-allocated session_id mapped to the provider id. For these we title the
135
+ // conversation from the first user message the user typed, matching how the
136
+ // app titles a brand-new conversation. Sessions discovered purely by
137
+ // indexing (session_id === provider_session_id) keep OpenCode's own stored
138
+ // title.
139
+ const isAppCreated =
140
+ existingSession != null &&
141
+ existingSession.provider_session_id != null &&
142
+ existingSession.session_id !== existingSession.provider_session_id;
143
+
144
+ let nextName: string | undefined;
145
+ if (existingName && existingName !== fallbackTitle) {
146
+ nextName = existingName;
147
+ } else if (isAppCreated) {
148
+ nextName = this.readFirstUserText(db, sessionId) ?? readOptionalString(row.title);
149
+ } else {
150
+ nextName = readOptionalString(row.title) ?? this.readFirstUserText(db, sessionId);
151
+ }
134
152
 
135
153
  // OpenCode stores every session in one shared sqlite database, so jsonl_path
136
154
  // must stay null to avoid deleting opencode.db when one app session is removed.
@@ -163,7 +181,10 @@ export class OpenCodeSessionSynchronizer implements IProviderSessionSynchronizer
163
181
  `).get(sessionId) as { data: string | null } | undefined;
164
182
 
165
183
  const data = readJsonRecord(row?.data);
166
- return readOptionalString(data?.text);
184
+ const text = readOptionalString(data?.text);
185
+ // OpenCode persists the first prompt as a JSON string literal (e.g.
186
+ // `"hello"`), so decode it to avoid titling the session with quotes.
187
+ return text === undefined ? undefined : unwrapJsonStringLiteral(text);
167
188
  } catch {
168
189
  return undefined;
169
190
  }
@@ -2,6 +2,7 @@ import fsSync from 'node:fs';
2
2
 
3
3
  import Database from 'better-sqlite3';
4
4
 
5
+ import { parseImagesInputTag } from '@/shared/image-attachments.js';
5
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
6
7
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
7
8
  import {
@@ -13,6 +14,7 @@ import {
13
14
  readJsonRecord,
14
15
  readOptionalString,
15
16
  sliceTailPage,
17
+ unwrapJsonStringLiteral,
16
18
  } from '@/shared/utils.js';
17
19
 
18
20
  const PROVIDER = 'opencode';
@@ -59,25 +61,6 @@ const formatToolContent = (value: unknown): string => {
59
61
  }
60
62
  };
61
63
 
62
- /**
63
- * OpenCode can persist the first prompt as a JSON string literal inside a text
64
- * part, for example `"hello"` instead of `hello`. Decode only complete JSON
65
- * string literals so normal assistant/user prose remains untouched.
66
- */
67
- const unwrapJsonStringLiteral = (value: string): string => {
68
- const trimmed = value.trim();
69
- if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
70
- return value;
71
- }
72
-
73
- try {
74
- const parsed = JSON.parse(trimmed);
75
- return typeof parsed === 'string' ? parsed : value;
76
- } catch {
77
- return value;
78
- }
79
- };
80
-
81
64
  const extractText = (value: unknown): string => {
82
65
  if (typeof value === 'string') {
83
66
  return unwrapJsonStringLiteral(value);
@@ -418,8 +401,13 @@ export class OpenCodeSessionsProvider implements IProviderSessions {
418
401
  }
419
402
 
420
403
  if (partType === 'text') {
421
- const content = extractText(partData);
422
- if (content.trim()) {
404
+ const rawContent = extractText(partData);
405
+ // User prompts sent with attachments carry an <images_input> path
406
+ // list; strip it for display and surface the paths as images.
407
+ const { text: content, attachments } = messageRole === 'user'
408
+ ? parseImagesInputTag(rawContent)
409
+ : { text: rawContent, attachments: [] };
410
+ if (content.trim() || attachments.length > 0) {
423
411
  normalized.push(createNormalizedMessage({
424
412
  id: baseId,
425
413
  sessionId,
@@ -428,6 +416,7 @@ export class OpenCodeSessionsProvider implements IProviderSessions {
428
416
  kind: 'text',
429
417
  role: messageRole === 'user' ? 'user' : 'assistant',
430
418
  content,
419
+ images: attachments.length > 0 ? attachments : undefined,
431
420
  }));
432
421
  }
433
422
  continue;
@@ -1,7 +1,6 @@
1
1
  import { ClaudeProvider } from '@/modules/providers/list/claude/claude.provider.js';
2
2
  import { CodexProvider } from '@/modules/providers/list/codex/codex.provider.js';
3
3
  import { CursorProvider } from '@/modules/providers/list/cursor/cursor.provider.js';
4
- import { GeminiProvider } from '@/modules/providers/list/gemini/gemini.provider.js';
5
4
  import { OpenCodeProvider } from '@/modules/providers/list/opencode/opencode.provider.js';
6
5
  import type { IProvider } from '@/shared/interfaces.js';
7
6
  import type { LLMProvider } from '@/shared/types.js';
@@ -11,7 +10,6 @@ const providers: Record<LLMProvider, IProvider> = {
11
10
  claude: new ClaudeProvider(),
12
11
  codex: new CodexProvider(),
13
12
  cursor: new CursorProvider(),
14
- gemini: new GeminiProvider(),
15
13
  opencode: new OpenCodeProvider(),
16
14
  };
17
15
 
@@ -285,7 +285,6 @@ const parseProvider = (value: unknown): LLMProvider => {
285
285
  normalized === 'claude'
286
286
  || normalized === 'codex'
287
287
  || normalized === 'cursor'
288
- || normalized === 'gemini'
289
288
  || normalized === 'opencode'
290
289
  ) {
291
290
  return normalized;
@@ -46,7 +46,7 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
46
46
  provider: 'cursor',
47
47
  permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
48
48
  defaultPermissionMode: 'default',
49
- supportsImages: false,
49
+ supportsImages: true,
50
50
  supportsAbort: true,
51
51
  supportsPermissionRequests: false,
52
52
  supportsTokenUsage: false,
@@ -56,27 +56,20 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
56
56
  provider: 'codex',
57
57
  permissionModes: ['default', 'acceptEdits', 'bypassPermissions'],
58
58
  defaultPermissionMode: 'default',
59
- supportsImages: false,
59
+ supportsImages: true,
60
60
  supportsAbort: true,
61
61
  supportsPermissionRequests: false,
62
62
  supportsTokenUsage: true,
63
63
  supportsEffort: true,
64
64
  },
65
- gemini: {
66
- provider: 'gemini',
67
- permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
68
- defaultPermissionMode: 'default',
69
- supportsImages: false,
70
- supportsAbort: true,
71
- supportsPermissionRequests: false,
72
- supportsTokenUsage: true,
73
- supportsEffort: false,
74
- },
75
65
  opencode: {
76
66
  provider: 'opencode',
77
- permissionModes: ['default'],
67
+ // Mapped by the runtime onto OpenCode's controls: `--agent plan` (plan),
68
+ // `--auto` (bypassPermissions) and the OPENCODE_PERMISSION env var
69
+ // (acceptEdits). See resolveOpenCodePermissionOptions in opencode-cli.js.
70
+ permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
78
71
  defaultPermissionMode: 'default',
79
- supportsImages: false,
72
+ supportsImages: true,
80
73
  supportsAbort: true,
81
74
  supportsPermissionRequests: false,
82
75
  supportsTokenUsage: true,
@@ -17,7 +17,7 @@ import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
17
17
 
18
18
  export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
19
19
  const PROVIDER_MODELS_CACHE_VERSION = 2;
20
- const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude', 'gemini']);
20
+ const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude']);
21
21
 
22
22
  type ProviderModelsServiceDependencies = {
23
23
  resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
@@ -8,7 +8,7 @@ import { rgPath } from '@vscode/ripgrep';
8
8
  import { projectsDb, sessionsDb } from '@/modules/database/index.js';
9
9
 
10
10
  type AnyRecord = Record<string, any>;
11
- type SearchableProvider = 'claude' | 'codex' | 'gemini';
11
+ type SearchableProvider = 'claude' | 'codex';
12
12
 
13
13
  type SearchSnippetHighlight = {
14
14
  start: number;
@@ -82,7 +82,7 @@ type ProjectBucket = {
82
82
  sessions: SearchableSessionRow[];
83
83
  };
84
84
 
85
- const SUPPORTED_PROVIDERS = new Set<SearchableProvider>(['claude', 'codex', 'gemini']);
85
+ const SUPPORTED_PROVIDERS = new Set<SearchableProvider>(['claude', 'codex']);
86
86
  const MAX_MATCHES_PER_SESSION = 2;
87
87
  const RIPGREP_FILE_CHUNK_SIZE = 40;
88
88
  const RIPGREP_CHUNK_CONCURRENCY = 6;
@@ -455,21 +455,6 @@ function extractCodexText(content: unknown): string {
455
455
  .join(' ');
456
456
  }
457
457
 
458
- function extractGeminiText(content: unknown): string {
459
- if (typeof content === 'string') {
460
- return content;
461
- }
462
-
463
- if (!Array.isArray(content)) {
464
- return '';
465
- }
466
-
467
- return content
468
- .filter((part: AnyRecord) => typeof part?.text === 'string')
469
- .map((part: AnyRecord) => String(part.text))
470
- .join(' ');
471
- }
472
-
473
458
  function normalizeSearchableSessions(rows: SessionRepositoryRow[]): SearchableSessionRow[] {
474
459
  const normalizedRows: SearchableSessionRow[] = [];
475
460
  const projectArchiveStateByPath = new Map<string, boolean>();
@@ -1065,81 +1050,6 @@ async function parseCodexSessionMatches(
1065
1050
  };
1066
1051
  }
1067
1052
 
1068
- async function parseGeminiSessionMatches(
1069
- session: SearchableSessionRow,
1070
- runtime: SearchRuntime,
1071
- ): Promise<SessionConversationResult | null> {
1072
- let data: string;
1073
- try {
1074
- data = await fs.readFile(session.jsonl_path, 'utf8');
1075
- } catch {
1076
- return null;
1077
- }
1078
-
1079
- let parsed: AnyRecord;
1080
- try {
1081
- parsed = JSON.parse(data) as AnyRecord;
1082
- } catch {
1083
- return null;
1084
- }
1085
-
1086
- const sourceMessages = Array.isArray(parsed.messages) ? parsed.messages as AnyRecord[] : [];
1087
- if (sourceMessages.length === 0) {
1088
- return null;
1089
- }
1090
-
1091
- const matches: SessionConversationMatch[] = [];
1092
- let firstUserText: string | null = null;
1093
-
1094
- for (const msg of sourceMessages) {
1095
- if (runtime.totalMatches >= runtime.limit || runtime.isAborted()) {
1096
- break;
1097
- }
1098
-
1099
- const role = msg.type === 'user'
1100
- ? 'user'
1101
- : (msg.type === 'gemini' || msg.type === 'assistant')
1102
- ? 'assistant'
1103
- : null;
1104
- if (!role) {
1105
- continue;
1106
- }
1107
-
1108
- const text = extractGeminiText(msg.content);
1109
- if (!text) {
1110
- continue;
1111
- }
1112
-
1113
- if (role === 'user' && !firstUserText) {
1114
- firstUserText = text;
1115
- }
1116
-
1117
- if (!runtime.matchesQuery(text)) {
1118
- continue;
1119
- }
1120
-
1121
- const { snippet, highlights } = runtime.buildSnippet(text);
1122
- addSessionMatch(runtime, matches, {
1123
- role,
1124
- snippet,
1125
- highlights,
1126
- timestamp: msg.timestamp ? String(msg.timestamp) : null,
1127
- provider: 'gemini',
1128
- });
1129
- }
1130
-
1131
- if (matches.length === 0) {
1132
- return null;
1133
- }
1134
-
1135
- return {
1136
- sessionId: session.session_id,
1137
- provider: 'gemini',
1138
- sessionSummary: toSummaryText(session.custom_name, firstUserText, 'Gemini Session'),
1139
- matches,
1140
- };
1141
- }
1142
-
1143
1053
  async function parseSessionMatches(
1144
1054
  session: SearchableSessionRow,
1145
1055
  runtime: SearchRuntime,
@@ -1150,7 +1060,7 @@ async function parseSessionMatches(
1150
1060
  if (session.provider === 'codex') {
1151
1061
  return parseCodexSessionMatches(session, runtime);
1152
1062
  }
1153
- return parseGeminiSessionMatches(session, runtime);
1063
+ return null;
1154
1064
  }
1155
1065
 
1156
1066
  export async function searchConversations(
@@ -21,7 +21,6 @@ export const sessionSynchronizerService = {
21
21
  claude: 0,
22
22
  codex: 0,
23
23
  cursor: 0,
24
- gemini: 0,
25
24
  opencode: 0,
26
25
  };
27
26
  const failures: string[] = [];
@@ -25,16 +25,6 @@ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> =
25
25
  provider: 'codex',
26
26
  rootPath: path.join(os.homedir(), '.codex', 'sessions'),
27
27
  },
28
- // {
29
- // provider: 'gemini',
30
- // rootPath: path.join(os.homedir(), '.gemini', 'sessions'),
31
- // },
32
- // Keep `sessions/` watcher disabled: Gemini also mirrors artifacts there,
33
- // which causes duplicate synchronization events.
34
- {
35
- provider: 'gemini',
36
- rootPath: path.join(os.homedir(), '.gemini', 'tmp'),
37
- },
38
28
  {
39
29
  provider: 'opencode',
40
30
  rootPath: path.join(os.homedir(), '.local', 'share', 'opencode'),
@@ -81,10 +71,6 @@ function isWatcherTargetFile(provider: LLMProvider, filePath: string): boolean {
81
71
  return path.basename(filePath) === 'opencode.db';
82
72
  }
83
73
 
84
- if (provider === 'gemini') {
85
- return filePath.endsWith('.json') || filePath.endsWith('.jsonl');
86
- }
87
-
88
74
  return filePath.endsWith('.jsonl');
89
75
  }
90
76
 
@@ -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 Gemini/Cursor MCP JSON formats and user/project scope persistence.
260
+ * This test covers Cursor MCP JSON format and user/project scope persistence.
261
261
  */
262
- test('providerMcpService handles gemini and cursor MCP JSON config formats', { concurrency: false }, async () => {
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, 5);
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
 
@@ -30,6 +30,7 @@ test('OpenCode models provider formats frontend labels from provider-prefixed id
30
30
  'opencode/nemotron-3-super-free',
31
31
  'anthropic/claude-3-5-sonnet-20241022',
32
32
  'anthropic/claude-opus-4-7-fast',
33
+ 'google/model-alpha',
33
34
  'openai/gpt-5.4-mini-fast',
34
35
  'openai/gpt-5.5-pro',
35
36
  'newprovider/alpha-v12-special-20261231',
@@ -104,6 +105,12 @@ anthropic/claude-sonnet-5
104
105
  }
105
106
  }
106
107
  }
108
+ google/model-alpha
109
+ {
110
+ "id": "model-alpha",
111
+ "providerID": "google",
112
+ "name": "Model Alpha"
113
+ }
107
114
  `);
108
115
 
109
116
  const definition = buildOpenCodeDefinitionFromVerboseModels(models);