@theia/ai-chat 1.60.0 → 1.61.0-next.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/lib/common/chat-agents.d.ts.map +1 -1
  2. package/lib/common/chat-agents.js +27 -20
  3. package/lib/common/chat-agents.js.map +1 -1
  4. package/lib/common/chat-model.d.ts +18 -0
  5. package/lib/common/chat-model.d.ts.map +1 -1
  6. package/lib/common/chat-model.js +46 -1
  7. package/lib/common/chat-model.js.map +1 -1
  8. package/lib/common/chat-session-naming-service.d.ts.map +1 -1
  9. package/lib/common/chat-session-naming-service.js +7 -9
  10. package/lib/common/chat-session-naming-service.js.map +1 -1
  11. package/lib/common/parse-contents-with-incomplete-parts.spec.d.ts +2 -0
  12. package/lib/common/parse-contents-with-incomplete-parts.spec.d.ts.map +1 -0
  13. package/lib/common/parse-contents-with-incomplete-parts.spec.js +103 -0
  14. package/lib/common/parse-contents-with-incomplete-parts.spec.js.map +1 -0
  15. package/lib/common/parse-contents.d.ts +1 -0
  16. package/lib/common/parse-contents.d.ts.map +1 -1
  17. package/lib/common/parse-contents.js +45 -5
  18. package/lib/common/parse-contents.js.map +1 -1
  19. package/lib/common/parse-contents.spec.d.ts +1 -0
  20. package/lib/common/parse-contents.spec.d.ts.map +1 -1
  21. package/lib/common/parse-contents.spec.js +25 -13
  22. package/lib/common/parse-contents.spec.js.map +1 -1
  23. package/lib/common/response-content-matcher.d.ts +6 -0
  24. package/lib/common/response-content-matcher.d.ts.map +1 -1
  25. package/lib/common/response-content-matcher.js +14 -3
  26. package/lib/common/response-content-matcher.js.map +1 -1
  27. package/package.json +10 -10
  28. package/src/common/chat-agents.ts +32 -21
  29. package/src/common/chat-model.ts +60 -0
  30. package/src/common/chat-session-naming-service.ts +10 -13
  31. package/src/common/parse-contents-with-incomplete-parts.spec.ts +114 -0
  32. package/src/common/parse-contents.spec.ts +24 -12
  33. package/src/common/parse-contents.ts +52 -6
  34. package/src/common/response-content-matcher.ts +21 -3
@@ -278,6 +278,12 @@ export interface ThinkingChatResponseContent
278
278
  signature: string;
279
279
  }
280
280
 
281
+ export interface ProgressChatResponseContent
282
+ extends Required<ChatResponseContent> {
283
+ kind: 'progress';
284
+ message: string;
285
+ }
286
+
281
287
  export interface Location {
282
288
  uri: URI;
283
289
  position: Position;
@@ -414,6 +420,17 @@ export namespace ThinkingChatResponseContent {
414
420
  }
415
421
  }
416
422
 
423
+ export namespace ProgressChatResponseContent {
424
+ export function is(obj: unknown): obj is ProgressChatResponseContent {
425
+ return (
426
+ ChatResponseContent.is(obj) &&
427
+ obj.kind === 'progress' &&
428
+ 'message' in obj &&
429
+ typeof obj.message === 'string'
430
+ );
431
+ }
432
+ }
433
+
417
434
  export type QuestionResponseHandler = (
418
435
  selectedOption: { text: string, value?: string },
419
436
  ) => void;
@@ -1127,6 +1144,12 @@ class ChatResponseImpl implements ChatResponse {
1127
1144
  return this._content;
1128
1145
  }
1129
1146
 
1147
+ clearContent(): void {
1148
+ this._content = [];
1149
+ this._updateResponseRepresentation();
1150
+ this._onDidChangeEmitter.fire();
1151
+ }
1152
+
1130
1153
  addContents(contents: ChatResponseContent[]): void {
1131
1154
  contents.forEach(c => this.doAddContent(c));
1132
1155
  this._onDidChangeEmitter.fire();
@@ -1353,3 +1376,40 @@ export class ErrorChatResponseModel extends MutableChatResponseModel {
1353
1376
  this.error(error);
1354
1377
  }
1355
1378
  }
