@openchambery/web 1.19.7-beta.9 → 1.19.8-beta.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 (203) hide show
  1. package/dist/assets/AgentAvatar-CX0a1rKE.js +1 -0
  2. package/dist/assets/AssistantView-DkXW9Fzq.js +2 -0
  3. package/dist/assets/DiagramView-C8MZY0fA.js +1 -0
  4. package/dist/assets/FadeInOnReveal-CK4asqVR.js +1 -0
  5. package/dist/assets/Icon-BYM02QyU.js +1 -0
  6. package/dist/assets/MarkdownLoadingSkeleton-DWs4Jg7b.js +3 -0
  7. package/dist/assets/MarkdownRenderer-cxYw8-8G.js +2 -0
  8. package/dist/assets/MarkdownRendererImpl-B39nwiZX.js +7 -0
  9. package/dist/assets/MarkstreamRendererImpl-CdYqdejg.js +1 -0
  10. package/dist/assets/MarkstreamRendererImpl-CsNwy1T5.css +1 -0
  11. package/dist/assets/MobileDetailNavigation-D6ri-ePj.js +1 -0
  12. package/dist/assets/MobileShareBridge-HRTN71wg.js +4 -0
  13. package/dist/assets/MobileSurface-D2yHjnZw.js +2 -0
  14. package/dist/assets/{MultiRunWindow-Ohj6UbWj.js → MultiRunWindow-CTRgfYQD.js} +1 -1
  15. package/dist/assets/OnboardingScreen-C3-wzeSX.js +1 -0
  16. package/dist/assets/OverlayScrollbar-KUpvYHdv.js +2 -0
  17. package/dist/assets/SettingsView-ImI02YSp.js +25 -0
  18. package/dist/assets/{SettingsWindow-B4lzhAbO.js → SettingsWindow-DWo2lUaN.js} +1 -1
  19. package/dist/assets/TerminalView-BseZ5dR-.js +8 -0
  20. package/dist/assets/ToolOutputDialog-BIK3cPqo.js +10 -0
  21. package/dist/assets/appThemeRegistry-4_bwYAWt.js +1 -0
  22. package/dist/assets/{button-z0GOBtWi.js → button-C0cth9tl.js} +1 -1
  23. package/dist/assets/clipboard-DJ4nQEuY.js +1 -0
  24. package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-DFT9cE80.js} +1 -1
  25. package/dist/assets/context-menu-CAp74NfV.js +1 -0
  26. package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
  27. package/dist/assets/desktopRelayRestore-9EbBYQ9s.js +1 -0
  28. package/dist/assets/{diagram-CoEqmReY.js → diagram-BryzMuta.js} +1 -1
  29. package/dist/assets/{dialog-x2hirWBd.js → dialog-CgbV5ljg.js} +1 -1
  30. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
  31. package/dist/assets/{es-C8Sqjsia.js → es-BHqnSjoZ.js} +1 -1
  32. package/dist/assets/fileQueries-BY-9OWzp.js +1 -0
  33. package/dist/assets/fileReferenceActions-BJJtX_S5.js +1 -0
  34. package/dist/assets/{fr-p-Zwm_j2.js → fr-BfrlJY9i.js} +1 -1
  35. package/dist/assets/{gitApi-CtS4kWoo.js → gitApi-m09MWmzE.js} +3 -3
  36. package/dist/assets/{input-BG3VWTIF.js → input-DFLpqjGR.js} +1 -1
  37. package/dist/assets/insertionBoundaries-B0fzEL22.js +4 -0
  38. package/dist/assets/{ja-B6REu1qP.js → ja-DP-7qt-x.js} +1 -1
  39. package/dist/assets/{ko-CJBNH9IZ.js → ko-E2B5vGQP.js} +1 -1
  40. package/dist/assets/main-BYX7_XVg.js +169 -0
  41. package/dist/assets/main-BpbNOMNf.js +2 -0
  42. package/dist/assets/markdown-worker-BuveaH3O.js +1 -0
  43. package/dist/assets/markdownTheme-CzZm3riB.js +2 -0
  44. package/dist/assets/miniChat-CFBgCVoZ.js +2 -0
  45. package/dist/assets/mobile-DcZkxRBz.js +2 -0
  46. package/dist/assets/multirun-CBqJQgS2.js +4 -0
  47. package/dist/assets/outsideFileGrants-DnplRxr-.js +1 -0
  48. package/dist/assets/path-utils-jGozTg9d.js +1 -0
  49. package/dist/assets/persistence-DdUac14I.js +3 -0
  50. package/dist/assets/{pl-B2cBI_7K.js → pl-r8X5gRM5.js} +1 -1
  51. package/dist/assets/platform-B4KKLnZ5.js +2 -0
  52. package/dist/assets/projectMeta-Ct1qHZhU.js +3 -0
  53. package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-DxmxYdiE.js +2 -0
  55. package/dist/assets/renderMobileApp-XXw7Fnrr.js +28 -0
  56. package/dist/assets/runtimeConfig-CcIqxMFB.js +2 -0
  57. package/dist/assets/runtimeEndpointReset-CU1HhJfC.js +147 -0
  58. package/dist/assets/sessionChangeSummary-DOq8EN_H.js +1 -0
  59. package/dist/assets/sessionLookup-CNMNR-yh.js +1 -0
  60. package/dist/assets/sortable-tabs-strip-C8_9kHqu.js +1 -0
  61. package/dist/assets/src-DasvfkEg.css +2 -0
  62. package/dist/assets/{store-CL3WP48T.js → store-BDFdnk6l.js} +2 -2
  63. package/dist/assets/terminalApi-947l7Ra3.js +1 -0
  64. package/dist/assets/textarea-Cgo_nWUU.js +1 -0
  65. package/dist/assets/toolHelpers-DEvVP9U0.js +2 -0
  66. package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BifLJ8o0.js} +1 -1
  67. package/dist/assets/{uk-C8YKAz0x.js → uk-Di0vl9V-.js} +1 -1
  68. package/dist/assets/url-Pk6tQ1EZ.js +1 -0
  69. package/dist/assets/{ChatContainer-CDExXwgn.js → useAppFontEffects-CBLEH9it.js} +44 -39
  70. package/dist/assets/useAssistantUIStore-me6s63b2.js +1 -0
  71. package/dist/assets/useDesktopWindowControlsLayout-DMCmF610.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-CMvw1bLA.js +1 -0
  73. package/dist/assets/useFontPreferences-BCOBlaAd.js +1 -0
  74. package/dist/assets/useMobileNavigationStore-Di9IWKfu.js +413 -0
  75. package/dist/assets/useSessionAutoCleanup-DMyMP_xI.js +1 -0
  76. package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
  77. package/dist/assets/useWorkerHighlightedLines-uNzY5rmU.js +7 -0
  78. package/dist/assets/utils-BiK3tBGM.js +1 -0
  79. package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
  80. package/dist/assets/{zh-CN-CCYwUnmB.js → zh-CN-coFCfeQE.js} +1 -1
  81. package/dist/assets/{zh-TW-D8Y6py08.js → zh-TW-oDRZ1-yb.js} +1 -1
  82. package/dist/index.html +9 -11
  83. package/dist/mini-chat.html +9 -11
  84. package/dist/mobile.html +15 -17
  85. package/package.json +2 -1
  86. package/server/index.js +2 -0
  87. package/server/lib/assistant-delivery-parts.js +3 -1
  88. package/server/lib/assistant-delivery-parts.test.js +11 -0
  89. package/server/lib/assistants/DOCUMENTATION.md +77 -4
  90. package/server/lib/assistants/assign.js +225 -0
  91. package/server/lib/assistants/assign.test.js +118 -0
  92. package/server/lib/assistants/bubbles.js +45 -0
  93. package/server/lib/assistants/bubbles.test.js +36 -0
  94. package/server/lib/assistants/cards.js +148 -0
  95. package/server/lib/assistants/cards.test.js +116 -0
  96. package/server/lib/assistants/contact-store.js +284 -0
  97. package/server/lib/assistants/contact-store.test.js +131 -0
  98. package/server/lib/assistants/contact-tools.js +764 -0
  99. package/server/lib/assistants/contact-tools.test.js +337 -0
  100. package/server/lib/assistants/contracts.js +20 -1
  101. package/server/lib/assistants/harness.js +575 -0
  102. package/server/lib/assistants/harness.test.js +697 -0
  103. package/server/lib/assistants/pi-tools.js +118 -0
  104. package/server/lib/assistants/pi-tools.test.js +114 -0
  105. package/server/lib/assistants/routes.js +44 -4
  106. package/server/lib/assistants/service.js +930 -17
  107. package/server/lib/assistants/service.test.js +1163 -45
  108. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  109. package/server/lib/event-stream/directory-ws-bridge.js +11 -2
  110. package/server/lib/event-stream/global-ws-bridge.js +19 -3
  111. package/server/lib/event-stream/protocol.js +11 -1
  112. package/server/lib/event-stream/reasoning-projection.js +425 -0
  113. package/server/lib/event-stream/reasoning-projection.test.js +300 -0
  114. package/server/lib/event-stream/runtime.js +10 -0
  115. package/server/lib/event-stream/runtime.test.js +53 -0
  116. package/server/lib/llm/DOCUMENTATION.md +84 -0
  117. package/server/lib/llm/catalog.js +100 -0
  118. package/server/lib/llm/catalog.test.js +78 -0
  119. package/server/lib/llm/completions.js +151 -0
  120. package/server/lib/llm/completions.test.js +127 -0
  121. package/server/lib/llm/generate.js +570 -0
  122. package/server/lib/llm/generate.test.js +459 -0
  123. package/server/lib/llm/routes.js +47 -0
  124. package/server/lib/llm/temp-directory.js +37 -0
  125. package/server/lib/notifications/APNS.md +19 -13
  126. package/server/lib/notifications/DOCUMENTATION.md +4 -3
  127. package/server/lib/notifications/runtime.js +148 -37
  128. package/server/lib/notifications/runtime.test.js +100 -0
  129. package/server/lib/opencode/DOCUMENTATION.md +2 -1
  130. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  131. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  132. package/server/lib/opencode/proxy.js +103 -9
  133. package/server/lib/opencode/proxy.test.js +177 -1
  134. package/server/lib/opencode/settings-runtime.js +2 -2
  135. package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
  136. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  137. package/server/lib/session-goal/runtime.js +20 -0
  138. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  139. package/server/lib/session-index/service.js +10 -4
  140. package/server/lib/session-index/service.test.js +37 -0
  141. package/server/lib/session-title/runtime.js +1 -0
  142. package/server/lib/session-title/runtime.test.js +11 -0
  143. package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
  144. package/server/lib/session-turn-pages/routes.js +22 -3
  145. package/server/lib/session-turn-pages/routes.test.js +43 -0
  146. package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
  147. package/server/lib/transcript-cache/routes.js +9 -2
  148. package/server/lib/transcript-cache/routes.test.js +32 -0
  149. package/dist/assets/AgentAvatar-Di8F4QJh.js +0 -1
  150. package/dist/assets/AssistantView-DSxVAAb_.js +0 -1
  151. package/dist/assets/ContextPanel-DgwEIdqR.js +0 -285
  152. package/dist/assets/DiagramView-BKQO61y8.js +0 -1
  153. package/dist/assets/FadeInOnReveal-CV2s6YN8.js +0 -1
  154. package/dist/assets/Icon-CLpn1mIz.js +0 -1
  155. package/dist/assets/MarkdownRendererImpl-CTgmghSh.js +0 -7
  156. package/dist/assets/MarkstreamRendererImpl-BKsc-avF.js +0 -1
  157. package/dist/assets/MarkstreamRendererImpl-CxeWpDCK.css +0 -1
  158. package/dist/assets/MobileDetailNavigation-BcA6rd8Q.js +0 -1
  159. package/dist/assets/MobileShareBridge-w6T2ZrsW.js +0 -4
  160. package/dist/assets/MobileSurface-CG1n7v0h.js +0 -2
  161. package/dist/assets/OnboardingScreen-D6rtE7rj.js +0 -1
  162. package/dist/assets/OverlayScrollbar-DlNZho3q.js +0 -2
  163. package/dist/assets/SettingsView-CiF3r83e.js +0 -25
  164. package/dist/assets/TerminalView-Ctx_Jt_b.js +0 -8
  165. package/dist/assets/ToolOutputDialog-Dv9_fUYl.js +0 -10
  166. package/dist/assets/appThemeRegistry-DnwUIQB9.js +0 -2
  167. package/dist/assets/clipboard-D35XSwID.js +0 -1
  168. package/dist/assets/desktop-CfLlmafK.js +0 -1
  169. package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
  170. package/dist/assets/desktopRelayRestore-8pvI_dSc.js +0 -1
  171. package/dist/assets/fileQueries-DGHZqZHc.js +0 -1
  172. package/dist/assets/insertionBoundaries-hK4mQj4W.js +0 -4
  173. package/dist/assets/main-BmbZFoO4.js +0 -29
  174. package/dist/assets/main-BwNhn8ml.js +0 -2
  175. package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
  176. package/dist/assets/markdownTheme-SPe4jOwX.js +0 -2
  177. package/dist/assets/miniChat-BN89siC3.js +0 -2
  178. package/dist/assets/mobile-DE9rVo5D.js +0 -2
  179. package/dist/assets/multirun-BiEuyDq3.js +0 -4
  180. package/dist/assets/persistence-w-AiX2i4.js +0 -3
  181. package/dist/assets/platform-4eCmsdol.js +0 -2
  182. package/dist/assets/projectMeta-DSLKcH1a.js +0 -3
  183. package/dist/assets/radio-HD5AB2Qf.js +0 -1
  184. package/dist/assets/renderElectronMiniChatApp-f_UxpFjv.js +0 -2
  185. package/dist/assets/renderMobileApp-BAhCai6d.js +0 -28
  186. package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
  187. package/dist/assets/runtimeConfig-qG_oNBoa.js +0 -2
  188. package/dist/assets/runtimeEndpointReset-BDeKD3EM.js +0 -4
  189. package/dist/assets/shikiHighlight-BphLZnac.js +0 -1
  190. package/dist/assets/sortable-tabs-strip-CNDgyX0B.js +0 -1
  191. package/dist/assets/src-DZt1nWf5.css +0 -2
  192. package/dist/assets/terminalApi-CGtOtc8I.js +0 -1
  193. package/dist/assets/url-KeihbE0d.js +0 -1
  194. package/dist/assets/useAppFontEffects--0dwVTSm.js +0 -7
  195. package/dist/assets/useAssistantUIStore-I0VWrB5V.js +0 -1
  196. package/dist/assets/useDesktopWindowControlsLayout-DsbeBLvv.js +0 -1
  197. package/dist/assets/useEffectiveDirectory-CHvvohcG.js +0 -1
  198. package/dist/assets/useFontPreferences-BLZ3y6mN.js +0 -1
  199. package/dist/assets/useMobileNavigationStore-peTDuC00.js +0 -412
  200. package/dist/assets/useSessionAutoCleanup-D_ZC9E5g.js +0 -1
  201. package/dist/assets/useUIStore-CNLmCoPU.js +0 -1
  202. package/dist/assets/useWorkerHighlightedLines-B1YzuMYR.js +0 -7
  203. package/dist/assets/utils-CTL16t2f.js +0 -1
