@openchambery/web 2.0.0-beta.26 → 2.0.0-beta.28

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 (175) hide show
  1. package/dist/assets/{AgentAvatar-FyTdEOMr.js → AgentAvatar-CqD6Pfom.js} +1 -1
  2. package/dist/assets/{AssistantView-DvOO2Kf5.js → AssistantView-BW9f9vF2.js} +3 -3
  3. package/dist/assets/{DiagramView-BqjeSOIg.js → DiagramView-BP3WDmFe.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-DHDffzHs.js → FadeInOnReveal-ezW_e-yz.js} +1 -1
  5. package/dist/assets/{Icon-D7NdMmbi.js → Icon-ChOAx1jX.js} +1 -1
  6. package/dist/assets/{MarkdownRenderer-kpkRqS42.js → MarkdownRenderer-BBHZa1Ts.js} +2 -2
  7. package/dist/assets/{MarkdownRendererImpl-CzBos-Gt.js → MarkdownRendererImpl-DfxEsttZ.js} +2 -2
  8. package/dist/assets/{MarkstreamRendererImpl-RY_rAgKD.js → MarkstreamRendererImpl-6k-n071l.js} +1 -1
  9. package/dist/assets/{MobileDetailNavigation-DgtRh0zu.js → MobileDetailNavigation-s48GkQY9.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-BM3cN-gG.js → MobileShareBridge-DAgShkFE.js} +2 -2
  11. package/dist/assets/MobileSurface-DZug2f4O.js +2 -0
  12. package/dist/assets/{MultiRunWindow-Bq4gz78C.js → MultiRunWindow-CIhHw9mo.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-DMbPMwlp.js → OnboardingScreen-ozxwbzDw.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-mI9WVa7E.js → OverlayScrollbar-BL-QnnrN.js} +1 -1
  15. package/dist/assets/SettingsView-DmUdWSji.js +25 -0
  16. package/dist/assets/{SettingsWindow-CHPjbEnI.js → SettingsWindow-CuIZwGhM.js} +1 -1
  17. package/dist/assets/{TerminalView-DCJuL2nP.js → TerminalView-BIJ-ILgl.js} +1 -1
  18. package/dist/assets/{ToolOutputDialog-Cg_Q1t3X.js → ToolOutputDialog-B6KIxVUo.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-BxuflaQt.js → appThemeRegistry-BuMGUGUK.js} +1 -1
  20. package/dist/assets/{button-e8fYWIMM.js → button-DoDvdeG-.js} +1 -1
  21. package/dist/assets/{collapsible-B6wLXHi3.js → collapsible-CZkyFVK0.js} +1 -1
  22. package/dist/assets/{context-menu-Bq7W1d7y.js → context-menu-DTjON-Ta.js} +1 -1
  23. package/dist/assets/{desktopHosts-C8vf9vRY.js → desktopHosts-CP1YHQ4Z.js} +1 -1
  24. package/dist/assets/{desktopRelayRestore-DDJtljeu.js → desktopRelayRestore-BeOGO7oA.js} +1 -1
  25. package/dist/assets/{diagram-BBL-LfsC.js → diagram-DUi801df.js} +1 -1
  26. package/dist/assets/{dialog-MRsJpHzu.js → dialog-DGzGqOhG.js} +1 -1
  27. package/dist/assets/{es-DvPwAj0y.js → es-DCjl24dV.js} +3 -3
  28. package/dist/assets/fileMentionTouchSelection-C3A6XKRM.js +2 -0
  29. package/dist/assets/{fileQueries-CBu-ytXS.js → fileQueries-CAH8-p27.js} +1 -1
  30. package/dist/assets/{fr-XQsoaNKY.js → fr-DfJ5j0t5.js} +3 -3
  31. package/dist/assets/{gitApi-CuIUGt04.js → gitApi-B1Nyw0PD.js} +3 -3
  32. package/dist/assets/{input-DbOe1mdT.js → input-Ct2uaiDc.js} +1 -1
  33. package/dist/assets/insertionBoundaries-BypTPUfp.js +1 -0
  34. package/dist/assets/{ja-DslzO96s.js → ja-B_9_w2Xf.js} +3 -3
  35. package/dist/assets/{ko-C_9XOAL0.js → ko-DlGbo3kV.js} +3 -3
  36. package/dist/assets/main-BREqmQUm.js +169 -0
  37. package/dist/assets/{main-Sd4Auh_x.js → main-BodZWzei.js} +2 -2
  38. package/dist/assets/main-qSeeJRW1.css +1 -0
  39. package/dist/assets/{markdownHeightCache-B5VSLl79.js → markdownHeightCache-CMoWScss.js} +1 -1
  40. package/dist/assets/{mcpQueries-BYNrkehM.js → mcpQueries-nScpWEqu.js} +1 -1
  41. package/dist/assets/{miniChat-Hp9bNmS0.js → miniChat-Ca-kdWf0.js} +2 -2
  42. package/dist/assets/{mobile-fBLEOx9_.js → mobile-BWi6F8Lj.js} +2 -2
  43. package/dist/assets/multirun-BdSrR6jN.js +3 -0
  44. package/dist/assets/persistence-B3_CPgm1.js +3 -0
  45. package/dist/assets/{pl-Dst9Wk53.js → pl-DjUsS8Ei.js} +3 -3
  46. package/dist/assets/platform-Bxdc4SbV.js +6 -0
  47. package/dist/assets/projectMeta-CIKLMweH.js +1 -0
  48. package/dist/assets/{pt-BR-Dq394BPb.js → pt-BR-B8sV7qo1.js} +3 -3
  49. package/dist/assets/{renderElectronMiniChatApp-D1xipCO5.js → renderElectronMiniChatApp-DtYCIW4n.js} +2 -2
  50. package/dist/assets/renderMobileApp-D47vojsP.js +28 -0
  51. package/dist/assets/{runtimeConfig-pTYamMLy.js → runtimeConfig-u9DrZmkS.js} +1 -1
  52. package/dist/assets/runtimeEndpointReset-Bn4vJ-gl.js +148 -0
  53. package/dist/assets/runtimeEndpointReset-D1_Y5I4F.css +1 -0
  54. package/dist/assets/{sessionChangeSummary-DcnZyuWt.js → sessionChangeSummary-C0OxmHed.js} +2 -2
  55. package/dist/assets/{sessionLookup-B3t-FerN.js → sessionLookup-D1NHRgWf.js} +1 -1
  56. package/dist/assets/{sortable-tabs-strip-B0sZ7mm6.js → sortable-tabs-strip-Cp5i5hI-.js} +1 -1
  57. package/dist/assets/{store-D8fmTWK3.js → store-BTqPn8Uq.js} +3 -3
  58. package/dist/assets/{terminalApi-CJyWCuc8.js → terminalApi-CqyW3_W3.js} +1 -1
  59. package/dist/assets/{textarea-B5GMntHk.js → textarea-jEbc6gqf.js} +1 -1
  60. package/dist/assets/{tooltip-C64SdRvR.js → tooltip-DKYT7xYe.js} +1 -1
  61. package/dist/assets/{uk-8XAR75Qh.js → uk-Bh7HXyLp.js} +3 -3
  62. package/dist/assets/{url-DF1NFwt_.js → url-hhmiocZY.js} +1 -1
  63. package/dist/assets/{useAppFontEffects-BQklk6XB.js → useAppFontEffects-BBX1KjLV.js} +53 -53
  64. package/dist/assets/{useDesktopWindowControlsLayout-CrzYwZDQ.js → useDesktopWindowControlsLayout-uKYzUX-m.js} +1 -1
  65. package/dist/assets/{useEffectiveDirectory-Ddx7sZ5A.js → useEffectiveDirectory-BOolveMp.js} +1 -1
  66. package/dist/assets/{useFontPreferences-CrteQiAp.js → useFontPreferences-BMJQP6t6.js} +1 -1
  67. package/dist/assets/useMobileNavigationStore-CC8QT7VC.js +415 -0
  68. package/dist/assets/useSessionAutoCleanup-CydMAlLf.js +2 -0
  69. package/dist/assets/{useWorkerHighlightedLines-VOtVUGkO.js → useWorkerHighlightedLines-BF1ClmMe.js} +1 -1
  70. package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
  71. package/dist/assets/{zh-CN-B-ILfrqC.js → zh-CN-Uzia0gk4.js} +3 -3
  72. package/dist/assets/{zh-TW-5HeTVP4z.js → zh-TW-CiI7DLdB.js} +3 -3
  73. package/dist/index.html +8 -8
  74. package/dist/mini-chat.html +8 -8
  75. package/dist/mobile.html +8 -8
  76. package/package.json +3 -3
  77. package/server/index.js +2 -20
  78. package/server/lib/git/DOCUMENTATION.md +1 -0
  79. package/server/lib/git/routes.js +7 -4
  80. package/server/lib/git/routes.test.js +34 -0
  81. package/server/lib/git/service.js +3 -1
  82. package/server/lib/llm/DOCUMENTATION.md +6 -4
  83. package/server/lib/llm/generate.js +8 -9
  84. package/server/lib/llm/generate.test.js +33 -6
  85. package/server/lib/opencode/DOCUMENTATION.md +24 -20
  86. package/server/lib/opencode/agents.js +13 -22
  87. package/server/lib/opencode/auth-state-runtime.js +3 -3
  88. package/server/lib/opencode/auth-state-runtime.test.js +59 -0
  89. package/server/lib/opencode/auth.d.ts +13 -0
  90. package/server/lib/opencode/auth.js +74 -64
  91. package/server/lib/opencode/auth.test.js +57 -0
  92. package/server/lib/opencode/commands.js +1 -1
  93. package/server/lib/opencode/config-entities-layering.test.js +72 -0
  94. package/server/lib/opencode/config-entity-routes.js +9 -122
  95. package/server/lib/opencode/config-entity-routes.test.js +7 -11
  96. package/server/lib/opencode/config-files.d.ts +17 -0
  97. package/server/lib/opencode/config-files.js +187 -0
  98. package/server/lib/opencode/config-files.test.js +201 -0
  99. package/server/lib/opencode/core-routes.js +0 -1
  100. package/server/lib/opencode/core-routes.test.js +0 -15
  101. package/server/lib/opencode/credential-boundary.d.ts +1 -0
  102. package/server/lib/opencode/credential-boundary.js +11 -0
  103. package/server/lib/opencode/ensure-cli.js +4 -1
  104. package/server/lib/opencode/env-config.js +4 -1
  105. package/server/lib/opencode/env-config.test.js +28 -0
  106. package/server/lib/opencode/env-runtime.test.js +2 -1
  107. package/server/lib/opencode/feature-routes-runtime.js +9 -7
  108. package/server/lib/opencode/hmr-state-runtime.js +4 -4
  109. package/server/lib/opencode/lifecycle.js +1 -49
  110. package/server/lib/opencode/lifecycle.test.js +30 -4
  111. package/server/lib/opencode/location-release.d.ts +8 -0
  112. package/server/lib/opencode/location-release.js +32 -0
  113. package/server/lib/opencode/location-release.test.js +43 -0
  114. package/server/lib/opencode/mcp.js +1 -1
  115. package/server/lib/opencode/opencode2-pin.js +2 -1
  116. package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
  117. package/server/lib/opencode/plugins.js +12 -60
  118. package/server/lib/opencode/providers.js +6 -3
  119. package/server/lib/opencode/proxy.js +7 -0
  120. package/server/lib/opencode/proxy.test.js +13 -0
  121. package/server/lib/opencode/routes.js +5 -5
  122. package/server/lib/opencode/routes.test.js +18 -18
  123. package/server/lib/opencode/runtime-contract.js +3 -2
  124. package/server/lib/opencode/runtime-contract.test.js +12 -12
  125. package/server/lib/opencode/shared.js +6 -175
  126. package/server/lib/opencode/upgrade-screen.js +4 -11
  127. package/server/lib/opencode/upgrade-screen.test.js +25 -25
  128. package/server/lib/opencode/version-alignment.test.js +40 -0
  129. package/server/lib/quota/DOCUMENTATION.md +9 -0
  130. package/server/lib/quota/provider-credentials.test.js +29 -0
  131. package/server/lib/quota/providers/claude.js +4 -4
  132. package/server/lib/quota/providers/codex.js +4 -4
  133. package/server/lib/quota/providers/codex.test.js +1 -1
  134. package/server/lib/quota/providers/copilot.js +5 -5
  135. package/server/lib/quota/providers/google/auth.js +3 -3
  136. package/server/lib/quota/providers/google/index.js +2 -2
  137. package/server/lib/quota/providers/index.js +3 -7
  138. package/server/lib/quota/providers/index.test.js +2 -2
  139. package/server/lib/quota/providers/kimi.js +4 -4
  140. package/server/lib/quota/providers/minimax-shared.js +4 -4
  141. package/server/lib/quota/providers/nanogpt.js +4 -4
  142. package/server/lib/quota/providers/openai.js +4 -4
  143. package/server/lib/quota/providers/openrouter.js +4 -4
  144. package/server/lib/quota/providers/wafer.js +4 -4
  145. package/server/lib/quota/providers/zai.js +4 -4
  146. package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
  147. package/server/lib/quota/routes.js +1 -1
  148. package/server/lib/scheduled-tasks/DOCUMENTATION.md +1 -0
  149. package/server/lib/scheduled-tasks/runtime.js +2 -3
  150. package/server/lib/session-index/DOCUMENTATION.md +7 -0
  151. package/server/lib/session-index/event-ingest.js +13 -0
  152. package/server/lib/session-index/event-ingest.test.js +7 -0
  153. package/server/lib/session-index/service.js +45 -0
  154. package/server/lib/session-index/service.test.js +47 -0
  155. package/server/lib/tts/DOCUMENTATION.md +7 -5
  156. package/server/lib/tts/credential-source.test.js +23 -0
  157. package/server/lib/tts/routes.js +2 -2
  158. package/server/lib/tts/service.js +16 -37
  159. package/dist/assets/MobileSurface-ocVWenHA.js +0 -2
  160. package/dist/assets/SettingsView-CIZz40Jg.js +0 -25
  161. package/dist/assets/fileMentionTouchSelection-Bj67pPo7.js +0 -3
  162. package/dist/assets/insertionBoundaries-nzs1Q3YR.js +0 -1
  163. package/dist/assets/main-D2BzpnUt.css +0 -1
  164. package/dist/assets/main-DjilfYN7.js +0 -169
  165. package/dist/assets/multirun-D7W7oCj4.js +0 -4
  166. package/dist/assets/persistence-CMWO3jFo.js +0 -3
  167. package/dist/assets/platform-B0p43squ.js +0 -6
  168. package/dist/assets/projectMeta-DrUKrYt0.js +0 -3
  169. package/dist/assets/renderMobileApp-DSk7Qs-_.js +0 -28
  170. package/dist/assets/runtimeEndpointReset-B5HEMuiB.js +0 -149
  171. package/dist/assets/useMobileNavigationStore-i4cXNoOf.js +0 -414
  172. package/dist/assets/useSessionAutoCleanup-Dzd0fGSe.js +0 -2
  173. package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
  174. package/server/lib/opencode/snippets.js +0 -242
  175. package/server/lib/opencode/snippets.test.js +0 -97
