@openchambery/web 1.19.7-beta.2 → 1.19.7-beta.20

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 (185) hide show
  1. package/dist/assets/AgentAvatar-DI2zs2WH.js +1 -0
  2. package/dist/assets/AssistantView-C0p-8f2B.js +2 -0
  3. package/dist/assets/DiagramView-CSXRZQ5S.js +1 -0
  4. package/dist/assets/FadeInOnReveal-BWJcUp5P.js +1 -0
  5. package/dist/assets/Icon-4My8YZwt.js +1 -0
  6. package/dist/assets/MarkdownRendererImpl-C6rkoZYH.js +7 -0
  7. package/dist/assets/{MarkstreamRendererImpl-CR0fBG5w.css → MarkstreamRendererImpl-Ca7RATbs.css} +1 -1
  8. package/dist/assets/MarkstreamRendererImpl-CowOCU5W.js +1 -0
  9. package/dist/assets/MobileDetailNavigation-ACMbLTqb.js +1 -0
  10. package/dist/assets/MobileShareBridge-CXR-B1xW.js +4 -0
  11. package/dist/assets/MobileSurface-CKBSKqUH.js +2 -0
  12. package/dist/assets/{MultiRunWindow-khpnWyZO.js → MultiRunWindow-el9_nl25.js} +1 -1
  13. package/dist/assets/OnboardingScreen-BNEtPmvw.js +1 -0
  14. package/dist/assets/OverlayScrollbar-BZB21Yp_.js +2 -0
  15. package/dist/assets/SettingsView-CVSiIRia.js +25 -0
  16. package/dist/assets/{SettingsWindow-Dx5rU9v5.js → SettingsWindow-BXYswtp5.js} +1 -1
  17. package/dist/assets/TerminalView-Bh0IQwa5.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-BfuXazW9.js → ToolOutputDialog-XeIJ2etT.js} +3 -3
  19. package/dist/assets/appThemeRegistry-rSdmh4sW.js +2 -0
  20. package/dist/assets/{button-z0GOBtWi.js → button-Or0daT9o.js} +1 -1
  21. package/dist/assets/clipboard-BXxLshVq.js +1 -0
  22. package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-C-kzSGkq.js} +1 -1
  23. package/dist/assets/context-menu-Dpjla5p4.js +1 -0
  24. package/dist/assets/desktopHosts-BzSE0sK7.js +1 -0
  25. package/dist/assets/desktopRelayRestore-BaKjk1kQ.js +1 -0
  26. package/dist/assets/{diagram-CoEqmReY.js → diagram-D0mT3mo1.js} +1 -1
  27. package/dist/assets/{dialog-HCE4eLUW.js → dialog-BkhHBTd4.js} +1 -1
  28. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
  29. package/dist/assets/{es-D9KcV5TX.js → es-BHqnSjoZ.js} +1 -1
  30. package/dist/assets/fileQueries-DFY6Kiy1.js +1 -0
  31. package/dist/assets/fileReferenceActions-3pPDAJSl.js +1 -0
  32. package/dist/assets/{fr-DPA9qUl3.js → fr-BfrlJY9i.js} +1 -1
  33. package/dist/assets/{gitApi-DNTMAa3F.js → gitApi-Uvb9w70Y.js} +3 -3
  34. package/dist/assets/{input-BG3VWTIF.js → input-BUsGnzwe.js} +1 -1
  35. package/dist/assets/insertionBoundaries-Bgec5nsP.js +4 -0
  36. package/dist/assets/{ja-Dsif3NoD.js → ja-DP-7qt-x.js} +1 -1
  37. package/dist/assets/{ko-Dixx4Ctg.js → ko-E2B5vGQP.js} +1 -1
  38. package/dist/assets/main-COfQt8Sj.js +169 -0
  39. package/dist/assets/main-PPlpRj1-.js +2 -0
  40. package/dist/assets/markdown-worker-wEFz5Hm_.js +3 -0
  41. package/dist/assets/markdownHeightCache-Bj707yG-.js +2 -0
  42. package/dist/assets/markdownTheme-CVS01wHf.js +2 -0
  43. package/dist/assets/miniChat-EZGRqQZr.js +2 -0
  44. package/dist/assets/mobile-DQNqMo5J.js +2 -0
  45. package/dist/assets/multirun-EViX73vm.js +4 -0
  46. package/dist/assets/outsideFileGrants-BB4GPvCq.js +1 -0
  47. package/dist/assets/path-utils-jGozTg9d.js +1 -0
  48. package/dist/assets/persistence-CSZp1CNB.js +3 -0
  49. package/dist/assets/{pl-Bj78ZGgf.js → pl-r8X5gRM5.js} +1 -1
  50. package/dist/assets/platform-CenlWqxu.js +2 -0
  51. package/dist/assets/projectMeta-Bv1L_nEb.js +3 -0
  52. package/dist/assets/{pt-BR-CFL3xSAf.js → pt-BR-DcpdXCf5.js} +1 -1
  53. package/dist/assets/renderElectronMiniChatApp-Nsq4Wc4s.js +2 -0
  54. package/dist/assets/renderMobileApp-5WJDYaMR.js +28 -0
  55. package/dist/assets/runtimeConfig-DKUQS4iY.js +2 -0
  56. package/dist/assets/runtimeEndpointReset-BMeu9Hqj.js +147 -0
  57. package/dist/assets/sessionChangeSummary-CX2MJIkc.js +1 -0
  58. package/dist/assets/sessionLookup-Sb2HN3G2.js +1 -0
  59. package/dist/assets/sortable-tabs-strip-C5gGnikj.js +1 -0
  60. package/dist/assets/src-Af9cMFKF.css +2 -0
  61. package/dist/assets/{store-DzpbXWX1.js → store-BDFdnk6l.js} +2 -2
  62. package/dist/assets/terminalApi-CIIs9vFx.js +1 -0
  63. package/dist/assets/textarea-CIO34TZz.js +1 -0
  64. package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BSrJYYYc.js} +1 -1
  65. package/dist/assets/{uk-BY60Ub3Z.js → uk-Di0vl9V-.js} +1 -1
  66. package/dist/assets/url-CE_gAfNa.js +1 -0
  67. package/dist/assets/{ChatContainer-CnOrdAgs.js → useAppFontEffects-B_6vc0u3.js} +46 -41
  68. package/dist/assets/useAssistantUIStore-BHacrxe_.js +1 -0
  69. package/dist/assets/useDesktopWindowControlsLayout-B2L5PWHW.js +1 -0
  70. package/dist/assets/useEffectiveDirectory-CBd5vt8N.js +1 -0
  71. package/dist/assets/useFontPreferences-BxOHKuVw.js +1 -0
  72. package/dist/assets/useMobileNavigationStore-CUGaydRW.js +412 -0
  73. package/dist/assets/useSessionAutoCleanup-Clhzc1wn.js +1 -0
  74. package/dist/assets/useUIStore-CqqO11ZI.js +1 -0
  75. package/dist/assets/useWorkerHighlightedLines-C4iQlbPQ.js +7 -0
  76. package/dist/assets/utils-B0mQXN9L.js +1 -0
  77. package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
  78. package/dist/assets/{zh-CN-Cox1LaOW.js → zh-CN-coFCfeQE.js} +1 -1
  79. package/dist/assets/{zh-TW-B6RU0mxa.js → zh-TW-oDRZ1-yb.js} +1 -1
  80. package/dist/index.html +9 -11
  81. package/dist/mini-chat.html +9 -11
  82. package/dist/mobile.html +15 -17
  83. package/package.json +2 -1
  84. package/server/index.js +2 -0
  85. package/server/lib/assistant-delivery-parts.js +3 -1
  86. package/server/lib/assistant-delivery-parts.test.js +11 -0
  87. package/server/lib/assistants/DOCUMENTATION.md +76 -3
  88. package/server/lib/assistants/assign.js +225 -0
  89. package/server/lib/assistants/assign.test.js +118 -0
  90. package/server/lib/assistants/bubbles.js +45 -0
  91. package/server/lib/assistants/bubbles.test.js +36 -0
  92. package/server/lib/assistants/cards.js +148 -0
  93. package/server/lib/assistants/cards.test.js +116 -0
  94. package/server/lib/assistants/contact-store.js +284 -0
  95. package/server/lib/assistants/contact-store.test.js +131 -0
  96. package/server/lib/assistants/contact-tools.js +761 -0
  97. package/server/lib/assistants/contact-tools.test.js +328 -0
  98. package/server/lib/assistants/contracts.js +20 -1
  99. package/server/lib/assistants/harness.js +551 -0
  100. package/server/lib/assistants/harness.test.js +607 -0
  101. package/server/lib/assistants/routes.js +44 -4
  102. package/server/lib/assistants/service.js +930 -17
  103. package/server/lib/assistants/service.test.js +1163 -45
  104. package/server/lib/llm/DOCUMENTATION.md +84 -0
  105. package/server/lib/llm/catalog.js +100 -0
  106. package/server/lib/llm/catalog.test.js +78 -0
  107. package/server/lib/llm/completions.js +151 -0
  108. package/server/lib/llm/completions.test.js +127 -0
  109. package/server/lib/llm/generate.js +570 -0
  110. package/server/lib/llm/generate.test.js +459 -0
  111. package/server/lib/llm/routes.js +47 -0
  112. package/server/lib/llm/temp-directory.js +37 -0
  113. package/server/lib/notifications/APNS.md +4 -4
  114. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  115. package/server/lib/notifications/apns-runtime.js +139 -20
  116. package/server/lib/notifications/apns-runtime.test.js +66 -7
  117. package/server/lib/notifications/apns-titles.js +62 -62
  118. package/server/lib/notifications/apns-titles.test.js +3 -1
  119. package/server/lib/notifications/routes.js +1 -1
  120. package/server/lib/notifications/runtime.js +81 -49
  121. package/server/lib/notifications/runtime.test.js +61 -0
  122. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  123. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  124. package/server/lib/opencode/settings-runtime.js +18 -2
  125. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  126. package/server/lib/session-goal/runtime.js +20 -0
  127. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  128. package/server/lib/session-index/service.js +10 -4
  129. package/server/lib/session-index/service.test.js +37 -0
  130. package/server/lib/session-title/runtime.js +1 -0
  131. package/server/lib/session-title/runtime.test.js +11 -0
  132. package/dist/assets/AgentAvatar-ziUs4sw3.js +0 -1
  133. package/dist/assets/AssistantView-DF4yXFgS.js +0 -1
  134. package/dist/assets/ContextPanel-Cg_uCHzJ.js +0 -285
  135. package/dist/assets/DiagramView-DYh3Zn2y.js +0 -1
  136. package/dist/assets/FadeInOnReveal-BAlsRFJH.js +0 -1
  137. package/dist/assets/Icon-CLpn1mIz.js +0 -1
  138. package/dist/assets/MarkdownRendererImpl-QzUEpBEm.js +0 -7
  139. package/dist/assets/MarkstreamRendererImpl-CXrfeUia.js +0 -1
  140. package/dist/assets/MobileDetailNavigation-DJXEYfq4.js +0 -1
  141. package/dist/assets/MobileShareBridge-CliVo1YH.js +0 -4
  142. package/dist/assets/MobileSurface-9Xh6WQkY.js +0 -2
  143. package/dist/assets/OnboardingScreen-CJYyR54B.js +0 -1
  144. package/dist/assets/OverlayScrollbar-C-jZ8ITG.js +0 -2
  145. package/dist/assets/SettingsView-DyCZehFG.js +0 -25
  146. package/dist/assets/TerminalView-XSALfUaH.js +0 -8
  147. package/dist/assets/appThemeRegistry-DDWrniMM.js +0 -2
  148. package/dist/assets/clipboard-Bz0Zm9VJ.js +0 -1
  149. package/dist/assets/desktop-CfLlmafK.js +0 -1
  150. package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
  151. package/dist/assets/desktopRelayRestore-LBt0CBOj.js +0 -1
  152. package/dist/assets/fileQueries-Dnn15WsA.js +0 -1
  153. package/dist/assets/insertionBoundaries-DMxuUnL_.js +0 -4
  154. package/dist/assets/main-B9HVtgXE.js +0 -2
  155. package/dist/assets/main-ZNyIs3Sn.js +0 -29
  156. package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
  157. package/dist/assets/markdownHeightCache-CvUyZT4q.js +0 -1
  158. package/dist/assets/markdownTheme-TdhQOZSN.js +0 -2
  159. package/dist/assets/miniChat-q0T0WGkZ.js +0 -2
  160. package/dist/assets/mobile-DEduvLJj.js +0 -2
  161. package/dist/assets/multirun-DU-wrxlG.js +0 -4
  162. package/dist/assets/persistence-CmjKImz2.js +0 -3
  163. package/dist/assets/platform-skqcMARt.js +0 -2
  164. package/dist/assets/projectMeta-DNWx6Uva.js +0 -3
  165. package/dist/assets/radio-BEonIX1k.js +0 -1
  166. package/dist/assets/renderElectronMiniChatApp--JYWY41L.js +0 -2
  167. package/dist/assets/renderMobileApp-B0Ix_uNo.js +0 -25
  168. package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
  169. package/dist/assets/runtimeConfig-BTDIG7mX.js +0 -2
  170. package/dist/assets/runtimeEndpointReset-CZQCYtVI.js +0 -4
  171. package/dist/assets/shikiHighlight-C9Cx56Qg.js +0 -1
  172. package/dist/assets/sortable-tabs-strip-Bj8N0VWf.js +0 -1
  173. package/dist/assets/src-2eFAiBLq.css +0 -2
  174. package/dist/assets/terminalApi-CNqqwjfG.js +0 -1
  175. package/dist/assets/url-KeihbE0d.js +0 -1
  176. package/dist/assets/useAppFontEffects-CKLSamae.js +0 -7
  177. package/dist/assets/useAssistantUIStore-DaplPyb0.js +0 -1
  178. package/dist/assets/useDesktopWindowControlsLayout-DJSkFfhT.js +0 -1
  179. package/dist/assets/useEffectiveDirectory-B3rbY1Kv.js +0 -1
  180. package/dist/assets/useFontPreferences-D8emhcNv.js +0 -1
  181. package/dist/assets/useMobileNavigationStore-CAvVUItx.js +0 -412
  182. package/dist/assets/useSessionAutoCleanup-DxNgqJ8s.js +0 -1
  183. package/dist/assets/useUIStore-B7KJS5G7.js +0 -1
  184. package/dist/assets/useWorkerHighlightedLines-DiDF68LR.js +0 -7
  185. 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/openchamber-yee', 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/openchamber-yee', 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/openchamber-yee', 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/openchamber-yee', 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/openchamber-yee', label: 'OpenChamber Yee' },
294
+ ]);
295
+ const sessions = await tools.find((tool) => tool.name === LIST_SESSIONS_TOOL_NAME).execute('call_s', {
296
+ projectPath: '/repo/openchamber-yee',
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
  });