1379
+
1380
+ export class ProgressChatResponseContentImpl implements ProgressChatResponseContent {
1381
+ readonly kind = 'progress';
1382
+ protected _message: string;
1383
+
1384
+ constructor(message: string) {
1385
+ this._message = message;
1386
+ }
1387
+
1388
+ get message(): string {
1389
+ return this._message;
1390
+ }
1391
+
1392
+ asString(): string {
1393
+ return JSON.stringify({
1394
+ type: 'progress',
1395
+ message: this.message
1396
+ });
1397
+ }
1398
+
1399
+ asDisplayString(): string | undefined {
1400
+ return `<Progress>${this.message}</Progress>`;
1401
+ }
1402
+
1403
+ merge(nextChatResponseContent: ProgressChatResponseContent): boolean {
1404
+ this._message = nextChatResponseContent.message;
1405
+ return true;
1406
+ }
1407
+
1408
+ toLanguageModelMessage(): TextMessage {
1409
+ return {
1410
+ actor: 'ai',
1411
+ type: 'text',
1412
+ text: this.message
1413
+ };
1414
+ }
1415
+ }
@@ -20,10 +20,10 @@ import {
20
20
  CommunicationRecordingService,
21
21
  getTextOfResponse,
22
22
  LanguageModelRegistry,
23
- LanguageModelRequest,
24
23
  LanguageModelRequirement,
25
24
  PromptService,
26
- PromptTemplate
25
+ PromptTemplate,
26
+ UserRequest
27
27
  } from '@theia/ai-core';
28
28
  import { inject, injectable } from '@theia/core/shared/inversify';
29
29
  import { ChatSession } from './chat-service';
@@ -103,22 +103,19 @@ export class ChatSessionNamingAgent implements Agent {
103
103
  throw new Error('Unable to create prompt message for generating chat session name');
104
104
  }
105
105
 
106
- const request: LanguageModelRequest = {
106
+ const sessionId = generateUuid();
107
+ const requestId = generateUuid();
108
+ const request: UserRequest = {
107
109
  messages: [{
108
110
  actor: 'user',
109
111
  text: message,
110
112
  type: 'text'
111
- }]
112
- };
113
-
114
- const sessionId = generateUuid();
115
- const requestId = generateUuid();
116
- this.recordingService.recordRequest({
117
- agentId: this.id,
118
- sessionId,
113
+ }],
119
114
  requestId,
120
- ...request
121
- });
115
+ sessionId,
116
+ agentId: this.id
117
+ };
118
+ this.recordingService.recordRequest(request);
122
119
 
123
120
  const result = await lm.request(request);
124
121
  const response = await getTextOfResponse(result);
