@openchambery/web 1.18.2-beta.17 → 1.18.2-beta.19

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 (60) hide show
  1. package/dist/assets/{AssistantView-DFHmpZ7G.js → AssistantView-DEAvjnft.js} +1 -1
  2. package/dist/assets/{ChatContainer-D7SxgV8T.js → ChatContainer-BYIW9HIv.js} +5 -5
  3. package/dist/assets/{DiagramView-DmHeDjDE.js → DiagramView-CUN3aCw7.js} +1 -1
  4. package/dist/assets/{MarkdownRendererImpl-CR70vDmk.js → MarkdownRendererImpl-COkICFQ6.js} +1 -1
  5. package/dist/assets/{MobileDetailNavigation-CosmubCm.js → MobileDetailNavigation-Dotl0uFK.js} +1 -1
  6. package/dist/assets/{MobileShareBridge-DzP5vJAv.js → MobileShareBridge-Dzpj505z.js} +1 -1
  7. package/dist/assets/{MobileSurface-bpfoNOdp.js → MobileSurface-CYF6jUKD.js} +1 -1
  8. package/dist/assets/{MultiRunWindow-Bj35xImj.js → MultiRunWindow-CTbEOWL7.js} +1 -1
  9. package/dist/assets/{OverlayScrollbar-C0r95yEi.js → OverlayScrollbar-DUQXxlLe.js} +1 -1
  10. package/dist/assets/{SettingsView-DlUCG6xP.js → SettingsView-C-m1kZb6.js} +1 -1
  11. package/dist/assets/{SettingsWindow-Bx6yUy4t.js → SettingsWindow-o3oQwjry.js} +1 -1
  12. package/dist/assets/{TerminalView-A6lT_pyN.js → TerminalView-D3PPugiI.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-74Ec3sow.js → ToolOutputDialog-Dvc8UO6W.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-C2czYOtx.js → appThemeRegistry-BO-X-_Y5.js} +2 -2
  15. package/dist/assets/{context-menu-B87qIDjC.js → context-menu-oaMnJuJM.js} +1 -1
  16. package/dist/assets/{gitApi-CAYz6qHu.js → gitApi-BPMTze1H.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-iAOuvfny.js → insertionBoundaries-BGh97ePU.js} +1 -1
  18. package/dist/assets/{main-BQGhNyeU.js → main-BjToXPkD.js} +4 -4
  19. package/dist/assets/{main-Cu7sZyJS.js → main-Ct2EUXIP.js} +2 -2
  20. package/dist/assets/{miniChat-CwrVreq-.js → miniChat-C8ngP354.js} +2 -2
  21. package/dist/assets/{mobile-SQEAwCJ8.js → mobile-Cg4Atz6k.js} +2 -2
  22. package/dist/assets/{multirun-CJ3fED_0.js → multirun-huuovQct.js} +1 -1
  23. package/dist/assets/{projectMeta-BM3a0rwT.js → projectMeta-tNQ9Kguo.js} +1 -1
  24. package/dist/assets/{radio-DfR3It8I.js → radio-D9xOUMnG.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-b3VyI_V2.js → renderElectronMiniChatApp-Bx3-FFHE.js} +2 -2
  26. package/dist/assets/renderMobileApp-CVMA5jZT.js +23 -0
  27. package/dist/assets/{runtimeEndpointReset-3gL_biDm.js → runtimeEndpointReset-C9s6_Cpl.js} +2 -2
  28. package/dist/assets/{sortable-tabs-strip-D0fuWvzp.js → sortable-tabs-strip-D2wN-rgC.js} +1 -1
  29. package/dist/assets/src-BcdldUpo.css +2 -0
  30. package/dist/assets/{useAppFontEffects-AWfqdYZm.js → useAppFontEffects-C3mR2bTn.js} +2 -2
  31. package/dist/assets/{useDesktopWindowControlsLayout-BgCS8KRJ.js → useDesktopWindowControlsLayout-C1HMRcis.js} +1 -1
  32. package/dist/assets/{useEffectiveDirectory-ktSK1ADz.js → useEffectiveDirectory-VHzoc25S.js} +1 -1
  33. package/dist/assets/{useMobileNavigationStore-Dy5a1iTA.js → useMobileNavigationStore-DXYzE0lX.js} +3 -3
  34. package/dist/assets/{useSessionAutoCleanup-BRYDBUlq.js → useSessionAutoCleanup-CH6xneXb.js} +1 -1
  35. package/dist/assets/{useWorkerHighlightedLines-RexsXGPu.js → useWorkerHighlightedLines-BZ1TxY5I.js} +1 -1
  36. package/dist/index.html +2 -2
  37. package/dist/mini-chat.html +2 -2
  38. package/dist/mobile.html +2 -2
  39. package/package.json +1 -1
  40. package/server/index.js +7 -0
  41. package/server/lib/notifications/DOCUMENTATION.md +2 -2
  42. package/server/lib/notifications/runtime.js +68 -27
  43. package/server/lib/notifications/runtime.test.js +176 -0
  44. package/server/lib/session-index/DOCUMENTATION.md +6 -5
  45. package/server/lib/session-index/service.js +1 -0
  46. package/server/lib/session-index/service.test.js +7 -2
  47. package/server/lib/session-title/DOCUMENTATION.md +3 -2
  48. package/server/lib/session-title/runtime.js +1 -0
  49. package/server/lib/session-title/runtime.test.js +6 -0
  50. package/server/lib/small-model/DOCUMENTATION.md +93 -38
  51. package/server/lib/small-model/catalog.js +10 -2
  52. package/server/lib/small-model/catalog.test.js +3 -1
  53. package/server/lib/small-model/index.js +52 -28
  54. package/server/lib/small-model/opencode-session.js +339 -0
  55. package/server/lib/small-model/opencode-session.test.js +177 -0
  56. package/server/lib/small-model/resolve.js +116 -5
  57. package/server/lib/small-model/resolve.test.js +76 -10
  58. package/server/lib/small-model/summary-settings.test.js +60 -3
  59. package/dist/assets/renderMobileApp-DrU0se98.js +0 -23
  60. package/dist/assets/src-DcPZ_rWS.css +0 -2