@@ -0,0 +1,337 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AssignError, ASSIGN_CODES, PROJECT_REQUIRED_MESSAGE } from './assign.js';
3
+ import {
4
+ ASSIGN_SESSION_TOOL_NAME,
5
+ CREATE_ASSISTANT_TOOL_NAME,
6
+ LIST_PROJECTS_TOOL_NAME,
7
+ LIST_SESSIONS_TOOL_NAME,
8
+ MESSAGE_ASSISTANT_TOOL_NAME,
9
+ NEW_CONVERSATION_CONFIRM_BUBBLE,
10
+ NEW_CONVERSATION_TOOL_NAME,
11
+ SCHEDULE_TASK_TOOL_NAME,
12
+ confirmBubbleAfterContactReset,
13
+ createContactTools,
14
+ detectRequestedContactTools,
15
+ filterRegisteredProjects,
16
+ formatContactToolsPrompt,
17
+ formatRegisteredProjectsPrompt,
18
+ matchesProjectQuery,
19
+ parseContactToolCalls,
20
+ resolveContactProviderModel,
21
+ resolvePeerAssistant,
22
+ stripContactToolFences,
23
+ } from './contact-tools.js';
24
+
25
+ describe('contact tool protocol', () => {
26
+ it('parses an assign_session fence and strips it from chat text', () => {
27
+ const text = 'On it.\n\n```openchamber-tool\n{"name":"assign_session","arguments":{"prompt":"Fix login","projectPath":"/repo"}}\n```';
28
+ const parsed = parseContactToolCalls(text, [ASSIGN_SESSION_TOOL_NAME]);
29
+ expect(parsed.chatText).toBe('On it.');
30
+ expect(parsed.toolCall).toEqual({
31
+ name: ASSIGN_SESSION_TOOL_NAME,
32
+ arguments: { prompt: 'Fix login', projectPath: '/repo' },
33
+ });
34
+ expect(stripContactToolFences(text)).toBe('On it.');
35
+ });
36
+
37
+ it('parses create_assistant and schedule_task fences from natural-language replies', () => {
38
+ const create = parseContactToolCalls(
39
+ '好。\n\n```openchamber-tool\n{"name":"create_assistant","arguments":{"name":"FlowQA","model":"opencode-go/deepseek-v4-flash"}}\n```',
40
+ [CREATE_ASSISTANT_TOOL_NAME, SCHEDULE_TASK_TOOL_NAME, ASSIGN_SESSION_TOOL_NAME],
41
+ );
42
+ expect(create.chatText).toBe('好。');
43
+ expect(create.toolCall).toEqual({
44
+ name: CREATE_ASSISTANT_TOOL_NAME,
45
+ arguments: { name: 'FlowQA', model: 'opencode-go/deepseek-v4-flash' },
46
+ });
47
+ const schedule = parseContactToolCalls(
48
+ '```openchamber-tool\n{"name":"schedule_task","arguments":{"name":"Daily ping","prompt":"ping","time":"18:00","timezone":"Asia/Shanghai"}}\n```',
49
+ [CREATE_ASSISTANT_TOOL_NAME, SCHEDULE_TASK_TOOL_NAME],
50
+ );
51
+ const peer = parseContactToolCalls(
52
+ '```openchamber-tool\n{"name":"message_assistant","arguments":{"to":"PeerQA","text":"hello-from-assistant 写好了"}}\n```',
53
+ [MESSAGE_ASSISTANT_TOOL_NAME],
54
+ );
55
+ expect(peer.toolCall).toEqual({
56
+ name: MESSAGE_ASSISTANT_TOOL_NAME,
57
+ arguments: { to: 'PeerQA', text: 'hello-from-assistant 写好了' },
58
+ });
59
+ expect(schedule.toolCall).toEqual({
60
+ name: SCHEDULE_TASK_TOOL_NAME,
61
+ arguments: { name: 'Daily ping', prompt: 'ping', time: '18:00', timezone: 'Asia/Shanghai' },
62
+ });
63
+ });
64
+
65
+ it('parses a bare {name, arguments} object anywhere in the assistant text', () => {
66
+ const parsed = parseContactToolCalls(
67
+ '好的,我来直接创建这个助理,不开编码会话。\n{"name":"create_assistant","arguments":{"name":"FlowNL","model":"opencode-go/deepseek-v4-flash"}}',
68
+ [CREATE_ASSISTANT_TOOL_NAME, ASSIGN_SESSION_TOOL_NAME],
69
+ );
70
+ expect(parsed.toolCall).toEqual({
71
+ name: CREATE_ASSISTANT_TOOL_NAME,
72
+ arguments: { name: 'FlowNL', model: 'opencode-go/deepseek-v4-flash' },
73
+ });
74
+ expect(parsed.chatText).toContain('好的,我来直接创建这个助理');
75
+ expect(parsed.chatText).not.toContain('create_assistant');
76
+ });
77
+
78
+ it('detects 建助理 without treating 不要开编码 session as assign_session', () => {
79
+ const tools = [
80
+ NEW_CONVERSATION_TOOL_NAME,
81
+ LIST_PROJECTS_TOOL_NAME,
82
+ LIST_SESSIONS_TOOL_NAME,
83
+ CREATE_ASSISTANT_TOOL_NAME,
84
+ SCHEDULE_TASK_TOOL_NAME,
85
+ MESSAGE_ASSISTANT_TOOL_NAME,
86
+ ASSIGN_SESSION_TOOL_NAME,
87
+ ];
88
+ expect(detectRequestedContactTools('帮我新建一个助理,名叫 FlowNL,不要开编码 session', tools)).toEqual([
89
+ CREATE_ASSISTANT_TOOL_NAME,
90
+ ]);
91
+ expect(detectRequestedContactTools('每天 18:00 排一个 ping 定时任务', tools)).toEqual([SCHEDULE_TASK_TOOL_NAME]);
92
+ expect(detectRequestedContactTools('建会话写一个文件', tools)).toEqual([ASSIGN_SESSION_TOOL_NAME]);
93
+ expect(detectRequestedContactTools('写一个文件', tools)).toEqual([]);
94
+ expect(detectRequestedContactTools('pwd', tools)).toEqual([]);
95
+ expect(detectRequestedContactTools('给 PeerQA 说一声 hello-from-assistant 写好了', tools)).toEqual([
96
+ MESSAGE_ASSISTANT_TOOL_NAME,
97
+ ]);
98
+ expect(detectRequestedContactTools('开新对话', tools)).toEqual([NEW_CONVERSATION_TOOL_NAME]);
99
+ expect(detectRequestedContactTools('new conversation please', tools)).toEqual([NEW_CONVERSATION_TOOL_NAME]);
100
+ expect(detectRequestedContactTools('clear chat', tools)).toEqual([NEW_CONVERSATION_TOOL_NAME]);
101
+ expect(detectRequestedContactTools('找项目 openchamber yee', tools)).toEqual([LIST_PROJECTS_TOOL_NAME]);
102
+ expect(detectRequestedContactTools('看看现有对话', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
103
+ expect(detectRequestedContactTools('list sessions in that project', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
104
+ expect(detectRequestedContactTools('开新对话', tools)).not.toContain(ASSIGN_SESSION_TOOL_NAME);
105
+ expect(detectRequestedContactTools('不要开编码 session', tools)).toEqual([]);
106
+ });
107
+
108
+ it('parses bash fences when the pi coding tools are allowed', () => {
109
+ const text = '```openchamber-tool\n{"name":"bash","arguments":{"command":"pwd"}}\n```';
110
+ expect(parseContactToolCalls(text, ['bash', ASSIGN_SESSION_TOOL_NAME]).toolCall).toEqual({
111
+ name: 'bash',
112
+ arguments: { command: 'pwd' },
113
+ });
114
+ expect(parseContactToolCalls(
115
+ '```openchamber-tool\n{"name":"glob","arguments":{"pattern":"*"}}\n```',
116
+ ['glob', 'bash'],
117
+ ).toolCall).toBeNull();
118
+ });
119
+
120
+ it('tells DeepSeek to call tools from natural language, not slash commands', () => {
121
+ const prompt = formatContactToolsPrompt(createContactTools());
122
+ expect(prompt).toContain('new_conversation');
123
+ expect(prompt).toContain('list_projects');
124
+ expect(prompt).toContain('list_sessions');
125
+ expect(prompt).toContain('create_assistant');
126
+ expect(prompt).toContain('schedule_task');
127
+ expect(prompt).toContain('message_assistant');
128
+ expect(prompt).toContain('assign_session');
129
+ expect(prompt).toContain('开新对话');
130
+ expect(prompt).toContain('找项目');
131
+ expect(prompt).toContain('现有对话');
132
+ expect(prompt).toContain('建助理');
133
+ expect(prompt).toContain('排定时任务');
134
+ expect(prompt).toContain('说一声');
135
+ expect(prompt).toContain('session/new');
136
+ expect(prompt).not.toContain('/card');
137
+ expect(prompt).not.toContain('/dm');
138
+ expect(prompt).toContain('A reply without the tool call does nothing');
139
+ expect(prompt).toContain('已创建');
140
+ expect(prompt).toContain('Never claim you cannot see projects');
141
+ });
142
+
143
+ it('fuzzy-matches project labels like openchamber yee / openchamer yee', () => {
144
+ const project = { id: 'p1', path: '/Users/me/Code/sample-app', label: 'OpenChamber Yee' };
145
+ expect(matchesProjectQuery(project, 'openchamber yee')).toBe(true);
146
+ expect(matchesProjectQuery(project, 'openchamer yee')).toBe(true);
147
+ expect(matchesProjectQuery(project, 'missing')).toBe(false);
148
+ expect(filterRegisteredProjects([project, { id: 'p2', path: '/other' }], 'openchamber')).toEqual([
149
+ { id: 'p1', path: '/Users/me/Code/sample-app', label: 'OpenChamber Yee' },
150
+ ]);
151
+ expect(formatRegisteredProjectsPrompt([project])).toContain('OpenChamber Yee');
152
+ expect(formatRegisteredProjectsPrompt([project])).toContain('never say you cannot see registered projects');
153
+ expect(formatRegisteredProjectsPrompt([])).toContain('none');
154
+ });
155
+
156
+ it('resolves provider/model from a combined OpenCode id', () => {
157
+ expect(resolveContactProviderModel({ model: 'opencode-go/deepseek-v4-flash' })).toEqual({
158
+ providerID: 'opencode-go',
159
+ modelID: 'deepseek-v4-flash',
160
+ });
161
+ expect(resolveContactProviderModel({}, { providerID: 'p', modelID: 'm' })).toEqual({
162
+ providerID: 'p',
163
+ modelID: 'm',
164
+ });
165
+ });
166
+ });
167
+
168
+ describe('createContactTools', () => {
169
+ it('exposes create_assistant, schedule_task, and assign_session and returns success cards', async () => {
170
+ const onCard = vi.fn();
171
+ const resetContact = vi.fn(async () => ({ reset: true }));
172
+ const tools = createContactTools({
173
+ resetContact,
174
+ createAssistant: async (input) => ({
175
+ id: 'asst_flow',
176
+ name: input.name,
177
+ providerID: input.providerID,
178
+ modelID: input.modelID,
179
+ mode: 'continuous',
180
+ }),
181
+ scheduleTask: async (input) => ({
182
+ taskID: 'task_1',
183
+ projectID: 'proj_1',
184
+ name: input.name,
185
+ kind: 'daily',
186
+ time: input.time,
187
+ timezone: input.timezone,
188
+ prompt: input.prompt,
189
+ }),
190
+ assignWork: async () => ({
191
+ sessionID: 'ses_1',
192
+ directory: '/repo',
193
+ title: 'Login',
194
+ status: 'busy',
195
+ }),
196
+ deliverPeerMessage: async (input) => ({
197
+ admitted: true,
198
+ role: 'peer',
199
+ toAssistantID: input.toAssistantID,
200
+ }),
201
+ listAssistants: async () => [{ id: 'asst_peer', name: 'PeerQA' }],
202
+ currentAssistant: { id: 'asst_host', providerID: 'p', modelID: 'm' },
203
+ onCard,
204
+ });
205
+ expect(tools.map((tool) => tool.name)).toEqual([
206
+ NEW_CONVERSATION_TOOL_NAME,
207
+ LIST_PROJECTS_TOOL_NAME,
208
+ LIST_SESSIONS_TOOL_NAME,
209
+ CREATE_ASSISTANT_TOOL_NAME,
210
+ SCHEDULE_TASK_TOOL_NAME,
211
+ MESSAGE_ASSISTANT_TOOL_NAME,
212
+ ASSIGN_SESSION_TOOL_NAME,
213
+ ]);
214
+ expect(tools.some((tool) => ['bash', 'edit', 'read', 'write'].includes(tool.name))).toBe(false);
215
+
216
+ const reset = await tools.find((tool) => tool.name === NEW_CONVERSATION_TOOL_NAME).execute('call_0', {});
217
+ expect(resetContact).toHaveBeenCalledTimes(1);
218
+ expect(reset.details.card).toBeUndefined();
219
+ expect(reset.details.reset).toBe(true);
220
+ expect(reset.terminate).toBe(true);
221
+ expect(reset.content[0].text).toBe(NEW_CONVERSATION_CONFIRM_BUBBLE);
222
+
223
+ const created = await tools.find((tool) => tool.name === CREATE_ASSISTANT_TOOL_NAME).execute('call_1', { name: 'FlowQA', model: 'opencode-go/deepseek-v4-flash' });
224
+ expect(created.details.card).toMatchObject({
225
+ type: 'card',
226
+ cardType: 'assistant',
227
+ assistantID: 'asst_flow',
228
+ name: 'FlowQA',
229
+ providerID: 'opencode-go',
230
+ modelID: 'deepseek-v4-flash',
231
+ });
232
+ expect(created.terminate).toBe(false);
233
+
234
+ const scheduled = await tools.find((tool) => tool.name === SCHEDULE_TASK_TOOL_NAME).execute('call_2', {
235
+ name: 'Daily ping',
236
+ prompt: 'ping',
237
+ time: '18:00',
238
+ timezone: 'Asia/Shanghai',
239
+ });
240
+ expect(scheduled.details.card).toMatchObject({
241
+ type: 'card',
242
+ cardType: 'schedule',
243
+ taskID: 'task_1',
244
+ name: 'Daily ping',
245
+ time: '18:00',
246
+ });
247
+
248
+ const messaged = await tools.find((tool) => tool.name === MESSAGE_ASSISTANT_TOOL_NAME).execute('call_3', {
249
+ to: 'PeerQA',
250
+ text: 'hello-from-assistant 写好了',
251
+ });
252
+ expect(messaged.details.card).toBeUndefined();
253
+ expect(messaged.content[0].text).toContain('Sent to PeerQA');
254
+ expect(messaged.details.toAssistantID).toBe('asst_peer');
255
+
256
+ const assigned = await tools.find((tool) => tool.name === ASSIGN_SESSION_TOOL_NAME).execute('call_4', { prompt: 'Fix login' });
257
+ expect(assigned.details.card).toMatchObject({
258
+ type: 'card',
259
+ cardType: 'session',
260
+ sessionID: 'ses_1',
261
+ title: 'Login',
262
+ });
263
+ expect(onCard).toHaveBeenCalledTimes(3);
264
+ });
265
+
266
+ it('resolves a peer by name and rejects a missing recipient', () => {
267
+ const listed = [{ id: 'asst_peer', name: 'PeerQA' }, { id: 'asst_host', name: 'DeepSeekQA' }];
268
+ expect(resolvePeerAssistant({ to: 'PeerQA' }, listed, { id: 'asst_host' })).toEqual(listed[0]);
269
+ expect(() => resolvePeerAssistant({ to: 'Missing' }, listed, { id: 'asst_host' })).toThrow(/No assistant named/);
270
+ expect(() => resolvePeerAssistant({ to: 'DeepSeekQA' }, listed, { id: 'asst_host' })).toThrow(/same assistant/);
271
+ });
272
+
273
+ it('returns a clear project_required result instead of throwing', async () => {
274
+ const tools = createContactTools({
275
+ assignWork: async () => {
276
+ throw new AssignError(ASSIGN_CODES.PROJECT_REQUIRED, PROJECT_REQUIRED_MESSAGE);
277
+ },
278
+ });
279
+ const result = await tools.find((tool) => tool.name === ASSIGN_SESSION_TOOL_NAME).execute('call_1', { prompt: 'Fix login' });
280
+ expect(result.details.error).toBe('project_required');
281
+ expect(result.content[0].text).toContain('Add a project in Settings');
282
+ expect(result.content[0].text).toContain('assistant-workspaces');
283
+ });
284
+
285
+ it('lists projects and sessions and surfaces list_sessions failure', async () => {
286
+ const tools = createContactTools({
287
+ listProjects: async () => [
288
+ { id: 'proj_yee', path: '/repo/sample-app', label: 'OpenChamber Yee' },
289
+ { id: 'proj_other', path: '/repo/other', label: 'Other' },
290
+ ],
291
+ listSessions: async () => ({
292
+ sessions: [
293
+ { sessionID: 'ses_1', title: 'Login fix', directory: '/repo/sample-app', updatedAt: 2 },
294
+ ],
295
+ truncated: false,
296
+ }),
297
+ });
298
+ const listed = await tools.find((tool) => tool.name === LIST_PROJECTS_TOOL_NAME).execute('call_p', {
299
+ query: 'openchamer yee',
300
+ });
301
+ expect(listed.details.projects).toEqual([
302
+ { id: 'proj_yee', path: '/repo/sample-app', label: 'OpenChamber Yee' },
303
+ ]);
304
+ const sessions = await tools.find((tool) => tool.name === LIST_SESSIONS_TOOL_NAME).execute('call_s', {
305
+ projectPath: '/repo/sample-app',
306
+ });
307
+ expect(sessions.details.sessions).toEqual([
308
+ expect.objectContaining({ sessionID: 'ses_1', title: 'Login fix' }),
309
+ ]);
310
+
311
+ const failing = createContactTools({
312
+ listSessions: async () => {
313
+ throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session index is unavailable.');
314
+ },
315
+ });
316
+ const failed = await failing.find((tool) => tool.name === LIST_SESSIONS_TOOL_NAME).execute('call_fail', {});
317
+ expect(failed.details.error).toBe('upstream_error');
318
+ expect(failed.content[0].text).toContain('Session index is unavailable');
319
+ expect(failed.details.sessions).toBeUndefined();
320
+ });
321
+ });
322
+
323
+ describe('confirmBubbleAfterContactReset', () => {
324
+ it('keeps the canonical confirm and drops leftover attachment bubbles', () => {
325
+ expect(confirmBubbleAfterContactReset([
326
+ NEW_CONVERSATION_CONFIRM_BUBBLE,
327
+ 'I still see your dot.png and note.txt.',
328
+ 'Those attachments are still in context.',
329
+ ])).toEqual([NEW_CONVERSATION_CONFIRM_BUBBLE]);
330
+ });
331
+
332
+ it('uses the canonical confirm when leftover text is the only extracted bubble', () => {
333
+ expect(confirmBubbleAfterContactReset([
334
+ 'I still see your dot.png and note.txt from earlier.',
335
+ ])).toEqual([NEW_CONVERSATION_CONFIRM_BUBBLE]);
336
+ });
337
+ });
@@ -1,9 +1,28 @@
1
1
  export const assistantContractFixtures = Object.freeze({
2
- assistant: Object.freeze({ id: 'assistant_fixture', revision: 4, enabled: true, name: 'Fixture', defaultPrompt: '', workspacePath: null, managedWorkspacePath: '/data/assistant-workspaces/assistant_fixture', effectiveWorkspacePath: '/workspace', providerID: 'provider_fixture', modelID: 'model_fixture', agent: null, variant: null, mode: 'continuous', sessionID: 'ses_fixture', sessionGeneration: 4, historySessionIDs: Object.freeze([]), historySessionCount: 0, createdAt: 1234, updatedAt: 1234, tombstoneAt: null }),
2
+ assistant: Object.freeze({ id: 'assistant_fixture', revision: 4, enabled: true, name: 'Fixture', defaultPrompt: '', workspacePath: null, managedWorkspacePath: '/data/assistant-workspaces/assistant_fixture', effectiveWorkspacePath: '/workspace', providerID: 'provider_fixture', modelID: 'model_fixture', agent: null, variant: null, mode: 'continuous', sessionID: 'ses_fixture', sessionGeneration: 4, historySessionIDs: Object.freeze([]), historySessionCount: 0, assignedSessionIDs: Object.freeze([]), working: false, createdAt: 1234, updatedAt: 1234, tombstoneAt: null }),
3
3
  sessionBinding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }),
4
4
  compactResponse: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), summarized: true }),
5
5
  messageAdmission: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), messageID: 'msg_fixture', admitted: true }),
