Skip to content

Commit b76d47e

Browse files
authored
Merge pull request #644 from Chris0Jeky/fix/616-chat-response-truncation
Fix chat response truncation and raw JSON display
2 parents 5b87f63 + 67290f5 commit b76d47e

File tree

6 files changed

+133
-3
lines changed

6 files changed

+133
-3
lines changed

backend/src/Taskdeck.Application/Services/GeminiLlmProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ internal static bool LooksLikeTruncatedJson(string text)
372372
return false;
373373

374374
var trimmed = text.TrimStart();
375-
if (!trimmed.StartsWith('{'))
375+
if (!trimmed.StartsWith('{') && !trimmed.StartsWith('['))
376376
return false;
377377

378378
try

backend/src/Taskdeck.Application/Services/OpenAiLlmProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ internal static bool LooksLikeTruncatedJson(string text)
356356
return false;
357357

358358
var trimmed = text.TrimStart();
359-
if (!trimmed.StartsWith('{'))
359+
if (!trimmed.StartsWith('{') && !trimmed.StartsWith('['))
360360
return false;
361361

362362
try

backend/tests/Taskdeck.Application.Tests/Services/GeminiLlmProviderTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,9 @@ [new ChatCompletionMessage("User", "tell me something")],
476476
[InlineData("plain text response", false)]
477477
[InlineData("", false)]
478478
[InlineData(" { broken json", true)]
479+
[InlineData("[{\"id\":1},", true)]
480+
[InlineData("[\"complete\"]", false)]
481+
[InlineData(" [ broken array", true)]
479482
public void LooksLikeTruncatedJson_ShouldDetectPartialJson(string input, bool expected)
480483
{
481484
GeminiLlmProvider.LooksLikeTruncatedJson(input).Should().Be(expected);

backend/tests/Taskdeck.Application.Tests/Services/OpenAiLlmProviderTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,9 @@ public async Task CompleteAsync_ShouldReturnDegraded_WhenJsonModeResponseIsInval
343343
[InlineData("plain text response", false)]
344344
[InlineData("", false)]
345345
[InlineData(" { broken json", true)]
346+
[InlineData("[{\"id\":1},", true)]
347+
[InlineData("[\"complete\"]", false)]
348+
[InlineData(" [ broken array", true)]
346349
public void LooksLikeTruncatedJson_ShouldDetectPartialJson(string input, bool expected)
347350
{
348351
OpenAiLlmProvider.LooksLikeTruncatedJson(input).Should().Be(expected);

frontend/taskdeck-web/src/tests/views/AutomationChatView.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,130 @@ describe('AutomationChatView', () => {
668668
expect(wrapper.find('.td-hint-card__patterns').exists()).toBe(false)
669669
})
670670

671+
it('shows truncation notice instead of raw JSON for truncated assistant messages', async () => {
672+
const now = new Date().toISOString()
673+
const truncatedSession = {
674+
id: 'session-trunc',
675+
userId: 'user-1',
676+
boardId: null,
677+
title: 'Truncation test',
678+
status: 'Active',
679+
createdAt: now,
680+
updatedAt: now,
681+
recentMessages: [
682+
{
683+
id: 'msg-user',
684+
sessionId: 'session-trunc',
685+
role: 'User',
686+
content: 'Tell me about the board',
687+
messageType: 'text' as const,
688+
proposalId: null,
689+
tokenUsage: null,
690+
createdAt: now,
691+
},
692+
{
693+
id: 'msg-trunc',
694+
sessionId: 'session-trunc',
695+
role: 'Assistant',
696+
content: '{"reply":"I understand your question about',
697+
messageType: 'text' as const,
698+
proposalId: null,
699+
tokenUsage: 50,
700+
createdAt: now,
701+
},
702+
],
703+
}
704+
mocks.getMySessions.mockResolvedValue([truncatedSession])
705+
mocks.getSession.mockResolvedValue(truncatedSession)
706+
707+
const wrapper = mountView()
708+
await waitForAsyncUi()
709+
710+
const truncatedMsg = wrapper.find('.td-message-content--truncated')
711+
expect(truncatedMsg.exists()).toBe(true)
712+
expect(truncatedMsg.text()).toContain('This response was cut short')
713+
expect(wrapper.text()).not.toContain('{"reply"')
714+
})
715+
716+
it('shows truncation notice for degraded messages with truncated JSON content', async () => {
717+
const now = new Date().toISOString()
718+
const degradedTruncSession = {
719+
id: 'session-deg-trunc',
720+
userId: 'user-1',
721+
boardId: null,
722+
title: 'Degraded truncation test',
723+
status: 'Active',
724+
createdAt: now,
725+
updatedAt: now,
726+
recentMessages: [
727+
{
728+
id: 'msg-user',
729+
sessionId: 'session-deg-trunc',
730+
role: 'User',
731+
content: 'Hello',
732+
messageType: 'text' as const,
733+
proposalId: null,
734+
tokenUsage: null,
735+
createdAt: now,
736+
},
737+
{
738+
id: 'msg-deg-trunc',
739+
sessionId: 'session-deg-trunc',
740+
role: 'Assistant',
741+
content: '{"reply":"I understand',
742+
messageType: 'degraded' as const,
743+
proposalId: null,
744+
tokenUsage: 10,
745+
createdAt: now,
746+
degradedReason: 'Response was truncated',
747+
},
748+
],
749+
}
750+
mocks.getMySessions.mockResolvedValue([degradedTruncSession])
751+
mocks.getSession.mockResolvedValue(degradedTruncSession)
752+
753+
const wrapper = mountView()
754+
await waitForAsyncUi()
755+
756+
expect(wrapper.text()).toContain('Degraded response: Response was truncated')
757+
expect(wrapper.find('.td-message-content--truncated').exists()).toBe(true)
758+
expect(wrapper.text()).toContain('This response was cut short')
759+
expect(wrapper.text()).not.toContain('{"reply"')
760+
})
761+
762+
it('detects truncated JSON arrays starting with [', async () => {
763+
const now = new Date().toISOString()
764+
const arrayTruncSession = {
765+
id: 'session-arr-trunc',
766+
userId: 'user-1',
767+
boardId: null,
768+
title: 'Array truncation test',
769+
status: 'Active',
770+
createdAt: now,
771+
updatedAt: now,
772+
recentMessages: [
773+
{
774+
id: 'msg-arr-trunc',
775+
sessionId: 'session-arr-trunc',
776+
role: 'Assistant',
777+
content: '[{"id":1,"name":"incomplete',
778+
messageType: 'text' as const,
779+
proposalId: null,
780+
tokenUsage: 30,
781+
createdAt: now,
782+
},
783+
],
784+
}
785+
mocks.getMySessions.mockResolvedValue([arrayTruncSession])
786+
mocks.getSession.mockResolvedValue(arrayTruncSession)
787+
788+
const wrapper = mountView()
789+
await waitForAsyncUi()
790+
791+
expect(wrapper.find('.td-message-content--truncated').exists()).toBe(true)
792+
expect(wrapper.text()).toContain('This response was cut short')
793+
})
794+
671795
it('surfaces provider-health loading failures explicitly', async () => {
672796
mocks.getHealth.mockRejectedValueOnce(new Error('health down'))
673797

frontend/taskdeck-web/src/views/AutomationChatView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function renderMarkdown(content: string): string {
3131
function isTruncatedJson(content: string): boolean {
3232
if (!content) return false
3333
const trimmed = content.trim()
34-
if (!trimmed.startsWith('{')) return false
34+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return false
3535
try {
3636
JSON.parse(trimmed)
3737
return false

0 commit comments

Comments
 (0)