@openchambery/web 1.19.13 → 1.19.15-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentAvatar-mbvX7YlW.js → AgentAvatar-m1jNq4bX.js} +1 -1
- package/dist/assets/AssistantView-D5rEZ-Ss.js +2 -0
- package/dist/assets/{DiagramView-BdI4PT2H.js → DiagramView-DaB9Sb7S.js} +1 -1
- package/dist/assets/{FadeInOnReveal-moBbL8Ll.js → FadeInOnReveal-UGwClbL_.js} +1 -1
- package/dist/assets/Icon-D1BcR8Tz.js +1 -0
- package/dist/assets/MarkdownRenderer-emLeoNjI.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-2ewbRZE2.js → MarkdownRendererImpl-BWCAPlqn.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-DXewO7S_.js → MarkstreamRendererImpl-BD_NFKUv.js} +2 -2
- package/dist/assets/MobileDetailNavigation-DqXVAIap.js +1 -0
- package/dist/assets/MobileShareBridge-Bk2fdkJk.js +4 -0
- package/dist/assets/MobileSurface-C4mpcOCh.js +2 -0
- package/dist/assets/{MultiRunWindow-C8ukIdLA.js → MultiRunWindow-rs49_K1E.js} +1 -1
- package/dist/assets/{OnboardingScreen-j0iCyH7f.js → OnboardingScreen-bIWeMLVx.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CDC5B3RH.js → OverlayScrollbar-Bahg_P3r.js} +1 -1
- package/dist/assets/SettingsView-b8LUjoad.js +25 -0
- package/dist/assets/{SettingsWindow-CSePZ_V8.js → SettingsWindow-D8JFlPdA.js} +1 -1
- package/dist/assets/{TerminalView-BmSIrZ81.js → TerminalView-DCd-Godi.js} +1 -1
- package/dist/assets/{ToolOutputDialog-2BYaWLo1.js → ToolOutputDialog-B50L_nUP.js} +1 -1
- package/dist/assets/{appThemeRegistry-Dk4gBaOu.js → appThemeRegistry-BWtCaTMe.js} +1 -1
- package/dist/assets/{button-xx1UOBA5.js → button-9R-WgVMl.js} +1 -1
- package/dist/assets/{clipboard-CUasgPiF.js → clipboard-GUUNlWIv.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-DhMGbF_B.js} +1 -1
- package/dist/assets/{es-CRjOwvYM.js → es-BQb6I8gl.js} +2 -2
- package/dist/assets/fileMentionTouchSelection-DKYkoypI.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-BjGouC_l.js} +2 -2
- package/dist/assets/{gitApi-DtPf-h-O.js → gitApi-D4PyYgLZ.js} +3 -3
- package/dist/assets/{input-CI730njs.js → input-Cd_1HyQv.js} +1 -1
- package/dist/assets/insertionBoundaries-Bnnx03Wj.js +1 -0
- package/dist/assets/{ja-CYQ5aoWp.js → ja-Cw9zliYY.js} +2 -2
- package/dist/assets/{ko-BwASV0Q0.js → ko-Dyz0Dk9b.js} +2 -2
- package/dist/assets/main-BRiQTNFX.js +169 -0
- package/dist/assets/{main-Ci3JmtpW.js → main-CnY0LWNc.js} +2 -2
- package/dist/assets/{markdownHeightCache-DEjGKLv8.js → markdownHeightCache-BL8CUQ0M.js} +1 -1
- package/dist/assets/miniChat-B0pw_mUs.js +2 -0
- package/dist/assets/mobile-BKHmxwXz.js +2 -0
- package/dist/assets/{multirun-CmaTeZON.js → multirun-BQKqVfFT.js} +1 -1
- package/dist/assets/{persistence-YbQKzHUQ.js → persistence-DwRBC4zl.js} +2 -2
- package/dist/assets/{pl-DUI1vPil.js → pl-C7EY-dTH.js} +1 -1
- package/dist/assets/platform-DQ5_LNKO.js +2 -0
- package/dist/assets/{projectMeta-cw9nrwpr.js → projectMeta-BvG8sdtC.js} +2 -2
- package/dist/assets/{pt-BR-CZQFJllP.js → pt-BR-B1uHpjFW.js} +2 -2
- package/dist/assets/renderElectronMiniChatApp-E5a0yy7E.js +2 -0
- package/dist/assets/renderMobileApp-DT0L4Ah6.js +28 -0
- package/dist/assets/{runtimeConfig-DI8l0VyZ.js → runtimeConfig-utWzrDwa.js} +1 -1
- package/dist/assets/runtimeEndpointReset-DE6lwHyg.js +147 -0
- package/dist/assets/sessionChangeSummary-BG7l2heP.js +2 -0
- package/dist/assets/sessionLookup-12zVOR0J.js +1 -0
- package/dist/assets/{sortable-tabs-strip-Dom6Sthu.js → sortable-tabs-strip-dYW6SH62.js} +1 -1
- package/dist/assets/src-CFUi4riW.css +2 -0
- package/dist/assets/{store-CSnH_QXM.js → store-ClnJdJSj.js} +3 -3
- package/dist/assets/{terminalApi-noa4Cdbh.js → terminalApi-CfgOfnZk.js} +1 -1
- package/dist/assets/{textarea-BfNtUdJE.js → textarea-B9CmN13Z.js} +1 -1
- package/dist/assets/{tooltip-DPxumadF.js → tooltip-DSjr26A_.js} +1 -1
- package/dist/assets/{uk-C1FYZlmA.js → uk-DgG8v2md.js} +2 -2
- package/dist/assets/{url-CVhsI9kR.js → url-CMQwHq4k.js} +1 -1
- package/dist/assets/{useAppFontEffects-BDFUND4v.js → useAppFontEffects-BhYuP4L2.js} +43 -44
- package/dist/assets/useDesktopWindowControlsLayout-DPNMRkrI.js +1 -0
- package/dist/assets/{useEffectiveDirectory-e9HjIqGz.js → useEffectiveDirectory-Ji8AKgzY.js} +1 -1
- package/dist/assets/{useFontPreferences-CB-cGo3I.js → useFontPreferences-DCaY8NbU.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BYdpuwL7.js +413 -0
- package/dist/assets/useSessionAutoCleanup-CADZCvZL.js +2 -0
- package/dist/assets/{useWorkerHighlightedLines-DTGE0CDs.js → useWorkerHighlightedLines-CuG88Lsj.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-C6OFKjeT.js} +2 -2
- package/dist/assets/{zh-TW-BsRk1Rbb.js → zh-TW-BVP99aMr.js} +2 -2
- 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 +120 -22
- package/server/lib/assistants/assign.js +117 -12
- package/server/lib/assistants/assign.test.js +79 -0
- package/server/lib/assistants/bubbles.js +1 -1
- package/server/lib/assistants/contact-store.js +520 -11
- package/server/lib/assistants/contact-store.test.js +362 -12
- package/server/lib/assistants/contact-tools.js +305 -54
- package/server/lib/assistants/contact-tools.test.js +239 -7
- package/server/lib/assistants/contracts.js +31 -0
- package/server/lib/assistants/harness.js +295 -132
- package/server/lib/assistants/harness.test.js +662 -10
- package/server/lib/assistants/pi-tools.js +2 -0
- package/server/lib/assistants/pi-tools.test.js +8 -0
- package/server/lib/assistants/routes.js +3 -0
- package/server/lib/assistants/service.js +861 -79
- package/server/lib/assistants/service.test.js +1234 -8
- package/server/lib/llm/DOCUMENTATION.md +10 -3
- package/server/lib/llm/completions.js +4 -0
- package/server/lib/llm/completions.test.js +3 -0
- package/server/lib/llm/generate.js +57 -11
- package/server/lib/llm/generate.test.js +92 -0
- package/server/lib/opencode/feature-routes-runtime.js +5 -0
- package/dist/assets/AssistantView-CKmIS1cS.js +0 -2
- package/dist/assets/Icon-CkzzhaLr.js +0 -1
- package/dist/assets/MarkdownRenderer-CLQ4Z8Z0.js +0 -2
- package/dist/assets/MobileDetailNavigation-BGaAyuBH.js +0 -1
- package/dist/assets/MobileShareBridge-BAZoyeIP.js +0 -4
- package/dist/assets/MobileSurface-BVb3xNWt.js +0 -2
- package/dist/assets/SettingsView-CkQ61pbr.js +0 -25
- package/dist/assets/insertionBoundaries-BMhzPFft.js +0 -4
- package/dist/assets/main-BjeOhdcG.js +0 -169
- package/dist/assets/miniChat-BYrjYEXJ.js +0 -2
- package/dist/assets/mobile-BVMNnPK2.js +0 -2
- package/dist/assets/platform-DzsGqlGW.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-COwia8Cs.js +0 -2
- package/dist/assets/renderMobileApp-CXwwpY1t.js +0 -28
- package/dist/assets/runtimeEndpointReset-BkAQXmXJ.js +0 -147
- package/dist/assets/sessionChangeSummary-DQDZ5HW-.js +0 -1
- package/dist/assets/sessionLookup-Imc995rW.js +0 -1
- package/dist/assets/src-AA-5nXsF.css +0 -2
- package/dist/assets/useDesktopWindowControlsLayout-C5hZD1co.js +0 -1
- package/dist/assets/useMobileNavigationStore-CYbDxRTM.js +0 -413
- package/dist/assets/useSessionAutoCleanup-BGy9_AhN.js +0 -2
|
@@ -13,9 +13,13 @@ import {
|
|
|
13
13
|
NEW_CONVERSATION_CONFIRM_BUBBLE,
|
|
14
14
|
NEW_CONVERSATION_TOOL_NAME,
|
|
15
15
|
SCHEDULE_TASK_TOOL_NAME,
|
|
16
|
+
STOP_SESSION_TOOL_NAME,
|
|
17
|
+
STOPPED_SESSION_FALLBACK_BUBBLE,
|
|
16
18
|
UPDATE_DEFAULT_PROMPT_CONFIRM_BUBBLE,
|
|
17
19
|
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
18
20
|
UPDATE_DEFAULT_PROMPT_UNCHANGED_BUBBLE,
|
|
21
|
+
WATCH_SESSION_TOOL_NAME,
|
|
22
|
+
WATCHED_SESSION_FALLBACK_BUBBLE,
|
|
19
23
|
confirmBubbleAfterContactReset,
|
|
20
24
|
contactTurnHasSuccessfulReset,
|
|
21
25
|
createContactTools,
|
|
@@ -35,11 +39,45 @@ import {
|
|
|
35
39
|
import { attachmentScopeKey } from './assign.js';
|
|
36
40
|
|
|
37
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
|
+
|
|
38
76
|
it('parses an assign_session fence and strips it from chat text', () => {
|
|
39
77
|
const text = 'On it.\n\n```openchamber-tool\n{"name":"assign_session","arguments":{"prompt":"Fix login","projectPath":"/repo"}}\n```';
|
|
40
78
|
const parsed = parseContactToolCalls(text, [ASSIGN_SESSION_TOOL_NAME]);
|
|
41
79
|
expect(parsed.chatText).toBe('On it.');
|
|
42
|
-
expect(parsed.
|
|
80
|
+
expect(parsed.toolCalls[0]).toEqual({
|
|
43
81
|
name: ASSIGN_SESSION_TOOL_NAME,
|
|
44
82
|
arguments: { prompt: 'Fix login', projectPath: '/repo' },
|
|
45
83
|
});
|
|
@@ -52,7 +90,7 @@ describe('contact tool protocol', () => {
|
|
|
52
90
|
[CREATE_ASSISTANT_TOOL_NAME, SCHEDULE_TASK_TOOL_NAME, ASSIGN_SESSION_TOOL_NAME],
|
|
53
91
|
);
|
|
54
92
|
expect(create.chatText).toBe('好。');
|
|
55
|
-
expect(create.
|
|
93
|
+
expect(create.toolCalls[0]).toEqual({
|
|
56
94
|
name: CREATE_ASSISTANT_TOOL_NAME,
|
|
57
95
|
arguments: { name: 'FlowQA', model: 'opencode-go/deepseek-v4-flash' },
|
|
58
96
|
});
|
|
@@ -64,11 +102,11 @@ describe('contact tool protocol', () => {
|
|
|
64
102
|
'```openchamber-tool\n{"name":"message_assistant","arguments":{"to":"PeerQA","text":"hello-from-assistant 写好了"}}\n```',
|
|
65
103
|
[MESSAGE_ASSISTANT_TOOL_NAME],
|
|
66
104
|
);
|
|
67
|
-
expect(peer.
|
|
105
|
+
expect(peer.toolCalls[0]).toEqual({
|
|
68
106
|
name: MESSAGE_ASSISTANT_TOOL_NAME,
|
|
69
107
|
arguments: { to: 'PeerQA', text: 'hello-from-assistant 写好了' },
|
|
70
108
|
});
|
|
71
|
-
expect(schedule.
|
|
109
|
+
expect(schedule.toolCalls[0]).toEqual({
|
|
72
110
|
name: SCHEDULE_TASK_TOOL_NAME,
|
|
73
111
|
arguments: { name: 'Daily ping', prompt: 'ping', time: '18:00', timezone: 'Asia/Shanghai' },
|
|
74
112
|
});
|
|
@@ -79,7 +117,7 @@ describe('contact tool protocol', () => {
|
|
|
79
117
|
'好的,我来直接创建这个助理,不开编码会话。\n{"name":"create_assistant","arguments":{"name":"FlowNL","model":"opencode-go/deepseek-v4-flash"}}',
|
|
80
118
|
[CREATE_ASSISTANT_TOOL_NAME, ASSIGN_SESSION_TOOL_NAME],
|
|
81
119
|
);
|
|
82
|
-
expect(parsed.
|
|
120
|
+
expect(parsed.toolCalls[0]).toEqual({
|
|
83
121
|
name: CREATE_ASSISTANT_TOOL_NAME,
|
|
84
122
|
arguments: { name: 'FlowNL', model: 'opencode-go/deepseek-v4-flash' },
|
|
85
123
|
});
|
|
@@ -87,6 +125,94 @@ describe('contact tool protocol', () => {
|
|
|
87
125
|
expect(parsed.chatText).not.toContain('create_assistant');
|
|
88
126
|
});
|
|
89
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
|
+
|
|
90
216
|
it('detects 建助理 without treating 不要开编码 session as assign_session', () => {
|
|
91
217
|
const tools = [
|
|
92
218
|
NEW_CONVERSATION_TOOL_NAME,
|
|
@@ -98,6 +224,8 @@ describe('contact tool protocol', () => {
|
|
|
98
224
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
99
225
|
SCHEDULE_TASK_TOOL_NAME,
|
|
100
226
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
227
|
+
WATCH_SESSION_TOOL_NAME,
|
|
228
|
+
STOP_SESSION_TOOL_NAME,
|
|
101
229
|
ASSIGN_SESSION_TOOL_NAME,
|
|
102
230
|
];
|
|
103
231
|
expect(detectRequestedContactTools('帮我新建一个助理,名叫 FlowNL,不要开编码 session', tools)).toEqual([
|
|
@@ -106,6 +234,14 @@ describe('contact tool protocol', () => {
|
|
|
106
234
|
expect(detectRequestedContactTools('每天 18:00 排一个 ping 定时任务', tools)).toEqual([SCHEDULE_TASK_TOOL_NAME]);
|
|
107
235
|
expect(detectRequestedContactTools('建会话写一个文件', tools)).toEqual([ASSIGN_SESSION_TOOL_NAME]);
|
|
108
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([]);
|
|
109
245
|
expect(detectRequestedContactTools('写一个文件', tools)).toEqual([]);
|
|
110
246
|
expect(detectRequestedContactTools('pwd', tools)).toEqual([]);
|
|
111
247
|
expect(detectRequestedContactTools('给 PeerQA 说一声 hello-from-assistant 写好了', tools)).toEqual([
|
|
@@ -141,14 +277,14 @@ describe('contact tool protocol', () => {
|
|
|
141
277
|
|
|
142
278
|
it('parses bash fences when the pi coding tools are allowed', () => {
|
|
143
279
|
const text = '```openchamber-tool\n{"name":"bash","arguments":{"command":"pwd"}}\n```';
|
|
144
|
-
expect(parseContactToolCalls(text, ['bash', ASSIGN_SESSION_TOOL_NAME]).
|
|
280
|
+
expect(parseContactToolCalls(text, ['bash', ASSIGN_SESSION_TOOL_NAME]).toolCalls[0]).toEqual({
|
|
145
281
|
name: 'bash',
|
|
146
282
|
arguments: { command: 'pwd' },
|
|
147
283
|
});
|
|
148
284
|
expect(parseContactToolCalls(
|
|
149
285
|
'```openchamber-tool\n{"name":"glob","arguments":{"pattern":"*"}}\n```',
|
|
150
286
|
['glob', 'bash'],
|
|
151
|
-
).
|
|
287
|
+
).toolCalls).toEqual([]);
|
|
152
288
|
});
|
|
153
289
|
|
|
154
290
|
it('tells DeepSeek to call tools from natural language, not slash commands', () => {
|
|
@@ -163,6 +299,8 @@ describe('contact tool protocol', () => {
|
|
|
163
299
|
expect(prompt).toContain('schedule_task');
|
|
164
300
|
expect(prompt).toContain('message_assistant');
|
|
165
301
|
expect(prompt).toContain('assign_session');
|
|
302
|
+
expect(prompt).toContain('watch_session');
|
|
303
|
+
expect(prompt).toContain('stop_session');
|
|
166
304
|
expect(prompt).toContain('开新对话');
|
|
167
305
|
expect(prompt).toContain('清除记忆');
|
|
168
306
|
expect(prompt).toContain('清空聊天记录');
|
|
@@ -173,6 +311,9 @@ describe('contact tool protocol', () => {
|
|
|
173
311
|
expect(prompt).toContain('建助理');
|
|
174
312
|
expect(prompt).toContain('排定时任务');
|
|
175
313
|
expect(prompt).toContain('说一声');
|
|
314
|
+
expect(prompt).toContain('监听会话');
|
|
315
|
+
expect(prompt).toContain('停止会话');
|
|
316
|
+
expect(prompt).toContain('plain @session:id reference alone is context');
|
|
176
317
|
expect(prompt).toContain('session/new');
|
|
177
318
|
expect(prompt).toContain('LLM memory only');
|
|
178
319
|
expect(prompt).toContain('Persists');
|
|
@@ -290,12 +431,18 @@ describe('createContactTools', () => {
|
|
|
290
431
|
CLEAR_CHAT_HISTORY_TOOL_NAME,
|
|
291
432
|
LIST_PROJECTS_TOOL_NAME,
|
|
292
433
|
LIST_SESSIONS_TOOL_NAME,
|
|
434
|
+
'read_session',
|
|
293
435
|
GET_ASSISTANT_SETTINGS_TOOL_NAME,
|
|
294
436
|
UPDATE_DEFAULT_PROMPT_TOOL_NAME,
|
|
295
437
|
CREATE_ASSISTANT_TOOL_NAME,
|
|
296
438
|
SCHEDULE_TASK_TOOL_NAME,
|
|
297
439
|
MESSAGE_ASSISTANT_TOOL_NAME,
|
|
298
440
|
ASSIGN_SESSION_TOOL_NAME,
|
|
441
|
+
WATCH_SESSION_TOOL_NAME,
|
|
442
|
+
STOP_SESSION_TOOL_NAME,
|
|
443
|
+
'steer_session',
|
|
444
|
+
'archive_session',
|
|
445
|
+
'delete_session',
|
|
299
446
|
]);
|
|
300
447
|
expect(tools.some((tool) => ['bash', 'edit', 'read', 'write'].includes(tool.name))).toBe(false);
|
|
301
448
|
|
|
@@ -374,6 +521,91 @@ describe('createContactTools', () => {
|
|
|
374
521
|
expect(onCard).toHaveBeenCalledTimes(3);
|
|
375
522
|
});
|
|
376
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
|
+
|
|
377
609
|
it('forwards server turn file parts on assign and keeps model selection in the tool args', async () => {
|
|
378
610
|
const image = { type: 'file', mime: 'image/png', url: 'data:image/png;base64,aa', filename: 'shot.png' };
|
|
379
611
|
const assignWork = vi.fn(async (params) => ({
|
|
@@ -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 }),
|