@@ -14,7 +14,7 @@ import {
14
14
  asNonEmptyString,
15
15
  toTimestamp
16
16
  } from '../../utils/index.js';
17
- import { readAuthFile } from '../../../opencode/auth.js';
17
+ import { readOpenCodeCredentials } from '../../../opencode/auth.js';
18
18
  import { parseGoogleRefreshToken } from './transforms.js';
19
19
 
20
20
  const ANTIGRAVITY_GOOGLE_CLIENT_ID =
@@ -90,8 +90,8 @@ const resolveAntigravityAuth = () => {
90
90
  return null;
91
91
  };
92
92
 
93
- export const resolveGoogleAuthSources = () => {
94
- const auth = readAuthFile();
93
+ export const resolveGoogleAuthSources = async () => {
94
+ const auth = await readOpenCodeCredentials();
95
95
  const sources = [];
96
96
 
97
97
  const geminiAuth = resolveGeminiCliAuth(auth);
@@ -17,10 +17,10 @@ export const providerId = 'google';
17
17
  export const providerName = 'Google';
18
18
  export const aliases = ['google', 'google.oauth'];
19
19
 
20
- export const isConfigured = () => resolveGoogleAuthSources().length > 0;
20
+ export const isConfigured = async () => (await resolveGoogleAuthSources()).length > 0;
21
21
 
22
22
  export const fetchGoogleQuota = async () => {
23
- const authSources = resolveGoogleAuthSources();
23
+ const authSources = await resolveGoogleAuthSources();
24
24
  if (!authSources.length) {
25
25
  return buildResult({
26
26
  providerId,
@@ -130,16 +130,12 @@ const registry = {
130
130
  }
131
131
  };
132
132
 
133
- export const listConfiguredQuotaProviders = () => {
133
+ export const listConfiguredQuotaProviders = async () => {
134
134
  const configured = [];
135
135
 
136
136
  for (const [id, provider] of Object.entries(registry)) {
137
- try {
138
- if (provider.isConfigured()) {
139
- configured.push(id);
140
- }
141
- } catch {
142
- // Ignore provider-specific config errors in list API.
137
+ if (await provider.isConfigured()) {
138
+ configured.push(id);
143
139
  }
144
140
  }
145
141
 
@@ -11,7 +11,7 @@ describe('quota provider registry', () => {
11
11
  expect(typeof google.resolveGoogleAuthSources).toBe('function');
12
12
  });
13
13
 
14
- it('can list configured providers without missing provider exports', () => {
15
- expect(() => listConfiguredQuotaProviders()).not.toThrow();
14
+ it('reports unavailable credentials instead of an empty configured-provider list', async () => {
15
+ await expect(listConfiguredQuotaProviders()).rejects.toThrow('Unable to read OpenCode credentials');
16
16
  });
17
17
  });
@@ -1,4 +1,4 @@
1
- import { readAuthFile } from '../../opencode/auth.js';
1
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import {
3
3
  getAuthEntry,
4
4
  normalizeAuthEntry,
@@ -14,14 +14,14 @@ export const providerId = 'kimi-for-coding';
14
14
  export const providerName = 'Kimi for Coding';
15
15
  const aliases = ['kimi-for-coding', 'kimi'];
16
16
 
17
- export const isConfigured = () => {
18
- const auth = readAuthFile();
17
+ export const isConfigured = async () => {
18
+ const auth = await readOpenCodeCredentials();
19
19
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
20
20
  return Boolean(entry?.key || entry?.token);
21
21
  };
22
22
 
23
23
  export const fetchQuota = async () => {
24
- const auth = readAuthFile();
24
+ const auth = await readOpenCodeCredentials();
25
25
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
26
26
  const apiKey = entry?.key ?? entry?.token;
27
27
 
@@ -1,4 +1,4 @@
1
- import { readAuthFile } from '../../opencode/auth.js';
1
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import {
3
3
  getAuthEntry,
4
4
  normalizeAuthEntry,
@@ -138,14 +138,14 @@ const calculateUsage = (model, isTokenPlan) => {
138
138
  };
139
139
 
140
140
  export const createMiniMaxCodingPlanProvider = ({ providerId, providerName, aliases, tokenPlanUrl, codingPlanUrl }) => {
141
- const isConfigured = () => {
142
- const auth = readAuthFile();
141
+ const isConfigured = async () => {
142
+ const auth = await readOpenCodeCredentials();
143
143
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
144
144
  return Boolean(entry?.key || entry?.token);
145
145
  };
146
146
 
147
147
  const fetchQuota = async () => {
148
- const auth = readAuthFile();
148
+ const auth = await readOpenCodeCredentials();
149
149
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
150
150
  const apiKey = entry?.key ?? entry?.token;
151
151
 
@@ -1,4 +1,4 @@
1
- import { readAuthFile } from '../../opencode/auth.js';
1
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import {
3
3
  getAuthEntry,
4
4
  normalizeAuthEntry,
@@ -14,14 +14,14 @@ export const providerId = 'nano-gpt';
14
14
  export const providerName = 'NanoGPT';
15
15
  const aliases = ['nano-gpt', 'nanogpt', 'nano_gpt'];
16
16
 
17
- export const isConfigured = () => {
18
- const auth = readAuthFile();
17
+ export const isConfigured = async () => {
18
+ const auth = await readOpenCodeCredentials();
19
19
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
20
20
  return Boolean(entry?.key || entry?.token);
21
21
  };
22
22
 
23
23
  export const fetchQuota = async () => {
24
- const auth = readAuthFile();
24
+ const auth = await readOpenCodeCredentials();
25
25
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
26
26
  const apiKey = entry?.key ?? entry?.token;
27
27
 
@@ -1,4 +1,4 @@
1
- import { readAuthFile } from '../../opencode/auth.js';
1
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import {
3
3
  getAuthEntry,
4
4
  normalizeAuthEntry,
@@ -12,14 +12,14 @@ const providerId = 'openai';
12
12
  const providerName = 'OpenAI';
13
13
  const aliases = ['openai', 'codex', 'chatgpt'];
14
14
 
15
- const isConfigured = () => {
16
- const auth = readAuthFile();
15
+ const isConfigured = async () => {
16
+ const auth = await readOpenCodeCredentials();
17
17
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
18
18
  return Boolean(entry?.access || entry?.token);
19
19
  };
20
20
 
21
21
  export const fetchQuota = async () => {
22
- const auth = readAuthFile();
22
+ const auth = await readOpenCodeCredentials();
23
23
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
24
24
  const accessToken = entry?.access ?? entry?.token;
25
25
 
@@ -1,4 +1,4 @@
1
- import { readAuthFile } from '../../opencode/auth.js';
1
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import {
3
3
  getAuthEntry,
4
4
  normalizeAuthEntry,
@@ -12,14 +12,14 @@ export const providerId = 'openrouter';
12
12
  export const providerName = 'OpenRouter';
13
13
  const aliases = ['openrouter'];
14
14
 
15
- export const isConfigured = () => {
16
- const auth = readAuthFile();
15
+ export const isConfigured = async () => {
16
+ const auth = await readOpenCodeCredentials();
17
17
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
18
18
  return Boolean(entry?.key || entry?.token);
19
19
  };
20
20
 
21
21
  export const fetchQuota = async () => {
22
- const auth = readAuthFile();
22
+ const auth = await readOpenCodeCredentials();
23
23
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
24
24
  const apiKey = entry?.key ?? entry?.token;
25
25
 
@@ -1,4 +1,4 @@
1
- import { readAuthFile } from '../../opencode/auth.js';
1
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import {
3
3
  getAuthEntry,
4
4
  normalizeAuthEntry,
@@ -17,14 +17,14 @@ const aliases = ['wafer', 'wafer-ai', 'wafer_ai', 'wafer.ai'];
17
17
  const WAFER_QUOTA_URL = 'https://pass.wafer.ai/v1/inference/quota';
18
18
  const WAFER_WINDOW_SECONDS = 5 * 3600;
19
19
 
20
- export const isConfigured = () => {
21
- const auth = readAuthFile();
20
+ export const isConfigured = async () => {
21
+ const auth = await readOpenCodeCredentials();
22
22
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
23
23
  return Boolean(entry?.key || entry?.token);
24
24
  };
25
25
 
26
26
  export const fetchQuota = async () => {
27
- const auth = readAuthFile();
27
+ const auth = await readOpenCodeCredentials();
28
28
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
29
29
  const apiKey = entry?.key ?? entry?.token;
30
30
 
@@ -1,4 +1,4 @@
1
- import { readAuthFile } from '../../opencode/auth.js';
1
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
2
2
  import {
3
3
  getAuthEntry,
4
4
  normalizeAuthEntry,
@@ -15,14 +15,14 @@ export const providerId = 'zai-coding-plan';
15
15
  export const providerName = 'z.ai';
16
16
  const aliases = ['zai-coding-plan', 'zai', 'z.ai'];
17
17
 
18
- export const isConfigured = () => {
19
- const auth = readAuthFile();
18
+ export const isConfigured = async () => {
19
+ const auth = await readOpenCodeCredentials();
20
20
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
21
21
  return Boolean(entry?.key || entry?.token);
22
22
  };
23
23
 
24
24
  export const fetchQuota = async () => {
25
- const auth = readAuthFile();
25
+ const auth = await readOpenCodeCredentials();
26
26
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
27
27
  const apiKey = entry?.key ?? entry?.token;
28
28
 
@@ -25,8 +25,7 @@
25
25
  * @property {number} [nextResetTime]
26
26
  * @property {Array<{modelCode: string, usage: number}>} [usageDetails]
27
27
  */
28
- import { readAuthFile } from '../../opencode/auth.js';
29
- import { readConfigLayers } from '../../opencode/shared.js';
28
+ import { readOpenCodeCredentials } from '../../opencode/auth.js';
30
29
  import {
31
30
  getAuthEntry,
32
31
  normalizeAuthEntry,
@@ -40,8 +39,8 @@ export const providerId = 'zhipuai-coding-plan';
40
39
  export const providerName = 'Zhipu AI Coding Plan';
41
40
  const aliases = ['zhipuai-coding-plan', 'zhipuai', 'zhipu'];
42
41
 
43
- function getApiKey() {
44
- const auth = readAuthFile();
42
+ async function getApiKey() {
43
+ const auth = await readOpenCodeCredentials();
45
44
  const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
46
45
  const apiKeyFromAuth = entry?.key ?? entry?.token;
47
46
 
@@ -49,28 +48,15 @@ function getApiKey() {
49
48
  return apiKeyFromAuth;
50
49
  }
51
50
 
52
- try {
53
- const { mergedConfig } = readConfigLayers();
54
-
55
- for (const alias of aliases) {
56
- const providerConfig = mergedConfig?.provider?.[alias];
57
- if (providerConfig?.options?.apiKey) {
58
- return providerConfig.options.apiKey;
59
- }
60
- }
61
- } catch {
62
- // Ignore config read errors; the provider will be treated as not configured.
63
- }
64
-
65
51
  return null;
66
52
  }
67
53
 
68
- export const isConfigured = () => {
69
- return Boolean(getApiKey());
54
+ export const isConfigured = async () => {
55
+ return Boolean(await getApiKey());
70
56
  };
71
57
 
72
58
  export const fetchQuota = async () => {
73
- const apiKey = getApiKey();
59
+ const apiKey = await getApiKey();
74
60
 
75
61
  if (!apiKey) {
76
62
  return buildResult({
@@ -28,7 +28,7 @@ export function registerQuotaRoutes(app, { getQuotaProviders }) {
28
28
  app.get('/api/quota/providers', async (_req, res) => {
29
29
  try {
30
30
  const { listConfiguredQuotaProviders } = await getQuotaProviders();
31
- res.json({ providers: listConfiguredQuotaProviders() });
31
+ res.json({ providers: await listConfiguredQuotaProviders() });
32
32
  } catch (error) {
33
33
  console.error('Failed to list quota providers:', error);
34
34
  res.status(500).json({ error: error.message || 'Failed to list quota providers' });
@@ -8,6 +8,7 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
8
8
  - Runtime orchestration and execution is owned by this module.
9
9
  - Durable run history is owned by an independent SQLite store in this module.
10
10
  - This module is OpenChamber feature logic; it is intentionally separate from OpenCode proxy/runtime internals.
11
+ - Prompt and goal execution preserve hash-prefixed text literally; no snippet discovery or expansion occurs before dispatch.
11
12
 
12
13
  ## Files
13
14
 
@@ -2,7 +2,6 @@ import crypto from 'node:crypto';
2
2
  import { DateTime } from 'luxon';
3
3
  import parser from 'cron-parser';
4
4
  import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
5
- import { expandSnippets } from '../opencode/snippets.js';
6
5
  import {
7
6
  getSessionGoalCapability,
8
7
  isSessionGoalSupported,
@@ -858,7 +857,7 @@ export const createScheduledTasksRuntime = (deps) => {
858
857
  };
859
858
 
860
859
  const buildPromptText = (task, projectPath) => {
861
- const promptText = expandSnippets(task.execution.prompt, projectPath);
860
+ const promptText = task.execution.prompt;
862
861
  if (!task.execution.goalEnabled) {
863
862
  return promptText;
864
863
  }
@@ -883,7 +882,7 @@ export const createScheduledTasksRuntime = (deps) => {
883
882
  // back to an inline objective so parseGoalMetadata still accepts the
884
883
  // record. Oversized prompts are distilled into audit criteria by the
885
884
  // small model; on distill failure a head+tail excerpt keeps intent.
886
- let objectiveText = expandSnippets(task.execution.prompt, projectPath);
885
+ let objectiveText = task.execution.prompt;
887
886
  if (objectiveText.length > 5000) {
888
887
  let distilled = null;
889
888
  try {
@@ -1,5 +1,12 @@
1
1
  # Session Index Runtime
2
2
 
3
+ Native `session.renamed` updates the existing runtime-scoped summary's title and
4
+ update clock directly, publishing through the normal index revision path. It
5
+ preserves activity ordering, pins and execution status; older/duplicate events
6
+ and unknown sessions are no-ops. Partial rename payloads never fabricate rows.
7
+
8
+ Native `session.moved` uses `data.location.directory`, not the old event-envelope directory. The transactional `move` operation retains summary activity/status and independent pin membership, maintains the newest-20 destination bound, and installs a runtime-scoped event-clock fence against delayed source-directory upserts/pages. Duplicate/older moves are no-ops. Unknown sessions are not fabricated from the partial move payload.
9
+
3
10
  Every OpenChamber Web Server enables `session-index.sqlite` in its data
4
11
  directory by default. Electron injects its user-data path explicitly.
5
12
  `sessionIndexDbPath` and `OPENCHAMBER_SESSION_INDEX_DB_PATH` override that
@@ -36,6 +36,19 @@ export const applySessionIndexEvent = (service, event, observedAt = Date.now(),
36
36
 
37
37
  const baseType = normalizeSessionEventType(payload.type);
38
38
 
39
+ if (baseType === 'session.renamed') {
40
+ if (isHostReady && !isHostReady()) return false;
41
+ const data = payload.data ?? properties;
42
+ return service.rename(data.sessionID, data.title, payload.created ?? properties.eventCreated ?? observedAt);
43
+ }
44
+
45
+ if (baseType === 'session.moved') {
46
+ const data = payload.data ?? properties;
47
+ return typeof data.sessionID === 'string' && typeof data.location?.directory === 'string'
48
+ ? service.move(data.sessionID, data.location.directory, typeof payload.created === 'number' ? payload.created : observedAt, payload.durable?.seq)
49
+ : false;
50
+ }
51
+
39
52
  if (baseType === 'session.created' || baseType === 'session.updated') {
40
53
  // Until Host metadata is ready, do not write session lifecycle into the
41
54
  // index (would bake unprojected upstream archive). Transcript paths use
@@ -3,6 +3,13 @@ import { describe, expect, it, vi } from 'vitest';
3
3
  import { applySessionIndexEvent } from './event-ingest.js';
4
4
 
5
5
  describe('session index event ingest', () => {
6
+ it('takes moved ownership from native data rather than the old event envelope directory', () => {
7
+ const service = { move: vi.fn(() => true) };
8
+ expect(applySessionIndexEvent(service, { directory: '/old', payload: {
9
+ type: 'session.moved', created: 20, data: { sessionID: 'ses_move', location: { directory: '/new' }, projectID: 'project' },
10
+ } }, 30)).toBe(true);
11
+ expect(service.move).toHaveBeenCalledWith('ses_move', '/new', 20, undefined);
12
+ });
6
13
  it('writes session summaries, user activity, and status to the service', () => {
7
14
  const service = {
8
15
  upsertAndReportChange: vi.fn(() => true),
@@ -277,6 +277,30 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
277
277
  WHERE runtime_key = ? AND session_id = ?
278
278
  `);
279
279
 
280
+ const movedLocations = new Map();
281
+ const isStaleLocation = (session, directory) => {
282
+ const moved = movedLocations.get(`${runtimeKey()}:${session?.id}`);
283
+ return moved && moved.directory !== normalizeDirectory(directory) && toTimestamp(session?.time?.updated) <= moved.at;
284
+ };
285
+ const move = db.transaction((sessionID, directory, at = Date.now(), sequence) => {
286
+ directory = normalizeDirectory(directory);
287
+ if (!sessionID || !directory) return false;
288
+ const key = runtimeKey();
289
+ const fenceKey = `${key}:${sessionID}`;
290
+ const previous = movedLocations.get(fenceKey);
291
+ if (previous && (sequence !== undefined && previous.sequence !== undefined
292
+ ? sequence <= previous.sequence
293
+ : at < previous.at || (at === previous.at && previous.directory === directory))) return false;
294
+ movedLocations.set(fenceKey, { directory, at, sequence });
295
+ touchExistingDirectory.run({ runtimeKey: key, directory, lastAccessedAt: at });
296
+ const roots = db.prepare('UPDATE OR REPLACE session_summary SET directory = ? WHERE runtime_key = ? AND session_id = ? AND directory != ?').run(directory, key, sessionID, directory);
297
+ const children = db.prepare('UPDATE OR REPLACE session_child SET directory = ? WHERE runtime_key = ? AND session_id = ? AND directory != ?').run(directory, key, sessionID, directory);
298
+ db.prepare(`DELETE FROM session_summary WHERE runtime_key = ? AND directory = ? AND session_id IN (
299
+ SELECT session_id FROM session_summary WHERE runtime_key = ? AND directory = ? ORDER BY activity_updated_at DESC, session_id DESC LIMIT -1 OFFSET ?
300
+ )`).run(key, directory, key, directory, MAX_ROOT_SESSIONS);
301
+ return roots.changes > 0 || children.changes > 0;
302
+ });
303
+
280
304
  const replaceDirectoryRows = ({ directory, sessions, cursor, hasMore, fullSync = true, now = Date.now() }) => {
281
305
  const normalizedDirectory = normalizeDirectory(directory);
282
306
  if (!normalizedDirectory) throw new Error('directory is required');
@@ -289,6 +313,7 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
289
313
  // stale unarchived cache cannot resurrect them.
290
314
  const summaries = Array.isArray(sessions)
291
315
  ? sessions
316
+ .filter((session) => !isStaleLocation(session, normalizedDirectory))
292
317
  .map((session) => toSummary(session, normalizedDirectory))
293
318
  .filter((summary) => summary && !summary.archivedAt)
294
319
  .slice(0, MAX_ROOT_SESSIONS)
@@ -328,6 +353,7 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
328
353
  });
329
354
 
330
355
  const upsertMutation = db.transaction((session, now = Date.now(), options = {}) => {
356
+ if (isStaleLocation(session, session?.directory ?? session?.location?.directory)) return { accepted: false, changed: false };
331
357
  if (!isVisibleSession(session)) {
332
358
  const changed = remove(session?.id);
333
359
  return { accepted: changed, changed };
@@ -517,6 +543,23 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
517
543
  return result.changes > 0;
518
544
  };
519
545
 
546
+ const rename = (sessionID, title, observedAt) => {
547
+ if (typeof sessionID !== 'string' || !sessionID || typeof title !== 'string') return false;
548
+ const timestamp = toTimestamp(observedAt);
549
+ if (!timestamp) return false;
550
+ const key = runtimeKey();
551
+ if (HIDDEN_SESSION_TITLES.has(title)) {
552
+ const current = db.prepare('SELECT updated_at FROM session_summary WHERE runtime_key = ? AND session_id = ?').get(key, sessionID);
553
+ return current && current.updated_at <= timestamp ? remove(sessionID) : false;
554
+ }
555
+ const result = db.prepare(`
556
+ UPDATE session_summary SET title = ?, updated_at = ?
557
+ WHERE runtime_key = ? AND session_id = ? AND updated_at <= ?
558
+ AND (title != ? OR updated_at != ?)
559
+ `).run(title, timestamp, key, sessionID, timestamp, title, timestamp);
560
+ return result.changes > 0;
561
+ };
562
+
520
563
  const updateStatus = (sessionID, status, observedAt) => {
521
564
  if (typeof sessionID !== 'string' || !sessionID) return false;
522
565
  if (status !== 'busy' && status !== 'retry' && status !== 'idle') return false;
@@ -570,6 +613,8 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
570
613
  upsertAndReportChange,
571
614
  touchActivity,
572
615
  updateStatus,
616
+ rename,
617
+ move,
573
618
  setPinned,
574
619
  clearPinned,
575
620
  remove,
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { afterEach, describe, expect, it } from 'vitest';
6
6
 
7
7
  import { createSessionIndexService } from './service.js';
8
+ import { applySessionIndexEvent } from './event-ingest.js';
8
9
 
9
10
  const tempDirectories = [];
10
11
 
@@ -31,6 +32,52 @@ afterEach(() => {
31
32
  });
32
33
 
33
34
  describe('Electron session index', () => {
35
+ it('ingests native renames without changing activity, status, pins or unrelated sessions', () => {
36
+ const runtimeRef = { value: 'http://runtime-a.test' };
37
+ const service = createService(runtimeRef);
38
+ service.upsert(session('ses_title', 10));
39
+ service.upsert(session('ses_other', 15));
40
+ service.setPinned('ses_title', 12);
41
+ service.updateStatus('ses_title', 'busy', 13);
42
+ const before = service.snapshot();
43
+ const rename = (title, created, sessionID = 'ses_title') => applySessionIndexEvent(service, {
44
+ payload: { type: 'session.renamed', created, data: { sessionID, title } },
45
+ });
46
+ expect(rename('Generated title', 20)).toBe(true);
47
+ const after = service.snapshot();
48
+ expect(after.directories[0].sessions.map((row) => row.id)).toEqual(before.directories[0].sessions.map((row) => row.id));
49
+ expect(service.findBySessionId('ses_title')).toMatchObject({ title: 'Generated title' });
50
+ expect(after.pinnedSessionIds).toEqual(before.pinnedSessionIds);
51
+ expect(after.directories[0].sessions.find((row) => row.id === 'ses_title').metadata).toEqual(
52
+ before.directories[0].sessions.find((row) => row.id === 'ses_title').metadata,
53
+ );
54
+ expect(rename('Generated title', 20)).toBe(false);
55
+ expect(rename('Stale title', 19)).toBe(false);
56
+ expect(rename('Unknown', 30, 'ses_missing')).toBe(false);
57
+ runtimeRef.value = 'http://runtime-b.test';
58
+ expect(rename('Other runtime', 30)).toBe(false);
59
+ service.close();
60
+ });
61
+ it('moves one authoritative identity, preserving pin/status and fencing delayed old-directory pages', () => {
62
+ const runtimeRef = { value: 'http://runtime-a.test' };
63
+ const service = createService(runtimeRef);
64
+ service.upsert(session('ses_move', 10, '/old'));
65
+ service.setPinned('ses_move', 12);
66
+ service.updateStatus('ses_move', 'retry', 15);
67
+ expect(service.move('ses_move', '/new', 20)).toBe(true);
68
+ expect(service.move('ses_move', '/new', 20)).toBe(false);
69
+ expect(service.move('ses_move', '/old', 19)).toBe(false);
70
+ service.replaceDirectory({ directory: '/old', sessions: [session('ses_move', 10, '/old')], now: 30 });
71
+ expect(service.upsertAndReportChange(session('ses_move', 10, '/old'), 40)).toBe(false);
72
+ const rows = service.snapshot().directories.flatMap((item) => item.sessions);
73
+ expect(rows).toHaveLength(1);
74
+ expect(rows[0]).toMatchObject({ id: 'ses_move', directory: '/new', metadata: { openchamber: { sessionStatus: { type: 'retry' } } } });
75
+ expect(service.snapshot().pinnedSessionIds).toContain('ses_move');
76
+ runtimeRef.value = 'http://runtime-b.test';
77
+ expect(service.upsert(session('ses_move', 1, '/old'))).toBe(true);
78
+ expect(service.findBySessionId('ses_move').directory).toBe('/old');
79
+ service.close();
80
+ });
34
81
  it('stores one bounded root-session page transactionally', () => {
35
82
  const runtimeRef = { value: 'http://runtime-a.test' };
36
83
  const service = createService(runtimeRef);
@@ -43,7 +43,8 @@ This module provides server-side Text-to-Speech services using OpenAI's TTS API.
43
43
  ## TTSService methods
44
44
 
45
45
  ### `isAvailable()`
46
- Returns boolean indicating whether OpenAI API key is configured (checks environment variable `OPENAI_API_KEY` or OpenCode auth file).
46
+ Returns a Promise of boolean after reading the current OpenCode selected credential.
47
+ Credential-source failure rejects; logout clears the previously constructed client.
47
48
 
48
49
  ### `generateSpeechStream(options)`
49
50
  Generates speech and returns as a web stream for direct streaming to clients.
@@ -81,10 +82,11 @@ Returns object with:
81
82
  Returns Buffer containing MP3 audio data.
82
83
 
83
84
  ## API key resolution
84
- OpenAI API keys are resolved in order:
85
- 1. Environment variable `OPENAI_API_KEY`.
86
- 2. OpenCode auth file (`auth.openai`, `auth.codex`, or `auth.chatgpt`).
87
- 3. Supports both string format (just token) and object format (with `access` or `token` fields).
85
+ Explicit request API key/custom base URL remains supported. Otherwise the host
86
+ reads the current OpenCode credential API (`openai`, `codex`, `chatgpt` aliases),
87
+ using API `key` or OAuth `access`. There is no host-local environment/file fallback
88
+ that could substitute another account for a remote service. A credential read
89
+ failure propagates; selected-account changes replace the client and logout clears it.
88
90
 
89
91
  ## Usage in web server
90
92
  The TTS module is used by `packages/web/server/index.js` for:
@@ -0,0 +1,23 @@
1
+ import { afterEach, expect, it } from 'vitest';
2
+ import { configureOpenCodeCredentials } from '../opencode/auth.js';
3
+ import { TTSService } from './service.js';
4
+
5
+ afterEach(() => configureOpenCodeCredentials(null));
6
+
7
+ it('TTS clears its old client after logout and obtains a new client after account switching', async () => {
8
+ let token = 'first';
9
+ configureOpenCodeCredentials(() => ({ credential: { list: async () => token ? [{ id: 'selected', integrationID: 'openai', active: true, value: { type: 'key', key: token } }] : [] } }));
10
+ const service = new TTSService();
11
+ const first = await service._getClient();
12
+ expect(await service.isAvailable()).toBe(true);
13
+ token = 'second';
14
+ expect(await service._getClient()).not.toBe(first);
15
+ token = '';
16
+ expect(await service.isAvailable()).toBe(false);
17
+ expect(await service._getClient()).toBeNull();
18
+ });
19
+
20
+ it('TTS does not present credential read failure as unavailable success', async () => {
21
+ configureOpenCodeCredentials(() => ({ credential: { list: async () => { throw new Error('private'); } } }));
22
+ await expect(new TTSService().isAvailable()).rejects.toThrow(/^Unable to read OpenCode credentials$/);
23
+ });
@@ -62,7 +62,7 @@ export function registerTtsRoutes(app, { sayTTSCapability }) {
62
62
  const { ttsService } = await getTtsModule();
63
63
 
64
64
  // Check availability - server-configured key, client-provided key, or custom server URL
65
- const hasServerKey = ttsService.isAvailable();
65
+ const hasServerKey = await ttsService.isAvailable();
66
66
  const hasClientKey = apiKey && typeof apiKey === 'string' && apiKey.trim().length > 0;
67
67
  const hasCustomBaseURL = typeof normalizedBaseURL === 'string' && normalizedBaseURL.length > 0;
68
68
 
@@ -134,7 +134,7 @@ export function registerTtsRoutes(app, { sayTTSCapability }) {
134
134
  try {
135
135
  const { ttsService } = await getTtsModule();
136
136
  res.json({
137
- available: ttsService.isAvailable(),
137
+ available: await ttsService.isAvailable(),
138
138
  voices: [
139
139
  'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable',
140
140
  'nova', 'onyx', 'sage', 'shimmer', 'verse', 'marin', 'cedar'