@openchambery/web 1.19.7-beta.13 → 1.19.7-beta.15

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 (120) hide show
  1. package/dist/assets/{AgentAvatar-Dp_hbVcD.js → AgentAvatar-BsU82CEP.js} +1 -1
  2. package/dist/assets/AssistantView-CxC9FX9C.js +2 -0
  3. package/dist/assets/{DiagramView-iX3nhWaN.js → DiagramView-yMUQZB0N.js} +1 -1
  4. package/dist/assets/{MarkdownRendererImpl-tDry9J76.js → MarkdownRendererImpl-DHLlx_NY.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-BDVGi72n.js → MarkstreamRendererImpl-npYzOsgz.js} +1 -1
  6. package/dist/assets/MobileDetailNavigation-CQjttwAO.js +1 -0
  7. package/dist/assets/{MobileShareBridge-BQ79cuPX.js → MobileShareBridge-BVq0juvZ.js} +3 -3
  8. package/dist/assets/MobileSurface-CNvc2sAe.js +2 -0
  9. package/dist/assets/{MultiRunWindow-CzMbzYD3.js → MultiRunWindow-BaPCe5H6.js} +1 -1
  10. package/dist/assets/{OnboardingScreen-CuvjeXSo.js → OnboardingScreen-BNEtPmvw.js} +1 -1
  11. package/dist/assets/SettingsView-lvmcERgb.js +25 -0
  12. package/dist/assets/{SettingsWindow-DqwoBYvU.js → SettingsWindow-4M8nitNm.js} +1 -1
  13. package/dist/assets/{TerminalView-4pFqXfvj.js → TerminalView-B-grl1Sg.js} +1 -1
  14. package/dist/assets/{ToolOutputDialog-GmD4kIoH.js → ToolOutputDialog-DB7tlLR-.js} +1 -1
  15. package/dist/assets/{appThemeRegistry-BlEYFhXM.js → appThemeRegistry-aEqhY6Jd.js} +2 -2
  16. package/dist/assets/{clipboard-Bu0lpyX1.js → clipboard-BXxLshVq.js} +1 -1
  17. package/dist/assets/context-menu-Dpjla5p4.js +1 -0
  18. package/dist/assets/{dialog-D21YndEx.js → dialog-BkhHBTd4.js} +1 -1
  19. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
  20. package/dist/assets/{es-C8Sqjsia.js → es-BHqnSjoZ.js} +1 -1
  21. package/dist/assets/{fr-p-Zwm_j2.js → fr-BfrlJY9i.js} +1 -1
  22. package/dist/assets/{gitApi-CciB055B.js → gitApi-iKSVqWGT.js} +3 -3
  23. package/dist/assets/insertionBoundaries-XFIbRzxa.js +4 -0
  24. package/dist/assets/{ja-B6REu1qP.js → ja-DP-7qt-x.js} +1 -1
  25. package/dist/assets/{ko-CJBNH9IZ.js → ko-E2B5vGQP.js} +1 -1
  26. package/dist/assets/{main-DYFky6Uj.js → main-CAh-fkHv.js} +2 -2
  27. package/dist/assets/main-DiVOSucH.js +169 -0
  28. package/dist/assets/{markdownTheme-E_Ina01F.js → markdownTheme-CVS01wHf.js} +1 -1
  29. package/dist/assets/miniChat-D-KGjCnF.js +2 -0
  30. package/dist/assets/mobile-Cd1e4fBX.js +2 -0
  31. package/dist/assets/multirun-iFn8TY6T.js +4 -0
  32. package/dist/assets/{pl-B2cBI_7K.js → pl-r8X5gRM5.js} +1 -1
  33. package/dist/assets/projectMeta-DpHkpJw_.js +3 -0
  34. package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
  35. package/dist/assets/renderElectronMiniChatApp-VzrrMvJ1.js +2 -0
  36. package/dist/assets/renderMobileApp-Dko2HpCa.js +28 -0
  37. package/dist/assets/{runtimeConfig-DNsnUkyp.js → runtimeConfig-ni8yyqoa.js} +1 -1
  38. package/dist/assets/runtimeEndpointReset-aBxjZ2PR.js +147 -0
  39. package/dist/assets/sessionChangeSummary-CX2MJIkc.js +1 -0
  40. package/dist/assets/sessionLookup-z52pghDe.js +1 -0
  41. package/dist/assets/{sortable-tabs-strip-Biow4Fqu.js → sortable-tabs-strip-C5gGnikj.js} +1 -1
  42. package/dist/assets/src-BgLw5Nu2.css +2 -0
  43. package/dist/assets/{store-CL3WP48T.js → store-BDFdnk6l.js} +2 -2
  44. package/dist/assets/textarea-CIO34TZz.js +1 -0
  45. package/dist/assets/{uk-C8YKAz0x.js → uk-Di0vl9V-.js} +1 -1
  46. package/dist/assets/{ChatContainer-C04WuLo8.js → useAppFontEffects-DCVqvGQe.js} +46 -41
  47. package/dist/assets/useDesktopWindowControlsLayout-9IP1JKnB.js +1 -0
  48. package/dist/assets/useEffectiveDirectory-j1hFzIcz.js +1 -0
  49. package/dist/assets/useMobileNavigationStore-CL-8n4jx.js +412 -0
  50. package/dist/assets/useSessionAutoCleanup-Dfhza4uy.js +1 -0
  51. package/dist/assets/{useWorkerHighlightedLines-BaNK-wuY.js → useWorkerHighlightedLines-D49Ie_e4.js} +1 -1
  52. package/dist/assets/{zh-CN-CCYwUnmB.js → zh-CN-coFCfeQE.js} +1 -1
  53. package/dist/assets/{zh-TW-D8Y6py08.js → zh-TW-oDRZ1-yb.js} +1 -1
  54. package/dist/index.html +3 -3
  55. package/dist/mini-chat.html +3 -3
  56. package/dist/mobile.html +9 -9
  57. package/package.json +2 -1
  58. package/server/index.js +2 -0
  59. package/server/lib/assistant-delivery-parts.js +3 -1
  60. package/server/lib/assistant-delivery-parts.test.js +11 -0
  61. package/server/lib/assistants/DOCUMENTATION.md +76 -3
  62. package/server/lib/assistants/assign.js +225 -0
  63. package/server/lib/assistants/assign.test.js +118 -0
  64. package/server/lib/assistants/bubbles.js +45 -0
  65. package/server/lib/assistants/bubbles.test.js +36 -0
  66. package/server/lib/assistants/cards.js +148 -0
  67. package/server/lib/assistants/cards.test.js +116 -0
  68. package/server/lib/assistants/contact-store.js +284 -0
  69. package/server/lib/assistants/contact-store.test.js +131 -0
  70. package/server/lib/assistants/contact-tools.js +761 -0
  71. package/server/lib/assistants/contact-tools.test.js +328 -0
  72. package/server/lib/assistants/contracts.js +20 -1
  73. package/server/lib/assistants/harness.js +551 -0
  74. package/server/lib/assistants/harness.test.js +607 -0
  75. package/server/lib/assistants/routes.js +44 -4
  76. package/server/lib/assistants/service.js +930 -17
  77. package/server/lib/assistants/service.test.js +1163 -45
  78. package/server/lib/llm/DOCUMENTATION.md +84 -0
  79. package/server/lib/llm/catalog.js +100 -0
  80. package/server/lib/llm/catalog.test.js +78 -0
  81. package/server/lib/llm/completions.js +151 -0
  82. package/server/lib/llm/completions.test.js +127 -0
  83. package/server/lib/llm/generate.js +570 -0
  84. package/server/lib/llm/generate.test.js +459 -0
  85. package/server/lib/llm/routes.js +47 -0
  86. package/server/lib/llm/temp-directory.js +37 -0
  87. package/server/lib/notifications/runtime.js +52 -0
  88. package/server/lib/notifications/runtime.test.js +39 -0
  89. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  90. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  91. package/server/lib/opencode/settings-runtime.js +2 -2
  92. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  93. package/server/lib/session-goal/runtime.js +20 -0
  94. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  95. package/server/lib/session-index/service.js +10 -4
  96. package/server/lib/session-index/service.test.js +37 -0
  97. package/server/lib/session-title/runtime.js +1 -0
  98. package/server/lib/session-title/runtime.test.js +11 -0
  99. package/dist/assets/AssistantView-CQHey6je.js +0 -1
  100. package/dist/assets/ContextPanel-BJEFzgz7.js +0 -285
  101. package/dist/assets/MobileDetailNavigation-Dcl7Upvc.js +0 -1
  102. package/dist/assets/MobileSurface-CpAbBAQ5.js +0 -2
  103. package/dist/assets/SettingsView-DRiEDi23.js +0 -25
  104. package/dist/assets/insertionBoundaries-DcS10A9Q.js +0 -4
  105. package/dist/assets/main-Coj1Z4LN.js +0 -29
  106. package/dist/assets/miniChat-DK-d_3-7.js +0 -2
  107. package/dist/assets/mobile-8QzXy5wz.js +0 -2
  108. package/dist/assets/multirun-BFHWrDOR.js +0 -4
  109. package/dist/assets/projectMeta-CEGa4Czs.js +0 -3
  110. package/dist/assets/radio-Bw11T40N.js +0 -1
  111. package/dist/assets/renderElectronMiniChatApp-_fchgm-8.js +0 -2
  112. package/dist/assets/renderMobileApp-B1j1BUXB.js +0 -28
  113. package/dist/assets/runtimeEndpointReset-C-Sm4wpj.js +0 -4
  114. package/dist/assets/shikiHighlight-GaNLBDUk.js +0 -1
  115. package/dist/assets/src-BGao3JmJ.css +0 -2
  116. package/dist/assets/useAppFontEffects-D0UWTJui.js +0 -7
  117. package/dist/assets/useDesktopWindowControlsLayout-_5M7NHJ8.js +0 -1
  118. package/dist/assets/useEffectiveDirectory-DqOLKISv.js +0 -1
  119. package/dist/assets/useMobileNavigationStore-DxH1DjU-.js +0 -412
  120. package/dist/assets/useSessionAutoCleanup-Dpp0-zw2.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
  });