@@ -0,0 +1,114 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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
+ import { expect } from 'chai';
18
+ import { MutableChatRequestModel, CodeChatResponseContentImpl, MarkdownChatResponseContentImpl } from './chat-model';
19
+ import { parseContents } from './parse-contents';
20
+ import { ResponseContentMatcher } from './response-content-matcher';
21
+
22
+ const fakeRequest = {} as MutableChatRequestModel;
23
+
24
+ // Custom matchers with incompleteContentFactory for testing
25
+ const TestCodeContentMatcher: ResponseContentMatcher = {
26
+ start: /^```.*?$/m,
27
+ end: /^```$/m,
28
+ contentFactory: (content: string) => {
29
+ const language = content.match(/^```(\w+)/)?.[1] || '';
30
+ const code = content.replace(/^```(\w+)\n|```$/g, '');
31
+ return new CodeChatResponseContentImpl(code.trim(), language);
32
+ },
33
+ incompleteContentFactory: (content: string) => {
34
+ const language = content.match(/^```(\w+)/)?.[1] || '';
35
+ // Remove only the start delimiter, since we don't have an end delimiter yet
36
+ const code = content.replace(/^```(\w+)\n?/g, '');
37
+ return new CodeChatResponseContentImpl(code.trim(), language);
38
+ }
39
+ };
40
+
41
+ describe('parseContents with incomplete parts', () => {
42
+ it('should handle incomplete code blocks with incompleteContentFactory', () => {
43
+ // Only the start of a code block without an end
44
+ const text = '```typescript\nconsole.log("Hello World");';
45
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
46
+
47
+ expect(result.length).to.equal(1);
48
+ expect(result[0]).to.be.instanceOf(CodeChatResponseContentImpl);
49
+ const codeContent = result[0] as CodeChatResponseContentImpl;
50
+ expect(codeContent.code).to.equal('console.log("Hello World");');
51
+ expect(codeContent.language).to.equal('typescript');
52
+ });
53
+
54
+ it('should handle complete code blocks with contentFactory', () => {
55
+ const text = '```typescript\nconsole.log("Hello World");\n```';
56
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
57
+
58
+ expect(result.length).to.equal(1);
59
+ expect(result[0]).to.be.instanceOf(CodeChatResponseContentImpl);
60
+ const codeContent = result[0] as CodeChatResponseContentImpl;
61
+ expect(codeContent.code).to.equal('console.log("Hello World");');
62
+ expect(codeContent.language).to.equal('typescript');
63
+ });
64
+
65
+ it('should handle mixed content with incomplete and complete blocks', () => {
66
+ const text = 'Some text\n```typescript\nconsole.log("Hello");\n```\nMore text\n```python\nprint("World")';
67
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
68
+
69
+ expect(result.length).to.equal(4);
70
+ expect(result[0]).to.be.instanceOf(MarkdownChatResponseContentImpl);
71
+ expect(result[1]).to.be.instanceOf(CodeChatResponseContentImpl);
72
+ const completeContent = result[1] as CodeChatResponseContentImpl;
73
+ expect(completeContent.language).to.equal('typescript');
74
+ expect(result[2]).to.be.instanceOf(MarkdownChatResponseContentImpl);
75
+ expect(result[3]).to.be.instanceOf(CodeChatResponseContentImpl);
76
+ const incompleteContent = result[3] as CodeChatResponseContentImpl;
77
+ expect(incompleteContent.language).to.equal('python');
78
+ });
79
+
80
+ it('should use default content factory if no incompleteContentFactory provided', () => {
81
+ // Create a matcher without incompleteContentFactory
82
+ const matcherWithoutIncomplete: ResponseContentMatcher = {
83
+ start: /^<test>$/m,
84
+ end: /^<\/test>$/m,
85
+ contentFactory: (content: string) => new MarkdownChatResponseContentImpl('complete: ' + content)
86
+ };
87
+
88
+ // Text with only the start delimiter
89
+ const text = '<test>\ntest content';
90
+ const result = parseContents(text, fakeRequest, [matcherWithoutIncomplete]);
91
+
92
+ expect(result.length).to.equal(1);
93
+ expect(result[0]).to.be.instanceOf(MarkdownChatResponseContentImpl);
94
+ expect((result[0] as MarkdownChatResponseContentImpl).content.value).to.equal('<test>\ntest content');
95
+ });
96
+
97
+ it('should prefer complete matches over incomplete ones', () => {
98
+ // Text with both a complete and incomplete match at same position
99
+ const text = '```typescript\nconsole.log();\n```\n<test>\ntest content';
100
+ const matcherWithoutIncomplete: ResponseContentMatcher = {
101
+ start: /^<test>$/m,
102
+ end: /^<\/test>$/m,
103
+ contentFactory: (content: string) => new MarkdownChatResponseContentImpl('complete: ' + content)
104
+ };
105
+
106
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher, matcherWithoutIncomplete]);
107
+
108
+ expect(result.length).to.equal(2);
109
+ expect(result[0]).to.be.instanceOf(CodeChatResponseContentImpl);
110
+ expect((result[0] as CodeChatResponseContentImpl).language).to.equal('typescript');
111
+ expect(result[1]).to.be.instanceOf(MarkdownChatResponseContentImpl);
112
+ expect((result[1] as MarkdownChatResponseContentImpl).content.value).to.contain('test content');
113
+ });
114
+ });
@@ -19,6 +19,16 @@ import { MutableChatRequestModel, ChatResponseContent, CodeChatResponseContentIm
19
19
  import { parseContents } from './parse-contents';
20
20
  import { CodeContentMatcher, ResponseContentMatcher } from './response-content-matcher';
21
21
 
22
+ export const TestCodeContentMatcher: ResponseContentMatcher = {
23
+ start: /^```.*?$/m,
24
+ end: /^```$/m,
25
+ contentFactory: (content: string) => {
26
+ const language = content.match(/^```(\w+)/)?.[1] || '';
27
+ const code = content.replace(/^```(\w+)\n|```$/g, '');
28
+ return new CodeChatResponseContentImpl(code.trim(), language);
29
+ }
30
+ };
31
+
22
32
  export class CommandChatResponseContentImpl implements ChatResponseContent {
23
33
  constructor(public readonly command: string) { }
24
34
  kind = 'command';
@@ -38,19 +48,19 @@ const fakeRequest = {} as MutableChatRequestModel;
38
48
  describe('parseContents', () => {
39
49
  it('should parse code content', () => {
40
50
  const text = '```typescript\nconsole.log("Hello World");\n```';
41
- const result = parseContents(text, fakeRequest);
51
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
42
52
  expect(result).to.deep.equal([new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript')]);
43
53
  });
44
54
 
45
55
  it('should parse markdown content', () => {
46
56
  const text = 'Hello **World**';
47
- const result = parseContents(text, fakeRequest);
57
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
48
58
  expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Hello **World**')]);
49
59
  });
50
60
 
51
61
  it('should parse multiple content blocks', () => {
52
62
  const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**';
53
- const result = parseContents(text, fakeRequest);
63
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
54
64
  expect(result).to.deep.equal([
55
65
  new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
56
66
  new MarkdownChatResponseContentImpl('\nHello **World**')
@@ -59,7 +69,7 @@ describe('parseContents', () => {
59
69
 
60
70
  it('should parse multiple content blocks with different languages', () => {
61
71
  const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")\n```';
62
- const result = parseContents(text, fakeRequest);
72
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
63
73
  expect(result).to.deep.equal([
64
74
  new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
65
75
  new CodeChatResponseContentImpl('print("Hello World")', 'python')
@@ -68,7 +78,7 @@ describe('parseContents', () => {
68
78
 
69
79
  it('should parse multiple content blocks with different languages and markdown', () => {
70
80
  const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**\n```python\nprint("Hello World")\n```';
71
- const result = parseContents(text, fakeRequest);
81
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
72
82
  expect(result).to.deep.equal([
73
83
  new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
74
84
  new MarkdownChatResponseContentImpl('\nHello **World**\n'),
@@ -78,7 +88,7 @@ describe('parseContents', () => {
78
88
 
79
89
  it('should parse content blocks with empty content', () => {
80
90
  const text = '```typescript\n```\nHello **World**\n```python\nprint("Hello World")\n```';
81
- const result = parseContents(text, fakeRequest);
91
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
82
92
  expect(result).to.deep.equal([
83
93
  new CodeChatResponseContentImpl('', 'typescript'),
84
94
  new MarkdownChatResponseContentImpl('\nHello **World**\n'),
@@ -88,7 +98,7 @@ describe('parseContents', () => {
88
98
 
89
99
  it('should parse content with markdown, code, and markdown', () => {
90
100
  const text = 'Hello **World**\n```typescript\nconsole.log("Hello World");\n```\nGoodbye **World**';
91
- const result = parseContents(text, fakeRequest);
101
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
92
102
  expect(result).to.deep.equal([
93
103
  new MarkdownChatResponseContentImpl('Hello **World**\n'),
94
104
  new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
@@ -98,34 +108,36 @@ describe('parseContents', () => {
98
108
 
99
109
  it('should handle text with no special content', () => {
100
110
  const text = 'Just some plain text.';
101
- const result = parseContents(text, fakeRequest);
111
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
102
112
  expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Just some plain text.')]);
103
113
  });
104
114
 
105
115
  it('should handle text with only start code block', () => {
106
116
  const text = '```typescript\nconsole.log("Hello World");';
117
+ // We're using the standard CodeContentMatcher which has incompleteContentFactory
107
118
  const result = parseContents(text, fakeRequest);
108
- expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('```typescript\nconsole.log("Hello World");')]);
119
+ expect(result).to.deep.equal([new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript')]);
109
120
  });
110
121
 
111
122
  it('should handle text with only end code block', () => {
112
123
  const text = 'console.log("Hello World");\n```';
113
- const result = parseContents(text, fakeRequest);
124
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
114
125
  expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('console.log("Hello World");\n```')]);
115
126
  });
116
127
 
117
128
  it('should handle text with unmatched code block', () => {
118
129
  const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")';
130
+ // We're using the standard CodeContentMatcher which has incompleteContentFactory
119
131
  const result = parseContents(text, fakeRequest);
120
132
  expect(result).to.deep.equal([
121
133
  new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
122
- new MarkdownChatResponseContentImpl('\n```python\nprint("Hello World")')
134
+ new CodeChatResponseContentImpl('print("Hello World")', 'python')
123
135
  ]);
124
136
  });
125
137
 
126
138
  it('should parse code block without newline after language', () => {
127
139
  const text = '```typescript console.log("Hello World");```';
128
- const result = parseContents(text, fakeRequest);
140
+ const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
129
141
  expect(result).to.deep.equal([
130
142
  new MarkdownChatResponseContentImpl('```typescript console.log("Hello World");```')
131
143
  ]);
@@ -20,6 +20,7 @@ interface Match {
20
20
  matcher: ResponseContentMatcher;
21
21
  index: number;
22
22
  content: string;
23
+ isComplete: boolean;
23
24
  }
24
25
 
25
26
  export function parseContents(
@@ -50,7 +51,16 @@ export function parseContents(
50
51
  }
51
52
  }
52
53
  // 2. Add the matched content object
53
- result.push(match.matcher.contentFactory(match.content, request));
54
+ if (match.isComplete) {
55
+ // Complete match, use regular content factory
56
+ result.push(match.matcher.contentFactory(match.content, request));
57
+ } else if (match.matcher.incompleteContentFactory) {
58
+ // Incomplete match with an incomplete content factory available
59
+ result.push(match.matcher.incompleteContentFactory(match.content, request));
60
+ } else {
61
+ // Incomplete match but no incomplete content factory available, use default
62
+ result.push(defaultContentFactory(match.content, request));
63
+ }
54
64
  // Update currentIndex to the end of the end of the match
55
65
  // And continue with the search after the end of the match
56
66
  currentIndex += match.index + match.content.length;
@@ -60,7 +70,9 @@ export function parseContents(
60
70
  }
61
71
 
62
72
  export function findFirstMatch(contentMatchers: ResponseContentMatcher[], text: string): Match | undefined {
63
- let firstMatch: { matcher: ResponseContentMatcher, index: number, content: string } | undefined;
73
+ let firstMatch: Match | undefined;
74
+ let firstIncompleteMatch: Match | undefined;
75
+
64
76
  for (const matcher of contentMatchers) {
65
77
  const startMatch = matcher.start.exec(text);
66
78
  if (!startMatch) {
@@ -70,24 +82,58 @@ export function findFirstMatch(contentMatchers: ResponseContentMatcher[], text:
70
82
  const endOfStartMatch = startMatch.index + startMatch[0].length;
71
83
  if (endOfStartMatch >= text.length) {
72
84
  // There is no text after the start match.
73
- // No need to search for the end match yet, try next matcher.
85
+ // This is an incomplete match if the matcher has an incompleteContentFactory
86
+ if (matcher.incompleteContentFactory) {
87
+ const incompleteMatch: Match = {
88
+ matcher,
89
+ index: startMatch.index,
90
+ content: text.substring(startMatch.index),
91
+ isComplete: false
92
+ };
93
+ if (!firstIncompleteMatch || incompleteMatch.index < firstIncompleteMatch.index) {
94
+ firstIncompleteMatch = incompleteMatch;
95
+ }
96
+ }
74
97
  continue;
75
98
  }
99
+
76
100
  const remainingTextAfterStartMatch = text.substring(endOfStartMatch);
77
101
  const endMatch = matcher.end.exec(remainingTextAfterStartMatch);
102
+
78
103
  if (!endMatch) {
79
- // No end match found, try next matcher.
104
+ // No end match found, this is an incomplete match
105
+ if (matcher.incompleteContentFactory) {
106
+ const incompleteMatch: Match = {
107
+ matcher,
108
+ index: startMatch.index,
109
+ content: text.substring(startMatch.index),
110
+ isComplete: false
111
+ };
112
+ if (!firstIncompleteMatch || incompleteMatch.index < firstIncompleteMatch.index) {
113
+ firstIncompleteMatch = incompleteMatch;
114
+ }
115
+ }
80
116
  continue;
81
117
  }
118
+
82
119
  // Found start and end match.
83
120
  // Record the full match, if it is the earliest found so far.
84
121
  const index = startMatch.index;
85
122
  const contentEnd = index + startMatch[0].length + endMatch.index + endMatch[0].length;
86
123
  const content = text.substring(index, contentEnd);
124
+ const completeMatch: Match = { matcher, index, content, isComplete: true };
125
+
87
126
  if (!firstMatch || index < firstMatch.index) {
88
- firstMatch = { matcher, index, content };
127
+ firstMatch = completeMatch;
89
128
  }
90
129
  }
91
- return firstMatch;
130
+
131
+ // If we found a complete match, return it
132
+ if (firstMatch) {
133
+ return firstMatch;
134
+ }
135
+
136
+ // Otherwise, return the first incomplete match if one exists
137
+ return firstIncompleteMatch;
92
138
  }
93
139
 
@@ -23,7 +23,7 @@ import { injectable } from '@theia/core/shared/inversify';
23
23
 
24
24
  export type ResponseContentFactory = (content: string, request: MutableChatRequestModel) => ChatResponseContent;
25
25
 
26
- export const MarkdownContentFactory: ResponseContentFactory = (content: string) =>
26
+ export const MarkdownContentFactory: ResponseContentFactory = (content: string, request: MutableChatRequestModel) =>
27
27
  new MarkdownChatResponseContentImpl(content);
28
28
 
29
29
  /**
@@ -53,14 +53,32 @@ export interface ResponseContentMatcher {
53
53
  * from start index to end index of the match (including delimiters).
54
54
  */
55
55
  contentFactory: ResponseContentFactory;
56
+ /**
57
+ * Optional factory for creating a response content when only the start delimiter has been matched,
58
+ * but not yet the end delimiter. Used during streaming to provide better visual feedback.
59
+ * If not provided, the default content factory will be used until the end delimiter is matched.
60
+ */
61
+ incompleteContentFactory?: ResponseContentFactory;
56
62
  }
57
63
 
58
64
  export const CodeContentMatcher: ResponseContentMatcher = {
59
- start: /^```.*?$/m,
65
+ // Only match when we have the complete first line ending with a newline
66
+ // This ensures we have the full language specification before creating the editor
67
+ start: /^```.*\n/m,
60
68
  end: /^```$/m,
61
- contentFactory: (content: string) => {
69
+ contentFactory: (content: string, request: MutableChatRequestModel) => {
62
70
  const language = content.match(/^```(\w+)/)?.[1] || '';
63
71
  const code = content.replace(/^```(\w+)\n|```$/g, '');
72
+ return new CodeChatResponseContentImpl(code.trim(), language);
73
+ },
74
+ incompleteContentFactory: (content: string, request: MutableChatRequestModel) => {
75
+ // By this point, we know we have at least the complete first line with ```
76
+ const firstLine = content.split('\n')[0];
77
+ const language = firstLine.match(/^```(\w+)/)?.[1] || '';
78
+
79
+ // Remove the first line to get just the code content
80
+ const code = content.substring(content.indexOf('\n') + 1);
81
+
64
82
  return new CodeChatResponseContentImpl(code.trim(), language);
65
83
  }
66
84
  };