6
6
  abortResponse: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), aborted: true }),
7
7
  shareOperation: Object.freeze({ operationID: 'share_fixture', assistantID: 'assistant_fixture', sessionID: 'ses_fixture', messageID: 'msg_fixture', state: 'running', phase: 'submitted', attempt: 1, leaseExpiresAt: 1234, errorCode: null }),
8
8
  historicalMessages: Object.freeze({ entries: Object.freeze([Object.freeze({ sessionID: 'ses_fixture', directory: null, info: Object.freeze({ id: 'msg_fixture', sessionID: 'ses_fixture', role: 'assistant', time: Object.freeze({ created: 1234 }) }), parts: Object.freeze([]) })]), nextCursor: null, complete: true }),
9
+ assistantScheduledTasks: Object.freeze({
10
+ tasks: Object.freeze([
11
+ Object.freeze({
12
+ assistantID: 'assistant_fixture',
13
+ projectID: 'proj_fixture',
14
+ taskID: 'task_fixture',
15
+ createdAt: 1234,
16
+ projectPath: '/workspace',
17
+ projectLabel: 'Fixture',
18
+ task: Object.freeze({
19
+ id: 'task_fixture',
20
+ name: 'Daily ping',
21
+ enabled: true,
22
+ schedule: Object.freeze({ kind: 'daily', time: '18:00', timezone: 'Asia/Shanghai' }),
23
+ execution: Object.freeze({ prompt: 'ping', providerID: 'provider_fixture', modelID: 'model_fixture' }),
24
+ }),
25
+ }),
26
+ ]),
27
+ }),
9
28
  });