@openchambery/web 1.19.7-beta.8 → 1.19.7

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 (201) hide show
  1. package/dist/assets/AgentAvatar-CX0a1rKE.js +1 -0
  2. package/dist/assets/AssistantView-nVF0kdFh.js +2 -0
  3. package/dist/assets/DiagramView-B2UtQ8mw.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-DT_GN1Ex.js +2 -0
  8. package/dist/assets/MarkdownRendererImpl-Cslh4XN_.js +7 -0
  9. package/dist/assets/MarkstreamRendererImpl-CsNwy1T5.css +1 -0
  10. package/dist/assets/MarkstreamRendererImpl-Ymm12x6J.js +1 -0
  11. package/dist/assets/MobileDetailNavigation-DD29ENgU.js +1 -0
  12. package/dist/assets/MobileShareBridge-CrTe_QND.js +4 -0
  13. package/dist/assets/MobileSurface-DjxchpUC.js +2 -0
  14. package/dist/assets/{MultiRunWindow-D_jo8cqz.js → MultiRunWindow-jG5dCEMH.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-UKlEewdq.js +25 -0
  18. package/dist/assets/{SettingsWindow-CzMoz2fi.js → SettingsWindow-DO1aAplT.js} +1 -1
  19. package/dist/assets/TerminalView-BG1UPLsz.js +8 -0
  20. package/dist/assets/ToolOutputDialog-CWr5eVTK.js +10 -0
  21. package/dist/assets/appThemeRegistry-BI5drVpC.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-BE0vA4zm.js → gitApi-D9hFlE3T.js} +3 -3
  36. package/dist/assets/{input-BG3VWTIF.js → input-DFLpqjGR.js} +1 -1
  37. package/dist/assets/insertionBoundaries-D2L7D0Oe.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-BRedOs_V.js +169 -0
  41. package/dist/assets/main-DlXpJ0Vw.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-CBLbFpfy.js +2 -0
  45. package/dist/assets/mobile-CoZsWdWk.js +2 -0
  46. package/dist/assets/multirun-BF-I0r2_.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-2t4L5_4a.js +3 -0
  53. package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-fl7O3iHy.js +2 -0
  55. package/dist/assets/renderMobileApp-CKYDwf4U.js +28 -0
  56. package/dist/assets/runtimeConfig-B4eb4ZiU.js +2 -0
  57. package/dist/assets/runtimeEndpointReset-BHuTYyzc.js +147 -0
  58. package/dist/assets/sessionChangeSummary-DOq8EN_H.js +1 -0
  59. package/dist/assets/sessionLookup-DNc0ZeR3.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-D3onaUkL.js → useAppFontEffects-Dle8xs5h.js} +44 -39
  70. package/dist/assets/useAssistantUIStore-me6s63b2.js +1 -0
  71. package/dist/assets/useDesktopWindowControlsLayout-_QAwKcQn.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-BSyuAgz6.js +1 -0
  73. package/dist/assets/useFontPreferences-BCOBlaAd.js +1 -0
  74. package/dist/assets/useMobileNavigationStore-f8xHJk7C.js +413 -0
  75. package/dist/assets/useSessionAutoCleanup-BHiXaW0J.js +1 -0
  76. package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
  77. package/dist/assets/useWorkerHighlightedLines-DuvXTSm_.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 +76 -3
  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 +761 -0
  99. package/server/lib/assistants/contact-tools.test.js +328 -0
  100. package/server/lib/assistants/contracts.js +20 -1
  101. package/server/lib/assistants/harness.js +551 -0
  102. package/server/lib/assistants/harness.test.js +607 -0
  103. package/server/lib/assistants/routes.js +44 -4
  104. package/server/lib/assistants/service.js +930 -17
  105. package/server/lib/assistants/service.test.js +1163 -45
  106. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  107. package/server/lib/event-stream/directory-ws-bridge.js +11 -2
  108. package/server/lib/event-stream/global-ws-bridge.js +19 -3
  109. package/server/lib/event-stream/protocol.js +11 -1
  110. package/server/lib/event-stream/reasoning-projection.js +425 -0
  111. package/server/lib/event-stream/reasoning-projection.test.js +300 -0
  112. package/server/lib/event-stream/runtime.js +10 -0
  113. package/server/lib/event-stream/runtime.test.js +53 -0
  114. package/server/lib/llm/DOCUMENTATION.md +84 -0
  115. package/server/lib/llm/catalog.js +100 -0
  116. package/server/lib/llm/catalog.test.js +78 -0
  117. package/server/lib/llm/completions.js +151 -0
  118. package/server/lib/llm/completions.test.js +127 -0
  119. package/server/lib/llm/generate.js +570 -0
  120. package/server/lib/llm/generate.test.js +459 -0
  121. package/server/lib/llm/routes.js +47 -0
  122. package/server/lib/llm/temp-directory.js +37 -0
  123. package/server/lib/notifications/APNS.md +19 -13
  124. package/server/lib/notifications/DOCUMENTATION.md +4 -3
  125. package/server/lib/notifications/runtime.js +153 -38
  126. package/server/lib/notifications/runtime.test.js +122 -0
  127. package/server/lib/opencode/DOCUMENTATION.md +2 -1
  128. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  129. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  130. package/server/lib/opencode/proxy.js +103 -9
  131. package/server/lib/opencode/proxy.test.js +177 -1
  132. package/server/lib/opencode/settings-runtime.js +2 -2
  133. package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
  134. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  135. package/server/lib/session-goal/runtime.js +20 -0
  136. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  137. package/server/lib/session-index/service.js +10 -4
  138. package/server/lib/session-index/service.test.js +37 -0
  139. package/server/lib/session-title/runtime.js +1 -0
  140. package/server/lib/session-title/runtime.test.js +11 -0
  141. package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
  142. package/server/lib/session-turn-pages/routes.js +22 -3
  143. package/server/lib/session-turn-pages/routes.test.js +43 -0
  144. package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
  145. package/server/lib/transcript-cache/routes.js +9 -2
  146. package/server/lib/transcript-cache/routes.test.js +32 -0
  147. package/dist/assets/AgentAvatar-Di8F4QJh.js +0 -1
  148. package/dist/assets/AssistantView-DYdXsGDh.js +0 -1
  149. package/dist/assets/ContextPanel-Cj6RVUAe.js +0 -285
  150. package/dist/assets/DiagramView-BoRTG67Z.js +0 -1
  151. package/dist/assets/FadeInOnReveal-CV2s6YN8.js +0 -1
  152. package/dist/assets/Icon-CLpn1mIz.js +0 -1
  153. package/dist/assets/MarkdownRendererImpl-DwUKY9k4.js +0 -7
  154. package/dist/assets/MarkstreamRendererImpl-BRvprELo.js +0 -1
  155. package/dist/assets/MarkstreamRendererImpl-Ca7RATbs.css +0 -1
  156. package/dist/assets/MobileDetailNavigation-C0Z8beRL.js +0 -1
  157. package/dist/assets/MobileShareBridge-DTvmm9vz.js +0 -4
  158. package/dist/assets/MobileSurface-CfrI-5uf.js +0 -2
  159. package/dist/assets/OnboardingScreen-D6rtE7rj.js +0 -1
  160. package/dist/assets/OverlayScrollbar-DlNZho3q.js +0 -2
  161. package/dist/assets/SettingsView-BOT158gD.js +0 -25
  162. package/dist/assets/TerminalView-sf_LfP6Z.js +0 -8
  163. package/dist/assets/ToolOutputDialog-BI5vwuaT.js +0 -10
  164. package/dist/assets/appThemeRegistry-G_zwgqAH.js +0 -2
  165. package/dist/assets/clipboard-D35XSwID.js +0 -1
  166. package/dist/assets/desktop-CfLlmafK.js +0 -1
  167. package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
  168. package/dist/assets/desktopRelayRestore-8pvI_dSc.js +0 -1
  169. package/dist/assets/fileQueries-DGHZqZHc.js +0 -1
  170. package/dist/assets/insertionBoundaries-CJksyugt.js +0 -4
  171. package/dist/assets/main-BTeHOAU_.js +0 -29
  172. package/dist/assets/main-BUBx-y1E.js +0 -2
  173. package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
  174. package/dist/assets/markdownTheme-SPe4jOwX.js +0 -2
  175. package/dist/assets/miniChat-Btnf42TH.js +0 -2
  176. package/dist/assets/mobile-CAjwV6ly.js +0 -2
  177. package/dist/assets/multirun-Rpz1_-fz.js +0 -4
  178. package/dist/assets/persistence-w-AiX2i4.js +0 -3
  179. package/dist/assets/platform-4eCmsdol.js +0 -2
  180. package/dist/assets/projectMeta-CvUuy-j5.js +0 -3
  181. package/dist/assets/radio-D_a28OlM.js +0 -1
  182. package/dist/assets/renderElectronMiniChatApp-DCzBQk2w.js +0 -2
  183. package/dist/assets/renderMobileApp-U5sclQRp.js +0 -28
  184. package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
  185. package/dist/assets/runtimeConfig-6inIl99b.js +0 -2
  186. package/dist/assets/runtimeEndpointReset-B_rUFDT5.js +0 -4
  187. package/dist/assets/shikiHighlight-CMpSAuTl.js +0 -1
  188. package/dist/assets/sortable-tabs-strip-CNDgyX0B.js +0 -1
  189. package/dist/assets/src-DZt1nWf5.css +0 -2
  190. package/dist/assets/terminalApi-CGtOtc8I.js +0 -1
  191. package/dist/assets/url-KeihbE0d.js +0 -1
  192. package/dist/assets/useAppFontEffects-3xHRVJcf.js +0 -7
  193. package/dist/assets/useAssistantUIStore-I0VWrB5V.js +0 -1
  194. package/dist/assets/useDesktopWindowControlsLayout-QJlddPRn.js +0 -1
  195. package/dist/assets/useEffectiveDirectory-Bcj1wP95.js +0 -1
  196. package/dist/assets/useFontPreferences-BLZ3y6mN.js +0 -1
  197. package/dist/assets/useMobileNavigationStore-CD0GRjaV.js +0 -412
  198. package/dist/assets/useSessionAutoCleanup-CN_6ABvx.js +0 -1
  199. package/dist/assets/useUIStore-CNLmCoPU.js +0 -1
  200. package/dist/assets/useWorkerHighlightedLines-DX9znDoW.js +0 -7
  201. package/dist/assets/utils-CTL16t2f.js +0 -1