@@ -1,19 +1,40 @@
1
- import { describe, it, expect } from 'bun:test';
2
- import { resolveSmallModel, parseModelRef, isUsableAuthEntry } from './resolve.js';
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ resolveSmallModel,
4
+ parseModelRef,
5
+ isUsableAuthEntry,
6
+ rankSmallModelCandidates,
7
+ compareSmallModelCandidates,
8
+ } from './resolve.js';
3
9
 
4
10
  const catalog = {
5
11
  google: {
6
12
  id: 'google',
7
13
  models: {
8
- 'gemini-2.5-flash': { id: 'gemini-2.5-flash', family: 'gemini-flash', release_date: '2025-06-01' },
9
- 'gemini-2.0-flash': { id: 'gemini-2.0-flash', family: 'gemini-flash', release_date: '2024-12-01' },
14
+ 'gemini-2.5-flash': {
15
+ id: 'gemini-2.5-flash',
16
+ family: 'gemini-flash',
17
+ release_date: '2025-06-01',
18
+ cost: { input: 0.15, output: 0.6 },
19
+ },
20
+ 'gemini-2.0-flash': {
21
+ id: 'gemini-2.0-flash',
22
+ family: 'gemini-flash',
23
+ release_date: '2024-12-01',
24
+ cost: { input: 0.1, output: 0.4 },
25
+ },
10
26
  'gemini-2.5-pro': { id: 'gemini-2.5-pro', family: 'gemini-pro', release_date: '2025-06-01' },
11
27
  },
12
28
  },
13
29
  anthropic: {
14
30
  id: 'anthropic',
15
31
  models: {
16
- 'claude-haiku-4-5': { id: 'claude-haiku-4-5', family: 'claude-haiku', release_date: '2025-10-01' },
32
+ 'claude-haiku-4-5': {
33
+ id: 'claude-haiku-4-5',
34
+ family: 'claude-haiku',
35
+ release_date: '2025-10-01',
36
+ cost: { input: 1, output: 5 },
37
+ },
17
38
  'claude-sonnet-4-5': { id: 'claude-sonnet-4-5', family: 'claude-sonnet', release_date: '2025-09-01' },
18
39
  },
19
40
  },
@@ -77,7 +98,7 @@ describe('resolveSmallModel', () => {
77
98
  expect(result).toEqual({ providerID: 'openai', modelID: 'gpt-4o-mini', source: 'config' });
78
99
  });
79
100
 
80
- it('scans authenticated providers by family priority, newest first', () => {
101
+ it('scans authenticated providers by family priority, cheapest then newest within family', () => {
81
102
  const result = resolveSmallModel({
82
103
  auth: {
83
104
  google: { type: 'api', key: 'g-key' },
@@ -86,7 +107,8 @@ describe('resolveSmallModel', () => {
86
107
  catalog,
87
108
  configSmallModel: null,
88
109
  });
89
- expect(result).toEqual({ providerID: 'google', modelID: 'gemini-2.5-flash', source: 'family-scan' });
110
+ // gemini-flash outranks other families; within family cheaper input wins.
111
+ expect(result).toEqual({ providerID: 'google', modelID: 'gemini-2.0-flash', source: 'family-scan' });
90
112
  });
91
113
 
92
114
  it('skips providers without a usable credential', () => {
@@ -135,7 +157,7 @@ describe('resolveSmallModel', () => {
135
157
  configSmallModel: null,
136
158
  preferredProviderID: 'anthropic',
137
159
  });
138
- expect(result).toEqual({ providerID: 'google', modelID: 'gemini-2.5-flash', source: 'family-scan' });
160
+ expect(result).toEqual({ providerID: 'google', modelID: 'gemini-2.0-flash', source: 'family-scan' });
139
161
  });
140
162
 
141
163
  it('never uses a session provider without a login (opencode free models)', () => {
@@ -163,7 +185,7 @@ describe('resolveSmallModel', () => {
163
185
  expect(result).toBeNull();
164
186
  });
165
187
 
166
- it('falls back to the session model instead of scanning other providers', () => {
188
+ it('stays on the preferred provider via keyword scan instead of switching providers', () => {
167
189
  const result = resolveSmallModel({
168
190
  auth: {
169
191
  'opencode-go': { type: 'api', key: 'oc-key' },
@@ -181,7 +203,9 @@ describe('resolveSmallModel', () => {
181
203
  preferredProviderID: 'opencode-go',
182
204
  preferredModelID: 'deepseek-v4-flash',
183
205
  });
184
- expect(result).toEqual({ providerID: 'opencode-go', modelID: 'deepseek-v4-flash', source: 'session-model' });
206
+ // No gemini-flash/gpt-nano/claude-haiku family, but "flash" keyword matches
207
+ // within the preferred provider — never silently switch to openai oauth.
208
+ expect(result).toEqual({ providerID: 'opencode-go', modelID: 'deepseek-v4-flash', source: 'keyword-scan' });
185
209
  });
186
210
 
187
211
  it('falls back to the session model itself when nothing resolves', () => {
@@ -194,4 +218,46 @@ describe('resolveSmallModel', () => {
194
218
  });
195
219
  expect(result).toEqual({ providerID: 'mistral', modelID: 'mistral-large-latest', source: 'session-model' });
196
220
  });
221
+
222
+ it('picks keyword+cheapest catalog models when no family match exists', () => {
223
+ const result = resolveSmallModel({
224
+ auth: {
225
+ codebuddy: { type: 'api', key: 'cb-key' },
226
+ deepseek: { type: 'api', key: 'ds-key' },
227
+ },
228
+ catalog: {
229
+ codebuddy: {
230
+ id: 'codebuddy',
231
+ models: {
232
+ 'codebuddy-pro': { id: 'codebuddy-pro', release_date: '2026-01-01', cost: { input: 0.01 } },
233
+ 'codebuddy-mini': { id: 'codebuddy-mini', release_date: '2025-06-01', cost: { input: 0.5 } },
234
+ 'codebuddy-lite': { id: 'codebuddy-lite', release_date: '2025-06-01', cost: { input: 0.05 } },
235
+ },
236
+ },
237
+ deepseek: {
238
+ id: 'deepseek',
239
+ models: {
240
+ 'deepseek-chat': { id: 'deepseek-chat', release_date: '2025-01-01', cost: { input: 0.14 } },
241
+ },
242
+ },
243
+ },
244
+ configSmallModel: null,
245
+ });
246
+ // mini outranks lite/chat; within mini cheaper input would win (only one mini).
247
+ expect(result).toEqual({ providerID: 'codebuddy', modelID: 'codebuddy-mini', source: 'keyword-scan' });
248
+ });
249
+
250
+ it('ranks missing cost after priced models and prefers newer release_date on ties', () => {
251
+ const ranked = rankSmallModelCandidates({
252
+ 'a-mini': { id: 'a-mini', release_date: '2024-01-01', cost: { input: 1 } },
253
+ 'b-mini': { id: 'b-mini', release_date: '2025-01-01', cost: { input: 1 } },
254
+ 'c-mini': { id: 'c-mini', release_date: '2026-01-01' },
255
+ 'd-flash': { id: 'd-flash', release_date: '2023-01-01', cost: { input: 2 } },
256
+ });
257
+ expect(ranked.map((m) => m.id)).toEqual(['d-flash', 'b-mini', 'a-mini', 'c-mini']);
258
+ expect(compareSmallModelCandidates(
259
+ { id: 'x', cost: { input: 1 }, release_date: '2024-01-01' },
260
+ { id: 'y', cost: { input: 2 }, release_date: '2026-01-01' },
261
+ )).toBeLessThan(0);
262
+ });
197
263
  });
@@ -19,8 +19,14 @@ vi.mock('./call.js', () => ({
19
19
  callSmallModel: vi.fn(async () => 'Generated summary'),
20
20
  }));
21
21
 
22
+ vi.mock('./opencode-session.js', () => ({
23
+ generateViaOpenCodeSession: vi.fn(async () => 'Session path summary'),
24
+ stop: vi.fn(async () => {}),
25
+ }));
26
+
22
27
  const { createSmallModelService } = await import('./index.js');
23
28
  const { callSmallModel } = await import('./call.js');
29
+ const { generateViaOpenCodeSession } = await import('./opencode-session.js');
24
30
  const { readAuthFile } = await import('../opencode/auth.js');
25
31
 
26
32
  const createService = (catalog = {}) => createSmallModelService({
@@ -32,6 +38,7 @@ const createService = (catalog = {}) => createSmallModelService({
32
38
  describe('summary AI settings', () => {
33
39
  beforeEach(async () => {
34
40
  vi.mocked(callSmallModel).mockClear();
41
+ vi.mocked(generateViaOpenCodeSession).mockClear();
35
42
  vi.mocked(readAuthFile).mockReturnValue({});
36
43
  await fsPromises.writeFile(path.join(tempRoot, 'settings.json'), JSON.stringify({
37
44
  summaryModelMode: 'custom',
@@ -195,10 +202,11 @@ describe('summary AI settings', () => {
195
202
  expect(await listCallableModels()).toEqual({ openai: ['gpt-5.4-mini'] });
196
203
  });
197
204
 
198
- it('does not list other providers without a catalog model api.url', async () => {
205
+ it('lists logged-in providers with catalog models even without api.url', async () => {
199
206
  vi.mocked(readAuthFile).mockReturnValue({
200
207
  mistral: { type: 'api', key: 'mistral-key' },
201
208
  deepseek: { type: 'api', key: 'deepseek-key' },
209
+ emptyplug: { type: 'api', key: 'empty-key' },
202
210
  });
203
211
 
204
212
  const catalog = {
@@ -219,10 +227,59 @@ describe('summary AI settings', () => {
219
227
  },
220
228
  },
221
229
  },
230
+ emptyplug: {
231
+ id: 'emptyplug',
232
+ name: 'Empty',
233
+ models: {},
234
+ },
222
235
  };
223
236
 
224
237
  const { listCallableProviders, listCallableModels } = createService(catalog);
225
- expect(await listCallableProviders()).toEqual(['deepseek']);
226
- expect(await listCallableModels()).toEqual({ deepseek: ['deepseek-chat'] });
238
+ expect((await listCallableProviders()).sort()).toEqual(['deepseek', 'mistral']);
239
+ expect(await listCallableModels()).toEqual({
240
+ mistral: ['mistral-small'],
241
+ deepseek: ['deepseek-chat'],
242
+ });
243
+ });
244
+
245
+ it('routes non-dedicated providers through the OpenCode session path', async () => {
246
+ vi.mocked(readAuthFile).mockReturnValue({
247
+ codebuddy: { type: 'api', key: 'codebuddy-key' },
248
+ });
249
+ await fsPromises.writeFile(path.join(tempRoot, 'settings.json'), JSON.stringify({
250
+ summaryModelMode: 'provider',
251
+ summaryProviderID: 'codebuddy',
252
+ summaryModelID: 'codebuddy-flash',
253
+ }), 'utf8');
254
+
255
+ const catalog = {
256
+ codebuddy: {
257
+ id: 'codebuddy',
258
+ name: 'CodeBuddy',
259
+ models: {
260
+ 'codebuddy-flash': { id: 'codebuddy-flash' },
261
+ },
262
+ },
263
+ };
264
+
265
+ const { generateSmallModelText } = createService(catalog);
266
+ const result = await generateSmallModelText({
267
+ purpose: 'commit',
268
+ prompt: 'Diff content',
269
+ system: 'Fallback system prompt',
270
+ });
271
+
272
+ expect(callSmallModel).not.toHaveBeenCalled();
273
+ expect(generateViaOpenCodeSession).toHaveBeenCalledWith(expect.objectContaining({
274
+ providerID: 'codebuddy',
275
+ modelID: 'codebuddy-flash',
276
+ purpose: 'commit',
277
+ }));
278
+ expect(result).toEqual({
279
+ text: 'Session path summary',
280
+ providerID: 'codebuddy',
281
+ modelID: 'codebuddy-flash',
282
+ source: 'summary-provider',
283
+ });
227
284
  });
228
285
  });