@theia/ai-chat 1.72.0-next.59 → 1.72.0
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/lib/browser/chat-session-store-impl.d.ts.map +1 -1
- package/lib/browser/chat-session-store-impl.js +7 -2
- package/lib/browser/chat-session-store-impl.js.map +1 -1
- package/lib/common/chat-content-deserializer.d.ts.map +1 -1
- package/lib/common/chat-content-deserializer.js +5 -6
- package/lib/common/chat-content-deserializer.js.map +1 -1
- package/lib/common/chat-content-deserializer.spec.js +12 -1
- package/lib/common/chat-content-deserializer.spec.js.map +1 -1
- package/lib/common/chat-model.d.ts +23 -1
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +23 -9
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-response-model.spec.js +15 -0
- package/lib/common/chat-response-model.spec.js.map +1 -1
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +4 -2
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-session-store.d.ts +8 -0
- package/lib/common/chat-session-store.d.ts.map +1 -1
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/provider-error-formatter.d.ts +16 -0
- package/lib/common/provider-error-formatter.d.ts.map +1 -0
- package/lib/common/provider-error-formatter.js +111 -0
- package/lib/common/provider-error-formatter.js.map +1 -0
- package/lib/common/provider-error-formatter.spec.d.ts +2 -0
- package/lib/common/provider-error-formatter.spec.d.ts.map +1 -0
- package/lib/common/provider-error-formatter.spec.js +167 -0
- package/lib/common/provider-error-formatter.spec.js.map +1 -0
- package/lib/common/tool-call-response-content.spec.js +17 -0
- package/lib/common/tool-call-response-content.spec.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/chat-session-store-impl.ts +7 -2
- package/src/common/chat-content-deserializer.spec.ts +16 -1
- package/src/common/chat-content-deserializer.ts +7 -6
- package/src/common/chat-model.ts +53 -11
- package/src/common/chat-response-model.spec.ts +20 -1
- package/src/common/chat-service.ts +4 -2
- package/src/common/chat-session-store.ts +8 -0
- package/src/common/index.ts +1 -0
- package/src/common/provider-error-formatter.spec.ts +190 -0
- package/src/common/provider-error-formatter.ts +110 -0
- package/src/common/tool-call-response-content.spec.ts +23 -0
|
@@ -15,9 +15,28 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { expect } from 'chai';
|
|
18
|
-
import { MutableChatResponseModel } from './chat-model';
|
|
18
|
+
import { MutableChatResponseModel, ToolCallChatResponseContentImpl } from './chat-model';
|
|
19
19
|
|
|
20
20
|
describe('MutableChatResponseModel', () => {
|
|
21
|
+
describe('content change propagation', () => {
|
|
22
|
+
it('should fire onDidChange when a tool call\'s result is updated after it was added', () => {
|
|
23
|
+
const response = new MutableChatResponseModel('req-1');
|
|
24
|
+
const toolCall = new ToolCallChatResponseContentImpl('tool-1', 'tool', '{}', false);
|
|
25
|
+
response.response.addContent(toolCall);
|
|
26
|
+
|
|
27
|
+
let fireCount = 0;
|
|
28
|
+
response.onDidChange(() => { fireCount++; });
|
|
29
|
+
|
|
30
|
+
toolCall.updateResult('partial');
|
|
31
|
+
|
|
32
|
+
// The response model must observe the change so auto-save can persist
|
|
33
|
+
// intermediate state. Without this propagation, mutations that don't go
|
|
34
|
+
// through addContent/merge (e.g. renderer-side partial results) would be
|
|
35
|
+
// lost on reload.
|
|
36
|
+
expect(fireCount).to.equal(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
21
40
|
describe('setTokenUsage', () => {
|
|
22
41
|
it('should also add a token usage entry', () => {
|
|
23
42
|
const response = new MutableChatResponseModel('req-1');
|
|
@@ -455,9 +455,11 @@ export class ChatServiceImpl implements ChatService {
|
|
|
455
455
|
return Promise.resolve();
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
// Store session with title
|
|
458
|
+
// Store session with title, pinned agent info, last interaction timestamp, and error state
|
|
459
|
+
const lastRequest = session.model.getRequests().at(-1);
|
|
460
|
+
const hasError = lastRequest?.response.isComplete === true && lastRequest?.response.isError === true;
|
|
459
461
|
return this.sessionStore.storeSessions(
|
|
460
|
-
{ model: session.model, title: session.title, pinnedAgentId: session.pinnedAgent?.id }
|
|
462
|
+
{ model: session.model, title: session.title, pinnedAgentId: session.pinnedAgent?.id, lastInteraction: session.lastInteraction?.getTime(), hasError }
|
|
461
463
|
).catch(error => {
|
|
462
464
|
this.logger.error('Failed to store chat sessions', error);
|
|
463
465
|
});
|
|
@@ -24,6 +24,10 @@ export interface ChatModelWithMetadata {
|
|
|
24
24
|
model: ChatModel;
|
|
25
25
|
title?: string;
|
|
26
26
|
pinnedAgentId?: string;
|
|
27
|
+
/** Timestamp (ms since epoch) of the last user interaction. Used as the display date for session cards. */
|
|
28
|
+
lastInteraction?: number;
|
|
29
|
+
/** Whether the last request ended with an error. */
|
|
30
|
+
hasError?: boolean;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export interface ChatSessionStore {
|
|
@@ -67,4 +71,8 @@ export interface ChatSessionMetadata {
|
|
|
67
71
|
title: string;
|
|
68
72
|
saveDate: number;
|
|
69
73
|
location: ChatAgentLocation;
|
|
74
|
+
/** ID of the agent pinned to the session (used for icon and display name on cards). */
|
|
75
|
+
pinnedAgentId?: string;
|
|
76
|
+
/** Whether the last request ended with an error. */
|
|
77
|
+
hasError?: boolean;
|
|
70
78
|
}
|
package/src/common/index.ts
CHANGED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { formatProviderError, formattedProviderErrorToShortString } from './provider-error-formatter';
|
|
19
|
+
|
|
20
|
+
describe('formatProviderError', () => {
|
|
21
|
+
|
|
22
|
+
it('should handle undefined input', () => {
|
|
23
|
+
const result = formatProviderError(undefined);
|
|
24
|
+
expect(result.message).to.equal('');
|
|
25
|
+
expect(result.status).to.be.undefined;
|
|
26
|
+
expect(result.details).to.be.undefined;
|
|
27
|
+
expect(result.raw).to.equal('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle empty string input', () => {
|
|
31
|
+
const result = formatProviderError('');
|
|
32
|
+
expect(result.message).to.equal('');
|
|
33
|
+
expect(result.status).to.be.undefined;
|
|
34
|
+
expect(result.details).to.be.undefined;
|
|
35
|
+
expect(result.raw).to.equal('');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle plain text error without JSON', () => {
|
|
39
|
+
const result = formatProviderError('Something went wrong');
|
|
40
|
+
expect(result.message).to.equal('Something went wrong');
|
|
41
|
+
expect(result.status).to.be.undefined;
|
|
42
|
+
expect(result.details).to.be.undefined;
|
|
43
|
+
expect(result.raw).to.equal('Something went wrong');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should extract HTTP status from leading prefix', () => {
|
|
47
|
+
const raw = '401 {"error":{"message":"Invalid API key","type":"auth_error"}}';
|
|
48
|
+
const result = formatProviderError(raw);
|
|
49
|
+
expect(result.status).to.equal('401');
|
|
50
|
+
expect(result.message).to.equal('Invalid API key');
|
|
51
|
+
expect(result.details).to.be.a('string');
|
|
52
|
+
expect(result.raw).to.equal(raw);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should extract status from JSON code field when no leading prefix', () => {
|
|
56
|
+
const raw = '{"error":{"message":"Permission denied","code":403}}';
|
|
57
|
+
const result = formatProviderError(raw);
|
|
58
|
+
expect(result.status).to.equal('403');
|
|
59
|
+
expect(result.message).to.equal('Permission denied');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should extract status from JSON status field', () => {
|
|
63
|
+
const raw = '{"error":{"message":"Not found","status":404}}';
|
|
64
|
+
const result = formatProviderError(raw);
|
|
65
|
+
expect(result.status).to.equal('404');
|
|
66
|
+
expect(result.message).to.equal('Not found');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should prefer leading HTTP status over JSON code field', () => {
|
|
70
|
+
const raw = '500 {"error":{"message":"Server error","code":503}}';
|
|
71
|
+
const result = formatProviderError(raw);
|
|
72
|
+
expect(result.status).to.equal('500');
|
|
73
|
+
expect(result.message).to.equal('Server error');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should extract the deepest message from nested JSON', () => {
|
|
77
|
+
const raw = '400 {"error":{"message":"Top level","details":{"message":"Deeper reason"}}}';
|
|
78
|
+
const result = formatProviderError(raw);
|
|
79
|
+
expect(result.status).to.equal('400');
|
|
80
|
+
expect(result.message).to.equal('Deeper reason');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle Gemini-style errors with JSON-encoded inner body', () => {
|
|
84
|
+
const inner = JSON.stringify({ error: { message: 'API key not valid', status: 'INVALID_ARGUMENT' } });
|
|
85
|
+
const raw = `{"error":{"message":${JSON.stringify(inner)},"code":400}}`;
|
|
86
|
+
const result = formatProviderError(raw);
|
|
87
|
+
expect(result.status).to.equal('400');
|
|
88
|
+
expect(result.message).to.equal('API key not valid');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should fall back to remainder when JSON has no message field', () => {
|
|
92
|
+
const raw = '429 {"error":{"type":"rate_limit"}}';
|
|
93
|
+
const result = formatProviderError(raw);
|
|
94
|
+
expect(result.status).to.equal('429');
|
|
95
|
+
expect(result.message).to.equal('{"error":{"type":"rate_limit"}}');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should not treat a leading 3-digit prefix as a status when no JSON body follows', () => {
|
|
99
|
+
// Avoid misreading arbitrary text that happens to start with 3 digits.
|
|
100
|
+
const raw = '404 routes were processed';
|
|
101
|
+
const result = formatProviderError(raw);
|
|
102
|
+
expect(result.status).to.be.undefined;
|
|
103
|
+
expect(result.message).to.equal('404 routes were processed');
|
|
104
|
+
expect(result.details).to.be.undefined;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should produce unwrapped details for nested JSON strings', () => {
|
|
108
|
+
const inner = JSON.stringify({ reason: 'quota exceeded' });
|
|
109
|
+
const raw = `{"error":{"message":"Rate limited","body":${JSON.stringify(inner)}}}`;
|
|
110
|
+
const result = formatProviderError(raw);
|
|
111
|
+
expect(result.details).to.be.a('string');
|
|
112
|
+
// The details should contain the unwrapped inner object, not the escaped JSON string
|
|
113
|
+
const parsed = JSON.parse(result.details!);
|
|
114
|
+
expect(parsed.error.body).to.deep.equal({ reason: 'quota exceeded' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should not treat a message that looks like JSON as the headline', () => {
|
|
118
|
+
const raw = '{"error":{"message":"{\\"key\\":\\"value\\"}","code":400}}';
|
|
119
|
+
const result = formatProviderError(raw);
|
|
120
|
+
// The JSON-like message should be skipped; headline falls back to remainder
|
|
121
|
+
expect(result.message).to.equal('{"error":{"message":"{\\"key\\":\\"value\\"}","code":400}}');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle leading status with colon separator', () => {
|
|
125
|
+
const raw = '401: {"error":{"message":"Unauthorized"}}';
|
|
126
|
+
const result = formatProviderError(raw);
|
|
127
|
+
expect(result.status).to.equal('401');
|
|
128
|
+
expect(result.message).to.equal('Unauthorized');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should extract message from an array of errors', () => {
|
|
132
|
+
const raw = '{"errors":[{"message":"Rate limit exceeded","code":429}]}';
|
|
133
|
+
const result = formatProviderError(raw);
|
|
134
|
+
expect(result.status).to.equal('429');
|
|
135
|
+
expect(result.message).to.equal('Rate limit exceeded');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should ignore non-3-digit numeric codes', () => {
|
|
139
|
+
const raw = '{"error":{"message":"Something broke","code":42}}';
|
|
140
|
+
const result = formatProviderError(raw);
|
|
141
|
+
expect(result.status).to.be.undefined;
|
|
142
|
+
expect(result.message).to.equal('Something broke');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should ignore non-numeric status strings', () => {
|
|
146
|
+
const raw = '{"error":{"message":"Invalid key","status":"INVALID_ARGUMENT"}}';
|
|
147
|
+
const result = formatProviderError(raw);
|
|
148
|
+
expect(result.status).to.be.undefined;
|
|
149
|
+
expect(result.message).to.equal('Invalid key');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should find JSON preceded by non-JSON text', () => {
|
|
153
|
+
const raw = 'Error occurred: {"error":{"message":"connection refused"}}';
|
|
154
|
+
const result = formatProviderError(raw);
|
|
155
|
+
expect(result.message).to.equal('connection refused');
|
|
156
|
+
expect(result.details).to.be.a('string');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should handle whitespace-only input', () => {
|
|
160
|
+
const result = formatProviderError(' ');
|
|
161
|
+
expect(result.message).to.equal(' ');
|
|
162
|
+
expect(result.status).to.be.undefined;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should walk into a top-level JSON array', () => {
|
|
166
|
+
const raw = '[{"error":{"message":"first"}},{"error":{"message":"second"}}]';
|
|
167
|
+
const result = formatProviderError(raw);
|
|
168
|
+
// walk visits in order, last message wins
|
|
169
|
+
expect(result.message).to.equal('second');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should extract 3-digit status from a longer string value', () => {
|
|
173
|
+
const raw = '{"error":{"message":"Too many requests","status":"429 RESOURCE_EXHAUSTED"}}';
|
|
174
|
+
const result = formatProviderError(raw);
|
|
175
|
+
expect(result.status).to.equal('429');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('formattedProviderErrorToShortString', () => {
|
|
180
|
+
|
|
181
|
+
it('should include status when present', () => {
|
|
182
|
+
const result = formattedProviderErrorToShortString({ status: '401', message: 'Invalid API key', raw: '' });
|
|
183
|
+
expect(result).to.equal('401: Invalid API key');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return only headline when no status', () => {
|
|
187
|
+
const result = formattedProviderErrorToShortString({ message: 'Something went wrong', raw: '' });
|
|
188
|
+
expect(result).to.equal('Something went wrong');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Structured form of a provider error string. Provider errors look something
|
|
19
|
+
* like `<httpStatus> <jsonBody>` (Anthropic, OpenAI) or `<jsonBody>` (Gemini).
|
|
20
|
+
* This helper makes a best-effort guess: walk the JSON, grab the deepest
|
|
21
|
+
* `message` string and the first 3-digit `code`/`status` field.
|
|
22
|
+
*/
|
|
23
|
+
export interface FormattedProviderError {
|
|
24
|
+
status?: string;
|
|
25
|
+
message: string;
|
|
26
|
+
details?: string;
|
|
27
|
+
raw: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatProviderError(raw: string | undefined): FormattedProviderError {
|
|
31
|
+
const safeRaw = (raw ?? '').toString();
|
|
32
|
+
const trimmed = safeRaw.trim();
|
|
33
|
+
if (!trimmed) { return { message: safeRaw, raw: safeRaw }; }
|
|
34
|
+
|
|
35
|
+
// Optional leading HTTP status followed by a JSON body, e.g. "401 {...}".
|
|
36
|
+
// The JSON-body anchor avoids misreading arbitrary text that happens to
|
|
37
|
+
// start with 3 digits (e.g. "404 routes were processed") as a status.
|
|
38
|
+
// Real provider errors (Anthropic, OpenAI) always carry a JSON body here.
|
|
39
|
+
const match = trimmed.match(/^(\d{3})\b[\s:-]*(?=[{[])/);
|
|
40
|
+
const remainder = match ? trimmed.slice(match[0].length).trim() : trimmed;
|
|
41
|
+
|
|
42
|
+
const body = parseJson(remainder);
|
|
43
|
+
if (!body || typeof body !== 'object') {
|
|
44
|
+
return { status: match?.[1], message: remainder || safeRaw, raw: safeRaw };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let message: string | undefined;
|
|
48
|
+
let code: string | undefined;
|
|
49
|
+
const walk = (node: unknown): void => {
|
|
50
|
+
if (typeof node === 'string') {
|
|
51
|
+
const inner = parseJson(node);
|
|
52
|
+
if (inner && typeof inner === 'object') { walk(inner); }
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!node || typeof node !== 'object') { return; }
|
|
56
|
+
for (const [k, v] of Object.entries(node)) {
|
|
57
|
+
if (k === 'message' && typeof v === 'string' && !looksLikeJson(v)) {
|
|
58
|
+
message = v;
|
|
59
|
+
} else if (!code && (k === 'code' || k === 'status')) {
|
|
60
|
+
code = asStatus(v);
|
|
61
|
+
}
|
|
62
|
+
walk(v);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
walk(body);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
status: match?.[1] ?? code,
|
|
69
|
+
message: message ?? remainder,
|
|
70
|
+
details: stringifyUnwrapped(body),
|
|
71
|
+
raw: safeRaw
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Compact one-liner suitable for notification toasts. */
|
|
76
|
+
export function formattedProviderErrorToShortString(e: FormattedProviderError): string {
|
|
77
|
+
return e.status ? `${e.status}: ${e.message}` : e.message;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseJson(text: string): unknown {
|
|
81
|
+
const start = text.search(/[{[]/);
|
|
82
|
+
if (start < 0) { return undefined; }
|
|
83
|
+
try { return JSON.parse(text.slice(start)); } catch { return undefined; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function looksLikeJson(text: string): boolean {
|
|
87
|
+
const t = text.trimStart();
|
|
88
|
+
return t.startsWith('{') || t.startsWith('[');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function asStatus(value: unknown): string | undefined {
|
|
92
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
93
|
+
const s = String(value);
|
|
94
|
+
return /^\d{3}$/.test(s) ? s : undefined;
|
|
95
|
+
}
|
|
96
|
+
return typeof value === 'string' ? value.match(/^(\d{3})\b/)?.[1] : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Pretty-print JSON, transparently parsing any string field that itself contains JSON. */
|
|
100
|
+
function stringifyUnwrapped(value: unknown): string | undefined {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.stringify(value, (_, v) => {
|
|
103
|
+
if (typeof v === 'string' && looksLikeJson(v)) {
|
|
104
|
+
const inner = parseJson(v);
|
|
105
|
+
if (inner && typeof inner === 'object') { return inner; }
|
|
106
|
+
}
|
|
107
|
+
return v;
|
|
108
|
+
}, 2);
|
|
109
|
+
} catch { return undefined; }
|
|
110
|
+
}
|
|
@@ -145,6 +145,29 @@ describe('ToolCallChatResponseContentImpl', () => {
|
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
describe('updateResult', () => {
|
|
149
|
+
it('should set the result without marking the tool finished', () => {
|
|
150
|
+
const toolCall = new ToolCallChatResponseContentImpl('id', 'tool', '{}', false);
|
|
151
|
+
|
|
152
|
+
toolCall.updateResult('partial');
|
|
153
|
+
|
|
154
|
+
expect(toolCall.result).to.equal('partial');
|
|
155
|
+
expect(toolCall.finished).to.be.false;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should fire onDidChange so auto-save picks up the partial result', () => {
|
|
159
|
+
const toolCall = new ToolCallChatResponseContentImpl('id', 'tool', '{}', false);
|
|
160
|
+
let fireCount = 0;
|
|
161
|
+
toolCall.onDidChange(() => { fireCount++; });
|
|
162
|
+
|
|
163
|
+
toolCall.updateResult('partial-1');
|
|
164
|
+
toolCall.updateResult('partial-2');
|
|
165
|
+
|
|
166
|
+
expect(fireCount).to.equal(2);
|
|
167
|
+
expect(toolCall.result).to.equal('partial-2');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
148
171
|
describe('restored tool calls', () => {
|
|
149
172
|
it('should have finished=true when restored with a result', () => {
|
|
150
173
|
const restoredToolCall = new ToolCallChatResponseContentImpl(
|