@@ -0,0 +1,328 @@
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('给 PeerQA 说一声 hello-from-assistant 写好了', tools)).toEqual([
94
+ MESSAGE_ASSISTANT_TOOL_NAME,
95
+ ]);
96
+ expect(detectRequestedContactTools('开新对话', tools)).toEqual([NEW_CONVERSATION_TOOL_NAME]);
97
+ expect(detectRequestedContactTools('new conversation please', tools)).toEqual([NEW_CONVERSATION_TOOL_NAME]);
98
+ expect(detectRequestedContactTools('clear chat', tools)).toEqual([NEW_CONVERSATION_TOOL_NAME]);
99
+ expect(detectRequestedContactTools('找项目 openchamber yee', tools)).toEqual([LIST_PROJECTS_TOOL_NAME]);
100
+ expect(detectRequestedContactTools('看看现有对话', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
101
+ expect(detectRequestedContactTools('list sessions in that project', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
102
+ expect(detectRequestedContactTools('开新对话', tools)).not.toContain(ASSIGN_SESSION_TOOL_NAME);
103
+ expect(detectRequestedContactTools('不要开编码 session', tools)).toEqual([]);
104
+ });
105
+
106
+ it('ignores bash/edit tool fences', () => {
107
+ const text = '```openchamber-tool\n{"name":"bash","arguments":{"command":"ls"}}\n```';
108
+ expect(parseContactToolCalls(text, ['bash', ASSIGN_SESSION_TOOL_NAME]).toolCall).toBeNull();
109
+ });
110
+
111
+ it('tells DeepSeek to call tools from natural language, not slash commands', () => {
112
+ const prompt = formatContactToolsPrompt(createContactTools());
113
+ expect(prompt).toContain('new_conversation');
114
+ expect(prompt).toContain('list_projects');
115
+ expect(prompt).toContain('list_sessions');
116
+ expect(prompt).toContain('create_assistant');
117
+ expect(prompt).toContain('schedule_task');
118
+ expect(prompt).toContain('message_assistant');
119
+ expect(prompt).toContain('assign_session');
120
+ expect(prompt).toContain('开新对话');
121
+ expect(prompt).toContain('找项目');
122
+ expect(prompt).toContain('现有对话');
123
+ expect(prompt).toContain('建助理');
124
+ expect(prompt).toContain('排定时任务');
125
+ expect(prompt).toContain('说一声');
126
+ expect(prompt).toContain('session/new');
127
+ expect(prompt).not.toContain('/card');
128
+ expect(prompt).not.toContain('/dm');
129
+ expect(prompt).toContain('A reply without the tool call does nothing');
130
+ expect(prompt).toContain('已创建');
131
+ expect(prompt).toContain('Never claim you cannot see projects');
132
+ });
133
+
134
+ it('fuzzy-matches project labels like openchamber yee / openchamer yee', () => {
135
+ const project = { id: 'p1', path: '/Users/me/Code/sample-app', label: 'OpenChamber Yee' };
136
+ expect(matchesProjectQuery(project, 'openchamber yee')).toBe(true);
137
+ expect(matchesProjectQuery(project, 'openchamer yee')).toBe(true);
138
+ expect(matchesProjectQuery(project, 'missing')).toBe(false);
139
+ expect(filterRegisteredProjects([project, { id: 'p2', path: '/other' }], 'openchamber')).toEqual([
140
+ { id: 'p1', path: '/Users/me/Code/sample-app', label: 'OpenChamber Yee' },
141
+ ]);
142
+ expect(formatRegisteredProjectsPrompt([project])).toContain('OpenChamber Yee');
143
+ expect(formatRegisteredProjectsPrompt([project])).toContain('never say you cannot see registered projects');
144
+ expect(formatRegisteredProjectsPrompt([])).toContain('none');
145
+ });
146
+
147
+ it('resolves provider/model from a combined OpenCode id', () => {
148
+ expect(resolveContactProviderModel({ model: 'opencode-go/deepseek-v4-flash' })).toEqual({
149
+ providerID: 'opencode-go',
150
+ modelID: 'deepseek-v4-flash',
151
+ });
152
+ expect(resolveContactProviderModel({}, { providerID: 'p', modelID: 'm' })).toEqual({
153
+ providerID: 'p',
154
+ modelID: 'm',
155
+ });
156
+ });
157
+ });
158
+
159
+ describe('createContactTools', () => {
160
+ it('exposes create_assistant, schedule_task, and assign_session and returns success cards', async () => {
161
+ const onCard = vi.fn();
162
+ const resetContact = vi.fn(async () => ({ reset: true }));
163
+ const tools = createContactTools({
164
+ resetContact,
165
+ createAssistant: async (input) => ({
166
+ id: 'asst_flow',
167
+ name: input.name,
168
+ providerID: input.providerID,
169
+ modelID: input.modelID,
170
+ mode: 'continuous',
171
+ }),
172
+ scheduleTask: async (input) => ({
173
+ taskID: 'task_1',
174
+ projectID: 'proj_1',
175
+ name: input.name,
176
+ kind: 'daily',
177
+ time: input.time,
178
+ timezone: input.timezone,
179
+ prompt: input.prompt,
180
+ }),
181
+ assignWork: async () => ({
182
+ sessionID: 'ses_1',
183
+ directory: '/repo',
184
+ title: 'Login',
185
+ status: 'busy',
186
+ }),
187
+ deliverPeerMessage: async (input) => ({
188
+ admitted: true,
189
+ role: 'peer',
190
+ toAssistantID: input.toAssistantID,
191
+ }),
192
+ listAssistants: async () => [{ id: 'asst_peer', name: 'PeerQA' }],
193
+ currentAssistant: { id: 'asst_host', providerID: 'p', modelID: 'm' },
194
+ onCard,
195
+ });
196
+ expect(tools.map((tool) => tool.name)).toEqual([
197
+ NEW_CONVERSATION_TOOL_NAME,
198
+ LIST_PROJECTS_TOOL_NAME,
199
+ LIST_SESSIONS_TOOL_NAME,
200
+ CREATE_ASSISTANT_TOOL_NAME,
201
+ SCHEDULE_TASK_TOOL_NAME,
202
+ MESSAGE_ASSISTANT_TOOL_NAME,
203
+ ASSIGN_SESSION_TOOL_NAME,
204
+ ]);
205
+ expect(tools.some((tool) => ['bash', 'edit', 'read', 'write'].includes(tool.name))).toBe(false);
206
+
207
+ const reset = await tools.find((tool) => tool.name === NEW_CONVERSATION_TOOL_NAME).execute('call_0', {});
208
+ expect(resetContact).toHaveBeenCalledTimes(1);
209
+ expect(reset.details.card).toBeUndefined();
210
+ expect(reset.details.reset).toBe(true);
211
+ expect(reset.terminate).toBe(true);
212
+ expect(reset.content[0].text).toBe(NEW_CONVERSATION_CONFIRM_BUBBLE);
213
+
214
+ const created = await tools.find((tool) => tool.name === CREATE_ASSISTANT_TOOL_NAME).execute('call_1', { name: 'FlowQA', model: 'opencode-go/deepseek-v4-flash' });
215
+ expect(created.details.card).toMatchObject({
216
+ type: 'card',
217
+ cardType: 'assistant',
218
+ assistantID: 'asst_flow',
219
+ name: 'FlowQA',
220
+ providerID: 'opencode-go',
221
+ modelID: 'deepseek-v4-flash',
222
+ });
223
+ expect(created.terminate).toBe(false);
224
+
225
+ const scheduled = await tools.find((tool) => tool.name === SCHEDULE_TASK_TOOL_NAME).execute('call_2', {
226
+ name: 'Daily ping',
227
+ prompt: 'ping',
228
+ time: '18:00',
229
+ timezone: 'Asia/Shanghai',
230
+ });
231
+ expect(scheduled.details.card).toMatchObject({
232
+ type: 'card',
233
+ cardType: 'schedule',
234
+ taskID: 'task_1',
235
+ name: 'Daily ping',
236
+ time: '18:00',
237
+ });
238
+
239
+ const messaged = await tools.find((tool) => tool.name === MESSAGE_ASSISTANT_TOOL_NAME).execute('call_3', {
240
+ to: 'PeerQA',
241
+ text: 'hello-from-assistant 写好了',
242
+ });
243
+ expect(messaged.details.card).toBeUndefined();
244
+ expect(messaged.content[0].text).toContain('Sent to PeerQA');
245
+ expect(messaged.details.toAssistantID).toBe('asst_peer');
246
+
247
+ const assigned = await tools.find((tool) => tool.name === ASSIGN_SESSION_TOOL_NAME).execute('call_4', { prompt: 'Fix login' });
248
+ expect(assigned.details.card).toMatchObject({
249
+ type: 'card',
250
+ cardType: 'session',
251
+ sessionID: 'ses_1',
252
+ title: 'Login',
253
+ });
254
+ expect(onCard).toHaveBeenCalledTimes(3);
255
+ });
256
+
257
+ it('resolves a peer by name and rejects a missing recipient', () => {
258
+ const listed = [{ id: 'asst_peer', name: 'PeerQA' }, { id: 'asst_host', name: 'DeepSeekQA' }];
259
+ expect(resolvePeerAssistant({ to: 'PeerQA' }, listed, { id: 'asst_host' })).toEqual(listed[0]);
260
+ expect(() => resolvePeerAssistant({ to: 'Missing' }, listed, { id: 'asst_host' })).toThrow(/No assistant named/);
261
+ expect(() => resolvePeerAssistant({ to: 'DeepSeekQA' }, listed, { id: 'asst_host' })).toThrow(/same assistant/);
262
+ });
263
+
264
+ it('returns a clear project_required result instead of throwing', async () => {
265
+ const tools = createContactTools({
266
+ assignWork: async () => {
267
+ throw new AssignError(ASSIGN_CODES.PROJECT_REQUIRED, PROJECT_REQUIRED_MESSAGE);
268
+ },
269
+ });
270
+ const result = await tools.find((tool) => tool.name === ASSIGN_SESSION_TOOL_NAME).execute('call_1', { prompt: 'Fix login' });
271
+ expect(result.details.error).toBe('project_required');
272
+ expect(result.content[0].text).toContain('Add a project in Settings');
273
+ expect(result.content[0].text).toContain('assistant-workspaces');
274
+ });
275
+
276
+ it('lists projects and sessions and surfaces list_sessions failure', async () => {
277
+ const tools = createContactTools({
278
+ listProjects: async () => [
279
+ { id: 'proj_yee', path: '/repo/sample-app', label: 'OpenChamber Yee' },
280
+ { id: 'proj_other', path: '/repo/other', label: 'Other' },
281
+ ],
282
+ listSessions: async () => ({
283
+ sessions: [
284
+ { sessionID: 'ses_1', title: 'Login fix', directory: '/repo/sample-app', updatedAt: 2 },
285
+ ],
286
+ truncated: false,
287
+ }),
288
+ });
289
+ const listed = await tools.find((tool) => tool.name === LIST_PROJECTS_TOOL_NAME).execute('call_p', {
290
+ query: 'openchamer yee',
291
+ });
292
+ expect(listed.details.projects).toEqual([
293
+ { id: 'proj_yee', path: '/repo/sample-app', label: 'OpenChamber Yee' },
294
+ ]);
295
+ const sessions = await tools.find((tool) => tool.name === LIST_SESSIONS_TOOL_NAME).execute('call_s', {
296
+ projectPath: '/repo/sample-app',
297
+ });
298
+ expect(sessions.details.sessions).toEqual([
299
+ expect.objectContaining({ sessionID: 'ses_1', title: 'Login fix' }),
300
+ ]);
301
+
302
+ const failing = createContactTools({
303
+ listSessions: async () => {
304
+ throw new AssignError(ASSIGN_CODES.UPSTREAM, 'Session index is unavailable.');
305
+ },
306
+ });
307
+ const failed = await failing.find((tool) => tool.name === LIST_SESSIONS_TOOL_NAME).execute('call_fail', {});
308
+ expect(failed.details.error).toBe('upstream_error');
309
+ expect(failed.content[0].text).toContain('Session index is unavailable');
310
+ expect(failed.details.sessions).toBeUndefined();
311
+ });
312
+ });
313
+
314
+ describe('confirmBubbleAfterContactReset', () => {
315
+ it('keeps the canonical confirm and drops leftover attachment bubbles', () => {
316
+ expect(confirmBubbleAfterContactReset([
317
+ NEW_CONVERSATION_CONFIRM_BUBBLE,
318
+ 'I still see your dot.png and note.txt.',
319
+ 'Those attachments are still in context.',
320
+ ])).toEqual([NEW_CONVERSATION_CONFIRM_BUBBLE]);
321
+ });
322
+
323
+ it('uses the canonical confirm when leftover text is the only extracted bubble', () => {
324
+ expect(confirmBubbleAfterContactReset([
325
+ 'I still see your dot.png and note.txt from earlier.',
326
+ ])).toEqual([NEW_CONVERSATION_CONFIRM_BUBBLE]);
327
+ });
328
+ });
@@ -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
  });