@openchambery/web 1.19.13-beta.3 → 1.19.13-beta.5
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.
- package/dist/assets/{AgentAvatar-mbvX7YlW.js → AgentAvatar-B5MiX004.js} +1 -1
- package/dist/assets/AssistantView-B6kZZdri.js +2 -0
- package/dist/assets/{DiagramView-W0IHFk-1.js → DiagramView-C40ituM2.js} +1 -1
- package/dist/assets/{FadeInOnReveal-moBbL8Ll.js → FadeInOnReveal-UGwClbL_.js} +1 -1
- package/dist/assets/{Icon-CkzzhaLr.js → Icon-ZAKaNHiG.js} +1 -1
- package/dist/assets/{MarkdownRenderer-v7n_D6nL.js → MarkdownRenderer-mcY597G0.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-Buz5LZsa.js → MarkdownRendererImpl-YsTrEeUY.js} +6 -3
- package/dist/assets/MarkstreamRendererImpl-kNx_G9Sd.js +3 -0
- package/dist/assets/MobileDetailNavigation-Uwzh6TWj.js +1 -0
- package/dist/assets/MobileShareBridge-D3oSiSC6.js +4 -0
- package/dist/assets/MobileSurface-B-SWjAj7.js +2 -0
- package/dist/assets/{MultiRunWindow-yWIHgr89.js → MultiRunWindow-LgdUonfa.js} +1 -1
- package/dist/assets/{OnboardingScreen-j0iCyH7f.js → OnboardingScreen-DHKq6y3W.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CDC5B3RH.js → OverlayScrollbar-Bahg_P3r.js} +1 -1
- package/dist/assets/{SettingsView-C3BbLpHT.js → SettingsView-iUiWTKbM.js} +8 -8
- package/dist/assets/{SettingsWindow-b0JWdygM.js → SettingsWindow-DCDEAC1n.js} +1 -1
- package/dist/assets/{TerminalView-C74IvmOt.js → TerminalView-CdZi_UXQ.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BORu8CtR.js → ToolOutputDialog-fZq6UHvy.js} +1 -1
- package/dist/assets/{appThemeRegistry-DOwhB93n.js → appThemeRegistry-DyFotcOM.js} +1 -1
- package/dist/assets/{button-xx1UOBA5.js → button-9R-WgVMl.js} +1 -1
- package/dist/assets/{clipboard-CUasgPiF.js → clipboard-CzXos-TI.js} +1 -1
- package/dist/assets/{collapsible-CACHBRYZ.js → collapsible-Bbz9KiJf.js} +1 -1
- package/dist/assets/{context-menu-CYbm4wBZ.js → context-menu-CSyOhRz8.js} +1 -1
- package/dist/assets/{desktopHosts-DILvrAok.js → desktopHosts-B052a4UK.js} +1 -1
- package/dist/assets/{desktopRelayRestore-t8asCr3m.js → desktopRelayRestore-OjKNXr10.js} +1 -1
- package/dist/assets/{diagram-u0BC9uuY.js → diagram-NdsZih8P.js} +1 -1
- package/dist/assets/{dialog-BwQ3rY_x.js → dialog-U4ML8hxt.js} +1 -1
- package/dist/assets/{es-CRjOwvYM.js → es-BtC17YQH.js} +1 -1
- package/dist/assets/fileMentionTouchSelection-DSKcxgU_.js +4 -0
- package/dist/assets/{fileQueries-Bgly-E8f.js → fileQueries-B_s_B-fH.js} +1 -1
- package/dist/assets/{fr-D9taEN9L.js → fr-DPohCCUO.js} +1 -1
- package/dist/assets/{gitApi-D1mvv14J.js → gitApi-BUubBKri.js} +3 -3
- package/dist/assets/{input-CI730njs.js → input-Cd_1HyQv.js} +1 -1
- package/dist/assets/insertionBoundaries-DeGd-RP9.js +1 -0
- package/dist/assets/{ja-CYQ5aoWp.js → ja-CflPxUNi.js} +1 -1
- package/dist/assets/{ko-BwASV0Q0.js → ko-CuSU80xd.js} +1 -1
- package/dist/assets/main-C6EluFKk.js +169 -0
- package/dist/assets/{main-phpzFgMN.js → main-iFGhn-zU.js} +2 -2
- package/dist/assets/{markdownHeightCache-DEjGKLv8.js → markdownHeightCache-BL8CUQ0M.js} +1 -1
- package/dist/assets/miniChat-Bs_H98PB.js +2 -0
- package/dist/assets/mobile-HfLUO-bG.js +2 -0
- package/dist/assets/{multirun-CYuxJPBX.js → multirun-BABuRg1O.js} +1 -1
- package/dist/assets/{persistence-YbQKzHUQ.js → persistence-DwRBC4zl.js} +2 -2
- package/dist/assets/{pl-DUI1vPil.js → pl-eJvVbQ74.js} +1 -1
- package/dist/assets/platform-DQ5_LNKO.js +2 -0
- package/dist/assets/{projectMeta-Cir66uF3.js → projectMeta-WcxDg0DZ.js} +2 -2
- package/dist/assets/{pt-BR-CZQFJllP.js → pt-BR-DSejAksY.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BzYqqr9n.js → renderElectronMiniChatApp-Cp-dVXj5.js} +2 -2
- package/dist/assets/renderMobileApp-B6rb6wuc.js +28 -0
- package/dist/assets/{runtimeConfig-BXf9GQ0Q.js → runtimeConfig-88DzHVMT.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CEbKea6x.js +147 -0
- package/dist/assets/sessionChangeSummary-C9071lsK.js +2 -0
- package/dist/assets/sessionLookup-CEw3tCz1.js +1 -0
- package/dist/assets/{sortable-tabs-strip-Dom6Sthu.js → sortable-tabs-strip-eL2whsf8.js} +1 -1
- package/dist/assets/src-B_DG0xy5.css +2 -0
- package/dist/assets/{store-CSnH_QXM.js → store-DHdMj3y4.js} +2 -2
- package/dist/assets/{terminalApi-noa4Cdbh.js → terminalApi-CfgOfnZk.js} +1 -1
- package/dist/assets/{textarea-BfNtUdJE.js → textarea-DE1UrdtK.js} +1 -1
- package/dist/assets/{tooltip-DPxumadF.js → tooltip-DSjr26A_.js} +1 -1
- package/dist/assets/{uk-C1FYZlmA.js → uk-DV9OgOAg.js} +1 -1
- package/dist/assets/{url-CVhsI9kR.js → url-CMQwHq4k.js} +1 -1
- package/dist/assets/{useAppFontEffects-NVyoI_Ei.js → useAppFontEffects-Ca0mASq5.js} +43 -44
- package/dist/assets/{useDesktopWindowControlsLayout-CP3NmMYG.js → useDesktopWindowControlsLayout-B3KJYYwp.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-AUoqSyOJ.js → useEffectiveDirectory-B4l9snxd.js} +1 -1
- package/dist/assets/{useFontPreferences-CB-cGo3I.js → useFontPreferences-DCaY8NbU.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CU2UwmN3.js +413 -0
- package/dist/assets/{useSessionAutoCleanup-ClvQQPqQ.js → useSessionAutoCleanup-B9El9yjX.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DOk4cAxv.js → useWorkerHighlightedLines-PPhFeDFI.js} +1 -1
- package/dist/assets/{utils-BHTVlVov.js → utils-2ZK8yboQ.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-DDoDDyM9.js → vendor-reactuses-core-BnW6GxJt.js} +1 -1
- package/dist/assets/{zh-CN-C96qP1ip.js → zh-CN-92n_rGTt.js} +1 -1
- package/dist/assets/{zh-TW-BsRk1Rbb.js → zh-TW-CwVxwJCj.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/index.js +1 -0
- package/server/lib/assistants/DOCUMENTATION.md +104 -18
- package/server/lib/assistants/assign.js +173 -13
- package/server/lib/assistants/assign.test.js +139 -0
- package/server/lib/assistants/bubbles.js +1 -1
- package/server/lib/assistants/contact-store.js +496 -11
- package/server/lib/assistants/contact-store.test.js +323 -7
- package/server/lib/assistants/contact-tools.js +514 -52
- package/server/lib/assistants/contact-tools.test.js +390 -7
- package/server/lib/assistants/contracts.js +31 -0
- package/server/lib/assistants/harness.js +224 -54
- package/server/lib/assistants/harness.test.js +372 -12
- package/server/lib/assistants/routes.js +3 -0
- package/server/lib/assistants/service.js +1079 -38
- package/server/lib/assistants/service.test.js +1235 -26
- package/server/lib/llm/DOCUMENTATION.md +2 -0
- package/server/lib/llm/completions.js +4 -0
- package/server/lib/llm/completions.test.js +3 -0
- package/server/lib/llm/generate.js +12 -5
- package/server/lib/llm/generate.test.js +40 -0
- package/server/lib/opencode/feature-routes-runtime.js +5 -0
- package/dist/assets/AssistantView-DOZPK9oh.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-D4nUXYpP.js +0 -3
- package/dist/assets/MobileDetailNavigation-Bfs4CTUh.js +0 -1
- package/dist/assets/MobileShareBridge-DtwVLkBL.js +0 -4
- package/dist/assets/MobileSurface-BmYYdY3g.js +0 -2
- package/dist/assets/insertionBoundaries-BBn8YW82.js +0 -4
- package/dist/assets/main-Cd7NwXx5.js +0 -169
- package/dist/assets/miniChat-D6UYe50H.js +0 -2
- package/dist/assets/mobile-w2Ed9I5O.js +0 -2
- package/dist/assets/platform-DzsGqlGW.js +0 -2
- package/dist/assets/renderMobileApp-BvIE86_O.js +0 -28
- package/dist/assets/runtimeEndpointReset-lF5S7U4I.js +0 -147
- package/dist/assets/sessionChangeSummary-DQDZ5HW-.js +0 -1
- package/dist/assets/sessionLookup-CNRDupYq.js +0 -1
- package/dist/assets/src-AA-5nXsF.css +0 -2
- package/dist/assets/useMobileNavigationStore-DANrWekj.js +0 -413
|
@@ -6,12 +6,20 @@ import {
|
|
|
6
6
|
CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
7
7
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
8
8
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
9
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
9
10
|
LIST_PROJECTS_TOOL_NAME,
|
|
10
11
|
LIST_SESSIONS_TOOL_NAME,
|
|
11
12
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
12
13
|
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
13
14
|
NEW_CONVERSATION_TOOL_NAME,
|
|
14
15
|
SCHEDULE_TASK_TOOL_NAME,
|
|
16
|
+
STOP_SESSION_TOOL_NAME,
|
|
17
|
+
STOPPED_SESSION_FALLBACK_BUBBLE,
|
|
18
|
+
UPDATE_DEFAULT_PROMPT_CONFIRM_BUBBLE,
|
|
19
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
20
|
+
UPDATE_DEFAULT_PROMPT_UNCHANGED_BUBBLE,
|
|
21
|
+
WATCH_SESSION_TOOL_NAME,
|
|
22
|
+
WATCHED_SESSION_FALLBACK_BUBBLE,
|
|
15
23
|
confirmBubbleAfterContactReset,
|
|
16
24
|
contactTurnHasSuccessfulReset,
|
|
17
25
|
createContactTools,
|
|
@@ -31,11 +39,45 @@ import {
|
|
|
31
39
|
import { attachmentScopeKey } from './assign.js';
|
|
32
40
|
|
|
33
41
|
describe('contact tool protocol', () => {
|
|
42
|
+
it('shows current-instance models with ordered favorites and recents using exact ids', () => {
|
|
43
|
+
const prompt = formatConnectedModelsPrompt([
|
|
44
|
+
{ providerID: 'p', providerName: 'Connected Provider', modelID: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' },
|
|
45
|
+
{ providerID: 'q', modelID: 'gpt-5.4', name: 'GPT 5.4' },
|
|
46
|
+
], { preferences: {
|
|
47
|
+
favoriteModels: [{ providerID: 'q', modelID: 'gpt-5.4', variant: 'high' }, { providerID: 'gone', modelID: 'offline' }],
|
|
48
|
+
recentModels: [{ providerID: 'p', modelID: 'claude-sonnet-4-6' }, { providerID: 'q', modelID: 'gpt-5.4' }],
|
|
49
|
+
} })
|
|
50
|
+
expect(prompt).toContain('Connected Provider')
|
|
51
|
+
expect(prompt).toContain('Favorite models (saved order)')
|
|
52
|
+
expect(prompt).toContain('Recent models (most recent first)')
|
|
53
|
+
expect(prompt).toContain('variant="high"')
|
|
54
|
+
expect(prompt).not.toContain('offline')
|
|
55
|
+
const recent = prompt.split('Recent models (most recent first)')[1]
|
|
56
|
+
expect(recent.indexOf('claude-sonnet-4-6')).toBeLessThan(recent.indexOf('gpt-5.4'))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('distinguishes unavailable model preferences from successfully empty lists', () => {
|
|
60
|
+
expect(formatConnectedModelsPrompt([], { preferences: null })).toContain('preferences unavailable')
|
|
61
|
+
expect(formatConnectedModelsPrompt([], { preferences: { favoriteModels: [], recentModels: [] } })).toContain('Favorite models (saved order): none')
|
|
62
|
+
expect(formatConnectedModelsPrompt([], { catalogAvailable: false })).toContain('catalog unavailable')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('parses watch_session and stop_session fences', () => {
|
|
66
|
+
const watchText = '```openchamber-tool\n{"name":"watch_session","arguments":{"sessionID":"ses_w"}}\n```';
|
|
67
|
+
expect(parseContactToolCalls(watchText, [WATCH_SESSION_TOOL_NAME, STOP_SESSION_TOOL_NAME]).toolCalls).toEqual([
|
|
68
|
+
{ name: WATCH_SESSION_TOOL_NAME, arguments: { sessionID: 'ses_w' } },
|
|
69
|
+
]);
|
|
70
|
+
const stopText = '好的 {"name":"stop_session","arguments":{"sessionID":"ses_s"}}';
|
|
71
|
+
expect(parseContactToolCalls(stopText, [WATCH_SESSION_TOOL_NAME, STOP_SESSION_TOOL_NAME]).toolCalls).toEqual([
|
|
72
|
+
{ name: STOP_SESSION_TOOL_NAME, arguments: { sessionID: 'ses_s' } },
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
34
76
|
it('parses an assign_session fence and strips it from chat text', () => {
|
|
35
77
|
const text = 'On it.\n\n```openchamber-tool\n{"name":"assign_session","arguments":{"prompt":"Fix login","projectPath":"/repo"}}\n```';
|
|
36
78
|
const parsed = parseContactToolCalls(text, [ASSIGN_SESSION_TOOL_NAME]);
|
|
37
79
|
expect(parsed.chatText).toBe('On it.');
|
|
38
|
-
expect(parsed.
|
|
80
|
+
expect(parsed.toolCalls[0]).toEqual({
|
|
39
81
|
name: ASSIGN_SESSION_TOOL_NAME,
|
|
40
82
|
arguments: { prompt: 'Fix login', projectPath: '/repo' },
|
|
41
83
|
});
|
|
@@ -48,7 +90,7 @@ describe('contact tool protocol', () => {
|
|
|
48
90
|
[CREATE_ASSISTANT_TOOL_NAME, SCHEDULE_TASK_TOOL_NAME, ASSIGN_SESSION_TOOL_NAME],
|
|
49
91
|
);
|
|
50
92
|
expect(create.chatText).toBe('好。');
|
|
51
|
-
expect(create.
|
|
93
|
+
expect(create.toolCalls[0]).toEqual({
|
|
52
94
|
name: CREATE_ASSISTANT_TOOL_NAME,
|
|
53
95
|
arguments: { name: 'FlowQA', model: 'opencode-go/deepseek-v4-flash' },
|
|
54
96
|
});
|
|
@@ -60,11 +102,11 @@ describe('contact tool protocol', () => {
|
|
|
60
102
|
'```openchamber-tool\n{"name":"message_assistant","arguments":{"to":"PeerQA","text":"hello-from-assistant 写好了"}}\n```',
|
|
61
103
|
[MESSAGE_ASSISTANT_TOOL_NAME],
|
|
62
104
|
);
|
|
63
|
-
expect(peer.
|
|
105
|
+
expect(peer.toolCalls[0]).toEqual({
|
|
64
106
|
name: MESSAGE_ASSISTANT_TOOL_NAME,
|
|
65
107
|
arguments: { to: 'PeerQA', text: 'hello-from-assistant 写好了' },
|
|
66
108
|
});
|
|
67
|
-
expect(schedule.
|
|
109
|
+
expect(schedule.toolCalls[0]).toEqual({
|
|
68
110
|
name: SCHEDULE_TASK_TOOL_NAME,
|
|
69
111
|
arguments: { name: 'Daily ping', prompt: 'ping', time: '18:00', timezone: 'Asia/Shanghai' },
|
|
70
112
|
});
|
|
@@ -75,7 +117,7 @@ describe('contact tool protocol', () => {
|
|
|
75
117
|
'好的,我来直接创建这个助理,不开编码会话。\n{"name":"create_assistant","arguments":{"name":"FlowNL","model":"opencode-go/deepseek-v4-flash"}}',
|
|
76
118
|
[CREATE_ASSISTANT_TOOL_NAME, ASSIGN_SESSION_TOOL_NAME],
|
|
77
119
|
);
|
|
78
|
-
expect(parsed.
|
|
120
|
+
expect(parsed.toolCalls[0]).toEqual({
|
|
79
121
|
name: CREATE_ASSISTANT_TOOL_NAME,
|
|
80
122
|
arguments: { name: 'FlowNL', model: 'opencode-go/deepseek-v4-flash' },
|
|
81
123
|
});
|
|
@@ -83,15 +125,107 @@ describe('contact tool protocol', () => {
|
|
|
83
125
|
expect(parsed.chatText).not.toContain('create_assistant');
|
|
84
126
|
});
|
|
85
127
|
|
|
128
|
+
it('accepts the existing single-call fence without a newline', () => {
|
|
129
|
+
expect(parseContactToolCalls('```openchamber-tool {"name":"read","arguments":{"path":"sample.md"}}```', ['read']).toolCalls)
|
|
130
|
+
.toEqual([{ name: 'read', arguments: { path: 'sample.md' } }]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('does not execute tool-shaped JSON inside other protocol or example fences', () => {
|
|
134
|
+
const text = '```openchamber-final\n{"status":"complete","text":"done","example":{"name":"bash","arguments":{"command":"pwd"}}}\n```';
|
|
135
|
+
expect(parseContactToolCalls(text, ['bash'])).toEqual({ chatText: text, toolCalls: [] });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it.each(['json\n', '\n', ''])('preserves complete tool payloads in generic fences: %s', (header) => {
|
|
139
|
+
const call = { name: 'read', arguments: { path: 'sample.md' } };
|
|
140
|
+
const result = parseContactToolCalls(`Before \`\`\`${header}${JSON.stringify(call)}\`\`\` After`, ['read']);
|
|
141
|
+
expect(result.toolCalls).toEqual([call]);
|
|
142
|
+
expect(result.chatText).toBe('Before After');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does not run nested or partial tool examples in generic fences', () => {
|
|
146
|
+
const text = '```json\n{"example":{"name":"bash","arguments":{"command":"pwd"}}}\n```';
|
|
147
|
+
expect(parseContactToolCalls(text, ['bash'])).toEqual({ chatText: text, toolCalls: [] });
|
|
148
|
+
const prose = '```\nExample: {"name":"bash","arguments":{"command":"pwd"}}\n```';
|
|
149
|
+
expect(parseContactToolCalls(prose, ['bash'])).toEqual({ chatText: prose, toolCalls: [] });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('retains every tool call in order across fenced and embedded JSON', () => {
|
|
153
|
+
const calls = [
|
|
154
|
+
{ name: 'read', arguments: { path: 'sample.md' } },
|
|
155
|
+
{ name: 'bash', arguments: { command: 'date +%F' } },
|
|
156
|
+
{ name: 'write', arguments: { path: 'result.md', content: 'done' } },
|
|
157
|
+
];
|
|
158
|
+
const text = `Before ${JSON.stringify(calls[0])}\n\n\`\`\`openchamber-tool\n${JSON.stringify(calls[1])}\n\`\`\`\nAfter ${JSON.stringify(calls[2])}`;
|
|
159
|
+
const result = parseContactToolCalls(text, ['read', 'bash', 'write']);
|
|
160
|
+
expect(result.toolCalls).toEqual(calls);
|
|
161
|
+
expect(result.chatText).toContain('Before');
|
|
162
|
+
expect(result.chatText).toContain('After');
|
|
163
|
+
expect(result.chatText).not.toContain('arguments');
|
|
164
|
+
expect(result.protocolError).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('keeps markdown fences and escaped quotes inside JSON string arguments', () => {
|
|
168
|
+
const calls = [
|
|
169
|
+
{ name: 'write', arguments: { path: 'note.md', content: '```json\n{"quoted": "value"}\n```' } },
|
|
170
|
+
{ name: 'read', arguments: { path: 'note.md' } },
|
|
171
|
+
];
|
|
172
|
+
const text = calls.map((call) => `\`\`\`openchamber-tool\n${JSON.stringify(call)}\n\`\`\``).join('\n');
|
|
173
|
+
expect(parseContactToolCalls(text, ['read', 'write'])).toEqual({ chatText: '', toolCalls: calls });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('preserves both calls from the observed read-sample and bash-date response', () => {
|
|
177
|
+
const calls = [
|
|
178
|
+
{ name: 'read', arguments: { path: 'notes/sample.md' } },
|
|
179
|
+
{ name: 'bash', arguments: { command: 'date +%F' } },
|
|
180
|
+
];
|
|
181
|
+
const text = calls.map((call) => `\`\`\`openchamber-tool\n${JSON.stringify(call)}\n\`\`\``).join('\n\n');
|
|
182
|
+
expect(parseContactToolCalls(text, ['read', 'bash'])).toEqual({ chatText: '', toolCalls: calls });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it.each([
|
|
186
|
+
'{bad json}',
|
|
187
|
+
'{"name":"unknown","arguments":{}}',
|
|
188
|
+
'{"name":"read","arguments":[]}',
|
|
189
|
+
'',
|
|
190
|
+
])('rejects the complete batch when an explicit tool fence is invalid: %s', (invalid) => {
|
|
191
|
+
const valid = '\`\`\`openchamber-tool\n{"name":"read","arguments":{"path":"sample.md"}}\n\`\`\`';
|
|
192
|
+
const result = parseContactToolCalls(`${valid}\n\`\`\`openchamber-tool\n${invalid}\n\`\`\``, ['read']);
|
|
193
|
+
expect(result.toolCalls).toEqual([]);
|
|
194
|
+
expect(result.protocolError).toEqual(expect.any(String));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('rejects a truncated explicit tool fence without executing an earlier call', () => {
|
|
198
|
+
const result = parseContactToolCalls('{"name":"read","arguments":{"path":"sample.md"}}\n\`\`\`openchamber-tool\n{"name":"read"', ['read']);
|
|
199
|
+
expect(result.toolCalls).toEqual([]);
|
|
200
|
+
expect(result.protocolError).toEqual(expect.any(String));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it.each(['', '{"name":"read","arguments":{"path":"sample.md"}}\n'])('rejects an unclosed outer JSON object without executing a nested call or valid prefix', (prefix) => {
|
|
204
|
+
const result = parseContactToolCalls(prefix + '{"example": {"name":"bash","arguments":{"command":"pwd"}}', ['read', 'bash']);
|
|
205
|
+
expect(result.toolCalls).toEqual([]);
|
|
206
|
+
expect(result.protocolError).toEqual(expect.any(String));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('does not discover nested JSON arguments or strings as additional tool calls', () => {
|
|
210
|
+
const nested = { name: 'bash', arguments: { command: 'pwd' } };
|
|
211
|
+
const call = { name: 'write', arguments: { path: 'result.json', content: JSON.stringify(nested), metadata: nested } };
|
|
212
|
+
expect(parseContactToolCalls(JSON.stringify(call), ['write', 'bash']).toolCalls).toEqual([call]);
|
|
213
|
+
expect(parseContactToolCalls(JSON.stringify({ example: nested }), ['bash']).toolCalls).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
86
216
|
it('detects 建助理 without treating 不要开编码 session as assign_session', () => {
|
|
87
217
|
const tools = [
|
|
88
218
|
NEW_CONVERSATION_TOOL_NAME,
|
|
89
219
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
90
220
|
LIST_PROJECTS_TOOL_NAME,
|
|
91
221
|
LIST_SESSIONS_TOOL_NAME,
|
|
222
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
223
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
92
224
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
93
225
|
SCHEDULE_TASK_TOOL_NAME,
|
|
94
226
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
227
|
+
WATCH_SESSION_TOOL_NAME,
|
|
228
|
+
STOP_SESSION_TOOL_NAME,
|
|
95
229
|
ASSIGN_SESSION_TOOL_NAME,
|
|
96
230
|
];
|
|
97
231
|
expect(detectRequestedContactTools('帮我新建一个助理,名叫 FlowNL,不要开编码 session', tools)).toEqual([
|
|
@@ -100,6 +234,14 @@ describe('contact tool protocol', () => {
|
|
|
100
234
|
expect(detectRequestedContactTools('每天 18:00 排一个 ping 定时任务', tools)).toEqual([SCHEDULE_TASK_TOOL_NAME]);
|
|
101
235
|
expect(detectRequestedContactTools('建会话写一个文件', tools)).toEqual([ASSIGN_SESSION_TOOL_NAME]);
|
|
102
236
|
expect(detectRequestedContactTools('对,你直接派给那个项目组,再建个会话去修这个问题', tools)).toEqual([ASSIGN_SESSION_TOOL_NAME]);
|
|
237
|
+
expect(detectRequestedContactTools('继续这个会话修 login', tools)).toEqual([ASSIGN_SESSION_TOOL_NAME]);
|
|
238
|
+
expect(detectRequestedContactTools('监听这个会话', tools)).toEqual([WATCH_SESSION_TOOL_NAME]);
|
|
239
|
+
expect(detectRequestedContactTools('watch this session', tools)).toEqual([WATCH_SESSION_TOOL_NAME]);
|
|
240
|
+
expect(detectRequestedContactTools('停止这个会话', tools)).toEqual([STOP_SESSION_TOOL_NAME]);
|
|
241
|
+
expect(detectRequestedContactTools('stop that session', tools)).toEqual([STOP_SESSION_TOOL_NAME]);
|
|
242
|
+
// Plain @session reference is context only — not auto watch/assign/stop.
|
|
243
|
+
expect(detectRequestedContactTools('看看 @session:ses_abc 里上次说了啥', tools)).toEqual([]);
|
|
244
|
+
expect(detectRequestedContactTools('@session:ses_abc', tools)).toEqual([]);
|
|
103
245
|
expect(detectRequestedContactTools('写一个文件', tools)).toEqual([]);
|
|
104
246
|
expect(detectRequestedContactTools('pwd', tools)).toEqual([]);
|
|
105
247
|
expect(detectRequestedContactTools('给 PeerQA 说一声 hello-from-assistant 写好了', tools)).toEqual([
|
|
@@ -118,6 +260,11 @@ describe('contact tool protocol', () => {
|
|
|
118
260
|
expect(detectRequestedContactTools('找项目 openchamber yee', tools)).toEqual([LIST_PROJECTS_TOOL_NAME]);
|
|
119
261
|
expect(detectRequestedContactTools('看看现有对话', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
|
|
120
262
|
expect(detectRequestedContactTools('list sessions in that project', tools)).toEqual([LIST_SESSIONS_TOOL_NAME]);
|
|
263
|
+
expect(detectRequestedContactTools('看看我的默认提示词', tools)).toEqual([GET_ASSISTANT_SETTINGS_TOOL_NAME]);
|
|
264
|
+
expect(detectRequestedContactTools('看看 OpenCode 配置助手的默认提示词', tools)).toEqual([GET_ASSISTANT_SETTINGS_TOOL_NAME]);
|
|
265
|
+
expect(detectRequestedContactTools('把默认提示词改成简洁中文', tools)).toEqual([UPDATE_DEFAULT_PROMPT_TOOL_NAME]);
|
|
266
|
+
expect(detectRequestedContactTools('改 OpenCode 配置助手的默认提示词', tools)).toEqual([UPDATE_DEFAULT_PROMPT_TOOL_NAME]);
|
|
267
|
+
expect(detectRequestedContactTools('update default prompt to be terse', tools)).toEqual([UPDATE_DEFAULT_PROMPT_TOOL_NAME]);
|
|
121
268
|
expect(detectRequestedContactTools('开新对话', tools)).not.toContain(ASSIGN_SESSION_TOOL_NAME);
|
|
122
269
|
expect(detectRequestedContactTools('开新对话', tools)).not.toContain(CLEAR_CHAT_HISTORY_TOOL_NAME);
|
|
123
270
|
expect(detectRequestedContactTools('不要开编码 session', tools)).toEqual([]);
|
|
@@ -130,14 +277,14 @@ describe('contact tool protocol', () => {
|
|
|
130
277
|
|
|
131
278
|
it('parses bash fences when the pi coding tools are allowed', () => {
|
|
132
279
|
const text = '```openchamber-tool\n{"name":"bash","arguments":{"command":"pwd"}}\n```';
|
|
133
|
-
expect(parseContactToolCalls(text, ['bash', ASSIGN_SESSION_TOOL_NAME]).
|
|
280
|
+
expect(parseContactToolCalls(text, ['bash', ASSIGN_SESSION_TOOL_NAME]).toolCalls[0]).toEqual({
|
|
134
281
|
name: 'bash',
|
|
135
282
|
arguments: { command: 'pwd' },
|
|
136
283
|
});
|
|
137
284
|
expect(parseContactToolCalls(
|
|
138
285
|
'```openchamber-tool\n{"name":"glob","arguments":{"pattern":"*"}}\n```',
|
|
139
286
|
['glob', 'bash'],
|
|
140
|
-
).
|
|
287
|
+
).toolCalls).toEqual([]);
|
|
141
288
|
});
|
|
142
289
|
|
|
143
290
|
it('tells DeepSeek to call tools from natural language, not slash commands', () => {
|
|
@@ -146,20 +293,31 @@ describe('contact tool protocol', () => {
|
|
|
146
293
|
expect(prompt).toContain('clear_chat_history');
|
|
147
294
|
expect(prompt).toContain('list_projects');
|
|
148
295
|
expect(prompt).toContain('list_sessions');
|
|
296
|
+
expect(prompt).toContain('get_assistant_settings');
|
|
297
|
+
expect(prompt).toContain('update_default_prompt');
|
|
149
298
|
expect(prompt).toContain('create_assistant');
|
|
150
299
|
expect(prompt).toContain('schedule_task');
|
|
151
300
|
expect(prompt).toContain('message_assistant');
|
|
152
301
|
expect(prompt).toContain('assign_session');
|
|
302
|
+
expect(prompt).toContain('watch_session');
|
|
303
|
+
expect(prompt).toContain('stop_session');
|
|
153
304
|
expect(prompt).toContain('开新对话');
|
|
154
305
|
expect(prompt).toContain('清除记忆');
|
|
155
306
|
expect(prompt).toContain('清空聊天记录');
|
|
156
307
|
expect(prompt).toContain('找项目');
|
|
157
308
|
expect(prompt).toContain('现有对话');
|
|
309
|
+
expect(prompt).toContain('默认提示词');
|
|
310
|
+
expect(prompt).toContain('助手设定');
|
|
158
311
|
expect(prompt).toContain('建助理');
|
|
159
312
|
expect(prompt).toContain('排定时任务');
|
|
160
313
|
expect(prompt).toContain('说一声');
|
|
314
|
+
expect(prompt).toContain('监听会话');
|
|
315
|
+
expect(prompt).toContain('停止会话');
|
|
316
|
+
expect(prompt).toContain('plain @session:id reference alone is context');
|
|
161
317
|
expect(prompt).toContain('session/new');
|
|
162
318
|
expect(prompt).toContain('LLM memory only');
|
|
319
|
+
expect(prompt).toContain('Persists');
|
|
320
|
+
expect(prompt).toContain('later turns');
|
|
163
321
|
expect(prompt).not.toContain('/card');
|
|
164
322
|
expect(prompt).not.toContain('/dm');
|
|
165
323
|
expect(prompt).toContain('A reply without the tool call does nothing');
|
|
@@ -273,10 +431,18 @@ describe('createContactTools', () => {
|
|
|
273
431
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
274
432
|
LIST_PROJECTS_TOOL_NAME,
|
|
275
433
|
LIST_SESSIONS_TOOL_NAME,
|
|
434
|
+
'read_session',
|
|
435
|
+
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
436
|
+
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
276
437
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
277
438
|
SCHEDULE_TASK_TOOL_NAME,
|
|
278
439
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
279
440
|
ASSIGN_SESSION_TOOL_NAME,
|
|
441
|
+
WATCH_SESSION_TOOL_NAME,
|
|
442
|
+
STOP_SESSION_TOOL_NAME,
|
|
443
|
+
'steer_session',
|
|
444
|
+
'archive_session',
|
|
445
|
+
'delete_session',
|
|
280
446
|
]);
|
|
281
447
|
expect(tools.some((tool) => ['bash', 'edit', 'read', 'write'].includes(tool.name))).toBe(false);
|
|
282
448
|
|
|
@@ -355,6 +521,91 @@ describe('createContactTools', () => {
|
|
|
355
521
|
expect(onCard).toHaveBeenCalledTimes(3);
|
|
356
522
|
});
|
|
357
523
|
|
|
524
|
+
it.each(['assign_session', 'watch_session'])('%s forwards AbortSignal and drops a late card after cancellation', async (name) => {
|
|
525
|
+
const controller = new AbortController();
|
|
526
|
+
const onCard = vi.fn();
|
|
527
|
+
const action = vi.fn(async input => {
|
|
528
|
+
expect(input.signal).toBe(controller.signal);
|
|
529
|
+
controller.abort();
|
|
530
|
+
return { sessionID: 'ses_late', directory: '/project', title: 'Late', status: 'busy' };
|
|
531
|
+
});
|
|
532
|
+
const tools = createContactTools({ assignWork: action, watchSession: action, onCard });
|
|
533
|
+
const tool = tools.find(t => t.name === name);
|
|
534
|
+
const result = await tool.execute('call', { sessionID: 'ses_late', prompt: 'work' }, controller.signal);
|
|
535
|
+
expect(result.details.error).toBeDefined();
|
|
536
|
+
expect(onCard).not.toHaveBeenCalled();
|
|
537
|
+
await tool.execute('call2', { sessionID: 'ses_late', prompt: 'work' }, controller.signal);
|
|
538
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('recognizes explicit session mutation requests and forwards cancellation signals', async () => {
|
|
542
|
+
const action = vi.fn(async () => ({ admitted: true }));
|
|
543
|
+
const tools = createContactTools({ steerSession: action, archiveSession: action, deleteSession: action });
|
|
544
|
+
expect(detectRequestedContactTools('取消这个 session', tools.map(t => t.name))).toEqual(['stop_session']);
|
|
545
|
+
expect(detectRequestedContactTools('归档这个会话', tools.map(t => t.name))).toEqual(['archive_session']);
|
|
546
|
+
expect(detectRequestedContactTools('删除这个 session', tools.map(t => t.name))).toEqual(['delete_session']);
|
|
547
|
+
expect(detectRequestedContactTools('给这个会话插话:先运行测试', tools.map(t => t.name))).toEqual(['steer_session']);
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
const result = await tools.find(t => t.name === 'steer_session').execute('call', { sessionID: 'ses_exact', text: '先运行测试' }, controller.signal);
|
|
550
|
+
expect(action).toHaveBeenCalledWith({ sessionID: 'ses_exact', text: '先运行测试', signal: controller.signal });
|
|
551
|
+
expect(result.terminate).toBe(true);
|
|
552
|
+
expect(result.isError).not.toBe(true);
|
|
553
|
+
const invalid = await tools.find(t => t.name === 'steer_session').execute('call2', { sessionID: 'ses_exact', text: ' ' });
|
|
554
|
+
expect(invalid.details.error).toBe('validation_error');
|
|
555
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('watch_session emits a baseline session card and stop_session returns true abort results', async () => {
|
|
559
|
+
const onCard = vi.fn();
|
|
560
|
+
const watchSession = vi.fn(async () => ({
|
|
561
|
+
sessionID: 'ses_watch',
|
|
562
|
+
directory: '/repo',
|
|
563
|
+
title: 'Watched',
|
|
564
|
+
status: 'complete',
|
|
565
|
+
watched: true,
|
|
566
|
+
reused: true,
|
|
567
|
+
}));
|
|
568
|
+
const stopSession = vi.fn(async () => ({
|
|
569
|
+
sessionID: 'ses_watch',
|
|
570
|
+
directory: '/repo',
|
|
571
|
+
title: 'Watched',
|
|
572
|
+
aborted: true,
|
|
573
|
+
}));
|
|
574
|
+
const tools = createContactTools({ watchSession, stopSession, onCard });
|
|
575
|
+
const watched = await tools.find((tool) => tool.name === WATCH_SESSION_TOOL_NAME).execute('call_w', {
|
|
576
|
+
sessionID: 'ses_watch',
|
|
577
|
+
});
|
|
578
|
+
expect(watchSession).toHaveBeenCalledWith({ sessionID: 'ses_watch' });
|
|
579
|
+
expect(watched.terminate).toBe(true);
|
|
580
|
+
expect(watched.content[0].text).toBe(WATCHED_SESSION_FALLBACK_BUBBLE);
|
|
581
|
+
expect(watched.details.card).toMatchObject({
|
|
582
|
+
type: 'card',
|
|
583
|
+
cardType: 'session',
|
|
584
|
+
sessionID: 'ses_watch',
|
|
585
|
+
status: 'complete',
|
|
586
|
+
});
|
|
587
|
+
expect(onCard).toHaveBeenCalledTimes(1);
|
|
588
|
+
|
|
589
|
+
const stopped = await tools.find((tool) => tool.name === STOP_SESSION_TOOL_NAME).execute('call_s', {
|
|
590
|
+
sessionID: 'ses_watch',
|
|
591
|
+
});
|
|
592
|
+
expect(stopSession).toHaveBeenCalledWith({ sessionID: 'ses_watch' });
|
|
593
|
+
expect(stopped.terminate).toBe(true);
|
|
594
|
+
expect(stopped.content[0].text).toBe(STOPPED_SESSION_FALLBACK_BUBBLE);
|
|
595
|
+
expect(stopped.details.stopped).toMatchObject({ aborted: true, sessionID: 'ses_watch' });
|
|
596
|
+
|
|
597
|
+
const failedStop = createContactTools({
|
|
598
|
+
stopSession: async () => {
|
|
599
|
+
throw new AssignError(ASSIGN_CODES.UPSTREAM, 'abort refused');
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
const failed = await failedStop.find((tool) => tool.name === STOP_SESSION_TOOL_NAME).execute('call_f', {
|
|
603
|
+
sessionID: 'ses_watch',
|
|
604
|
+
});
|
|
605
|
+
expect(failed.details.error).toBe('upstream_error');
|
|
606
|
+
expect(failed.content[0].text).toContain('abort refused');
|
|
607
|
+
});
|
|
608
|
+
|
|
358
609
|
it('forwards server turn file parts on assign and keeps model selection in the tool args', async () => {
|
|
359
610
|
const image = { type: 'file', mime: 'image/png', url: 'data:image/png;base64,aa', filename: 'shot.png' };
|
|
360
611
|
const assignWork = vi.fn(async (params) => ({
|
|
@@ -535,6 +786,138 @@ describe('createContactTools', () => {
|
|
|
535
786
|
expect(failed.content[0].text).toContain('Session index is unavailable');
|
|
536
787
|
expect(failed.details.sessions).toBeUndefined();
|
|
537
788
|
});
|
|
789
|
+
|
|
790
|
+
it('reads assistant settings via live callback and formats empty defaultPrompt', async () => {
|
|
791
|
+
const readAssistantSettings = vi.fn(async () => ({
|
|
792
|
+
id: 'asst_host',
|
|
793
|
+
name: 'DeepSeekQA',
|
|
794
|
+
defaultPrompt: '',
|
|
795
|
+
providerID: 'p',
|
|
796
|
+
modelID: 'm',
|
|
797
|
+
agent: null,
|
|
798
|
+
variant: null,
|
|
799
|
+
mode: 'continuous',
|
|
800
|
+
workspacePath: null,
|
|
801
|
+
enabled: true,
|
|
802
|
+
}));
|
|
803
|
+
const tools = createContactTools({ readAssistantSettings });
|
|
804
|
+
const result = await tools.find((tool) => tool.name === GET_ASSISTANT_SETTINGS_TOOL_NAME).execute('call_get', {});
|
|
805
|
+
expect(readAssistantSettings).toHaveBeenCalledTimes(1);
|
|
806
|
+
expect(result.details.settings).toMatchObject({
|
|
807
|
+
id: 'asst_host',
|
|
808
|
+
name: 'DeepSeekQA',
|
|
809
|
+
defaultPrompt: '',
|
|
810
|
+
providerID: 'p',
|
|
811
|
+
modelID: 'm',
|
|
812
|
+
enabled: true,
|
|
813
|
+
});
|
|
814
|
+
expect(result.content[0].text).toContain('defaultPrompt: (empty)');
|
|
815
|
+
expect(result.terminate).toBe(false);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('updates default prompt, no-ops when unchanged, and reports callback failures', async () => {
|
|
819
|
+
const updateAssistantSettings = vi.fn(async ({ defaultPrompt }) => {
|
|
820
|
+
if (defaultPrompt === 'same') {
|
|
821
|
+
return { updated: false, unchanged: true, defaultPrompt: 'same' };
|
|
822
|
+
}
|
|
823
|
+
return { updated: true, defaultPrompt };
|
|
824
|
+
});
|
|
825
|
+
const tools = createContactTools({ updateAssistantSettings });
|
|
826
|
+
const update = tools.find((tool) => tool.name === UPDATE_DEFAULT_PROMPT_TOOL_NAME);
|
|
827
|
+
|
|
828
|
+
const saved = await update.execute('call_set', { prompt: 'Be terse.' });
|
|
829
|
+
expect(updateAssistantSettings).toHaveBeenCalledWith({ defaultPrompt: 'Be terse.' });
|
|
830
|
+
expect(saved.details).toEqual({ updated: true, defaultPrompt: 'Be terse.' });
|
|
831
|
+
expect(saved.content[0].text).toBe(UPDATE_DEFAULT_PROMPT_CONFIRM_BUBBLE);
|
|
832
|
+
expect(saved.terminate).toBe(false);
|
|
833
|
+
|
|
834
|
+
const cleared = await update.execute('call_clear', { prompt: '' });
|
|
835
|
+
expect(updateAssistantSettings).toHaveBeenCalledWith({ defaultPrompt: '' });
|
|
836
|
+
expect(cleared.details).toEqual({ updated: true, defaultPrompt: '' });
|
|
837
|
+
expect(cleared.terminate).toBe(false);
|
|
838
|
+
|
|
839
|
+
const unchanged = await update.execute('call_same', { prompt: 'same' });
|
|
840
|
+
expect(unchanged.details).toEqual({ updated: false, unchanged: true, defaultPrompt: 'same' });
|
|
841
|
+
expect(unchanged.content[0].text).toBe(UPDATE_DEFAULT_PROMPT_UNCHANGED_BUBBLE);
|
|
842
|
+
expect(unchanged.terminate).toBe(false);
|
|
843
|
+
|
|
844
|
+
const missing = await update.execute('call_missing', {});
|
|
845
|
+
expect(missing.details.error).toBe('validation_error');
|
|
846
|
+
expect(missing.terminate).toBe(true);
|
|
847
|
+
|
|
848
|
+
updateAssistantSettings.mockRejectedValueOnce(Object.assign(new Error('revision_conflict'), { code: 'revision_conflict' }));
|
|
849
|
+
const conflicted = await update.execute('call_conflict', { prompt: 'X' });
|
|
850
|
+
expect(conflicted.details.error).toBe('revision_conflict');
|
|
851
|
+
expect(conflicted.terminate).toBe(true);
|
|
852
|
+
|
|
853
|
+
updateAssistantSettings.mockRejectedValueOnce(Object.assign(new Error('sqlite busy'), { code: 'upstream_error' }));
|
|
854
|
+
const storageFailed = await update.execute('call_storage', { prompt: 'Y' });
|
|
855
|
+
expect(storageFailed.details.error).toBe('upstream_error');
|
|
856
|
+
expect(storageFailed.terminate).toBe(true);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('reads and updates another assistant defaultPrompt by name without changing self', async () => {
|
|
860
|
+
const peer = {
|
|
861
|
+
id: 'asst_peer',
|
|
862
|
+
name: 'OpenCode 配置助手',
|
|
863
|
+
defaultPrompt: 'old peer',
|
|
864
|
+
providerID: 'p',
|
|
865
|
+
modelID: 'm',
|
|
866
|
+
agent: null,
|
|
867
|
+
variant: null,
|
|
868
|
+
mode: 'continuous',
|
|
869
|
+
workspacePath: null,
|
|
870
|
+
enabled: true,
|
|
871
|
+
};
|
|
872
|
+
const host = {
|
|
873
|
+
id: 'asst_host',
|
|
874
|
+
name: 'Host',
|
|
875
|
+
defaultPrompt: 'host prompt',
|
|
876
|
+
providerID: 'p',
|
|
877
|
+
modelID: 'm',
|
|
878
|
+
agent: null,
|
|
879
|
+
variant: null,
|
|
880
|
+
mode: 'continuous',
|
|
881
|
+
workspacePath: null,
|
|
882
|
+
enabled: true,
|
|
883
|
+
};
|
|
884
|
+
const readAssistantSettings = vi.fn(async (input) => (
|
|
885
|
+
input?.assistantID === 'asst_peer' ? peer : host
|
|
886
|
+
));
|
|
887
|
+
const updateAssistantSettings = vi.fn(async (patch) => ({
|
|
888
|
+
updated: true,
|
|
889
|
+
defaultPrompt: patch.defaultPrompt,
|
|
890
|
+
}));
|
|
891
|
+
const listAssistants = vi.fn(async () => [host, peer]);
|
|
892
|
+
const tools = createContactTools({
|
|
893
|
+
readAssistantSettings,
|
|
894
|
+
updateAssistantSettings,
|
|
895
|
+
listAssistants,
|
|
896
|
+
currentAssistant: host,
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
const got = await tools.find((tool) => tool.name === GET_ASSISTANT_SETTINGS_TOOL_NAME)
|
|
900
|
+
.execute('call_peer_get', { to: 'OpenCode 配置助手' });
|
|
901
|
+
expect(readAssistantSettings).toHaveBeenCalledWith({ assistantID: 'asst_peer' });
|
|
902
|
+
expect(got.details.settings).toMatchObject({ id: 'asst_peer', name: 'OpenCode 配置助手', defaultPrompt: 'old peer' });
|
|
903
|
+
|
|
904
|
+
const saved = await tools.find((tool) => tool.name === UPDATE_DEFAULT_PROMPT_TOOL_NAME)
|
|
905
|
+
.execute('call_peer_set', { to: 'OpenCode 配置助手', prompt: 'new peer persona' });
|
|
906
|
+
expect(updateAssistantSettings).toHaveBeenCalledWith({
|
|
907
|
+
defaultPrompt: 'new peer persona',
|
|
908
|
+
assistantID: 'asst_peer',
|
|
909
|
+
});
|
|
910
|
+
expect(saved.details).toMatchObject({
|
|
911
|
+
updated: true,
|
|
912
|
+
defaultPrompt: 'new peer persona',
|
|
913
|
+
assistantID: 'asst_peer',
|
|
914
|
+
name: 'OpenCode 配置助手',
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
const missing = await tools.find((tool) => tool.name === UPDATE_DEFAULT_PROMPT_TOOL_NAME)
|
|
918
|
+
.execute('call_missing_peer', { to: '没有这个助手', prompt: 'x' });
|
|
919
|
+
expect(missing.details.error).toBe('not_found');
|
|
920
|
+
});
|
|
538
921
|
});
|
|
539
922
|
|
|
540
923
|
describe('confirmBubbleAfterContactReset', () => {
|
|
@@ -28,10 +28,41 @@ export const assistantContractFixtures = Object.freeze({
|
|
|
28
28
|
text: 'Fixture reply',
|
|
29
29
|
fallbackKind: null,
|
|
30
30
|
}),
|
|
31
|
+
/** Countable assistant/peer replies after the shared read watermark. */
|
|
32
|
+
unreadCount: 0,
|
|
33
|
+
/** Persisted shared read watermark (multi-client). */
|
|
34
|
+
readWatermark: Object.freeze({
|
|
35
|
+
generation: 0,
|
|
36
|
+
ordinal: 2,
|
|
37
|
+
messageID: 'msg_contact_fixture',
|
|
38
|
+
}),
|
|
39
|
+
/** Highest safe mark-read tip (transcript head + generation). */
|
|
40
|
+
readTip: Object.freeze({
|
|
41
|
+
generation: 0,
|
|
42
|
+
ordinal: 2,
|
|
43
|
+
messageID: 'msg_contact_fixture',
|
|
44
|
+
}),
|
|
31
45
|
createdAt: 1234,
|
|
32
46
|
updatedAt: 1234,
|
|
33
47
|
tombstoneAt: null,
|
|
34
48
|
}),
|
|
49
|
+
/** Frozen POST /assistants/:id/contact/read response. */
|
|
50
|
+
contactReadResponse: Object.freeze({
|
|
51
|
+
assistantID: 'assistant_fixture',
|
|
52
|
+
changed: true,
|
|
53
|
+
unreadCount: 0,
|
|
54
|
+
readWatermark: Object.freeze({
|
|
55
|
+
generation: 0,
|
|
56
|
+
ordinal: 2,
|
|
57
|
+
messageID: 'msg_contact_fixture',
|
|
58
|
+
}),
|
|
59
|
+
readTip: Object.freeze({
|
|
60
|
+
generation: 0,
|
|
61
|
+
ordinal: 2,
|
|
62
|
+
messageID: 'msg_contact_fixture',
|
|
63
|
+
}),
|
|
64
|
+
revision: 4,
|
|
65
|
+
}),
|
|
35
66
|
sessionBinding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }),
|
|
36
67
|
compactResponse: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), summarized: true }),
|
|
37
68
|
messageAdmission: Object.freeze({ binding: Object.freeze({ sessionID: 'ses_fixture', directory: '/workspace', sessionGeneration: 4 }), messageID: 'msg_fixture', admitted: true, revision: 4 }),
|