From 4119730d2444b466e8431a97b742f32316ac49ac Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 29 Mar 2026 22:49:34 +0100 Subject: [PATCH 1/9] Add structured parse hint with intent detection and closest pattern matching Replace flat error string in AutomationPlannerService with a structured ParseHintPayload containing supportedPatterns array, detectedIntent, closestPattern, and exampleInstruction. Uses keyword scoring to find the most relevant pattern suggestion for the user's input. --- .../Services/AutomationPlannerService.cs | 102 +++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/backend/src/Taskdeck.Application/Services/AutomationPlannerService.cs b/backend/src/Taskdeck.Application/Services/AutomationPlannerService.cs index 60b03869a..ee87c4802 100644 --- a/backend/src/Taskdeck.Application/Services/AutomationPlannerService.cs +++ b/backend/src/Taskdeck.Application/Services/AutomationPlannerService.cs @@ -370,8 +370,8 @@ public async Task> ParseInstructionAsync( } if (!operations.Any()) - return Result.Failure(ErrorCodes.ValidationError, - "Could not parse instruction. Supported patterns: 'create card \"title\"', 'move card {id} to column \"name\"', 'archive card {id}', 'archive cards matching \"pattern\"', 'update card {id} title/description \"value\"', 'rename board to \"name\"', 'update board description \"value\"', 'archive board', 'unarchive board', 'move column \"name\" to position {n}'"); + return Result.Failure(ErrorCodes.ValidationError, + BuildParseHintMessage(instruction)); // Classify risk var operationDtos = operations.Select(o => new ProposalOperationDto( @@ -420,6 +420,104 @@ public async Task> ParseInstructionAsync( } } + internal static readonly string ParseHintMarker = "[PARSE_HINT]"; + + internal static readonly (string Pattern, string Example, string[] Keywords)[] SupportedPatterns = new[] + { + ("create card \"title\"", "create card \"My new task\"", new[] { "create", "add", "new", "card", "task" }), + ("create card \"title\" in column \"name\"", "create card \"Bug fix\" in column \"In Progress\"", new[] { "create", "add", "new", "card", "column", "in" }), + ("move card {id} to column \"name\"", "move card abc-123 to column \"Done\"", new[] { "move", "card", "column", "to" }), + ("archive card {id}", "archive card abc-123", new[] { "archive", "card", "remove", "delete" }), + ("archive cards matching \"pattern\"", "archive cards matching \"old tasks\"", new[] { "archive", "cards", "matching", "bulk", "batch" }), + ("update card {id} title \"value\"", "update card abc-123 title \"New title\"", new[] { "update", "edit", "change", "card", "title", "rename" }), + ("update card {id} description \"value\"", "update card abc-123 description \"Updated details\"", new[] { "update", "edit", "change", "card", "description", "desc" }), + ("rename board to \"name\"", "rename board to \"Sprint 5\"", new[] { "rename", "board", "name", "title" }), + ("update board description \"value\"", "update board description \"Team workspace\"", new[] { "update", "board", "description", "desc" }), + ("archive board", "archive board", new[] { "archive", "board" }), + ("unarchive board", "unarchive board", new[] { "unarchive", "restore", "board" }), + ("move column \"name\" to position {n}", "move column \"Done\" to position 0", new[] { "move", "column", "position", "reorder" }), + }; + + internal static string BuildParseHintMessage(string instruction) + { + var detectedIntent = DetectIntent(instruction); + var bestMatch = FindClosestPattern(instruction, detectedIntent); + + var patterns = SupportedPatterns.Select(p => p.Pattern).ToArray(); + var hint = new ParseHintPayload( + patterns, + bestMatch.Example, + bestMatch.Pattern, + detectedIntent); + + var hintJson = JsonSerializer.Serialize(hint, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + return $"Could not parse instruction into a proposal.{ParseHintMarker}{hintJson}"; + } + + internal static string? DetectIntent(string instruction) + { + var lower = instruction.Trim().ToLowerInvariant(); + + if (lower.Contains("create") || lower.Contains("add") || lower.Contains("new")) + return "create"; + if (lower.Contains("move") || lower.Contains("drag") || lower.Contains("transfer")) + return "move"; + if (lower.Contains("archive") || lower.Contains("remove") || lower.Contains("delete")) + return "archive"; + if (lower.Contains("update") || lower.Contains("edit") || lower.Contains("change") || lower.Contains("rename") || lower.Contains("set")) + return "update"; + if (lower.Contains("unarchive") || lower.Contains("restore")) + return "unarchive"; + if (lower.Contains("reorder") || lower.Contains("position")) + return "reorder"; + + return null; + } + + internal static (string Pattern, string Example) FindClosestPattern(string instruction, string? detectedIntent) + { + var lower = instruction.Trim().ToLowerInvariant(); + var words = lower.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var bestScore = -1; + var bestPattern = SupportedPatterns[0]; + + foreach (var entry in SupportedPatterns) + { + var score = 0; + + // Boost patterns whose keywords overlap with the instruction words + foreach (var keyword in entry.Keywords) + { + if (words.Any(w => w.Contains(keyword))) + score += 2; + } + + // Extra boost if the detected intent matches the first keyword + if (detectedIntent != null && entry.Keywords.Length > 0 && + entry.Keywords[0].Equals(detectedIntent, StringComparison.OrdinalIgnoreCase)) + score += 5; + + if (score > bestScore) + { + bestScore = score; + bestPattern = entry; + } + } + + return (bestPattern.Pattern, bestPattern.Example); + } + + internal record ParseHintPayload( + string[] SupportedPatterns, + string ExampleInstruction, + string ClosestPattern, + string? DetectedIntent); + private static bool TryResolveCorrelationId(string? correlationId, out string resolvedCorrelationId, out string error) { if (correlationId == null) From 6fc5744bc74f49b568758841bb7c6c14124b190f Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 29 Mar 2026 22:49:38 +0100 Subject: [PATCH 2/9] Route parse hint errors to parse-hint message type in ChatService Detect structured parse hint marker in planner error messages and set messageType to parse-hint so the frontend can render a hint card instead of inline error text. Non-hint errors still fall through to status type. --- backend/src/Taskdeck.Application/Services/ChatService.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/src/Taskdeck.Application/Services/ChatService.cs b/backend/src/Taskdeck.Application/Services/ChatService.cs index 9b2a26a75..c3dd50bc2 100644 --- a/backend/src/Taskdeck.Application/Services/ChatService.cs +++ b/backend/src/Taskdeck.Application/Services/ChatService.cs @@ -243,6 +243,15 @@ await _quotaService.RecordUsageAsync( proposalId = proposalResult.Value.Id; assistantContent = $"{llmResult.Content}\n\nProposal created for review: {proposalResult.Value.Id}"; } + else if (proposalResult.ErrorMessage?.Contains(AutomationPlannerService.ParseHintMarker) == true) + { + // Structured parse hint — use parse-hint message type so frontend can render a hint card + var hintContext = llmResult.IsActionable + ? "I detected a task request but could not parse it into a proposal." + : "Could not create the requested proposal."; + assistantContent = $"{llmResult.Content}\n\n{hintContext}\n{proposalResult.ErrorMessage}"; + messageType = "parse-hint"; + } else if (llmResult.IsActionable) { // Planner could not parse an auto-detected actionable message — hint the user From 0e8ace598b3e4dd8b9352f32e669243e7ed764fc Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 29 Mar 2026 22:49:41 +0100 Subject: [PATCH 3/9] Add parse-hint message type and ParseHintPayload interface --- frontend/taskdeck-web/src/types/chat.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/taskdeck-web/src/types/chat.ts b/frontend/taskdeck-web/src/types/chat.ts index 16e904b93..3b8974b96 100644 --- a/frontend/taskdeck-web/src/types/chat.ts +++ b/frontend/taskdeck-web/src/types/chat.ts @@ -2,7 +2,14 @@ export type ChatSessionStatus = 'Active' | 'Archived' export type ChatSessionStatusValue = ChatSessionStatus | number export type ChatRole = 'User' | 'Assistant' | 'System' export type ChatRoleValue = ChatRole | number -export type ChatMessageType = 'text' | 'proposal-reference' | 'error' | 'status' | 'degraded' +export type ChatMessageType = 'text' | 'proposal-reference' | 'error' | 'status' | 'degraded' | 'parse-hint' + +export interface ParseHintPayload { + supportedPatterns: string[] + exampleInstruction: string + closestPattern: string + detectedIntent: string | null +} export interface ChatMessage { id: string From 61d3430eba19f39f85b6f311f0fcffbc768e8837 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 29 Mar 2026 22:49:44 +0100 Subject: [PATCH 4/9] Add extractParseHint utility for parsing structured hint payloads --- frontend/taskdeck-web/src/utils/chat.ts | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/taskdeck-web/src/utils/chat.ts b/frontend/taskdeck-web/src/utils/chat.ts index 3f02feedb..c2d002029 100644 --- a/frontend/taskdeck-web/src/utils/chat.ts +++ b/frontend/taskdeck-web/src/utils/chat.ts @@ -1,4 +1,4 @@ -import type { ChatRoleValue, ChatSessionStatusValue } from '../types/chat' +import type { ChatRoleValue, ChatSessionStatusValue, ParseHintPayload } from '../types/chat' const chatRoleByIndex = ['User', 'Assistant', 'System'] as const const chatSessionStatusByIndex = ['Active', 'Archived'] as const @@ -12,6 +12,34 @@ export function normalizeChatRole(value: ChatRoleValue): typeof chatRoleByIndex[ return found ?? 'User' } +const PARSE_HINT_MARKER = '[PARSE_HINT]' + +export interface ParsedHintMessage { + textBeforeHint: string + hint: ParseHintPayload +} + +export function extractParseHint(content: string): ParsedHintMessage | null { + const markerIndex = content.indexOf(PARSE_HINT_MARKER) + if (markerIndex === -1) { + return null + } + + const jsonStart = markerIndex + PARSE_HINT_MARKER.length + const jsonStr = content.substring(jsonStart) + const textBeforeHint = content.substring(0, markerIndex).trimEnd() + + try { + const hint = JSON.parse(jsonStr) as ParseHintPayload + if (!hint.supportedPatterns || !Array.isArray(hint.supportedPatterns)) { + return null + } + return { textBeforeHint, hint } + } catch { + return null + } +} + export function normalizeChatSessionStatus(value: ChatSessionStatusValue): typeof chatSessionStatusByIndex[number] { if (typeof value === 'number') { return chatSessionStatusByIndex[value] ?? 'Active' From cc93dc704210134c00bff44439bb218da8768bb7 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 29 Mar 2026 22:49:50 +0100 Subject: [PATCH 5/9] Render parse failures as hint card with try-this-instead button Display parse-hint messages as a styled info card showing detected intent, closest matching pattern, and a pre-fill button. Include a collapsible list of all supported patterns. Card uses info styling, not error styling. --- .../src/views/AutomationChatView.vue | 164 +++++++++++++++++- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/frontend/taskdeck-web/src/views/AutomationChatView.vue b/frontend/taskdeck-web/src/views/AutomationChatView.vue index 799565b7a..911fbb012 100644 --- a/frontend/taskdeck-web/src/views/AutomationChatView.vue +++ b/frontend/taskdeck-web/src/views/AutomationChatView.vue @@ -8,7 +8,8 @@ import { boardsApi } from '../api/boardsApi' import { useToastStore } from '../store/toastStore' import type { ChatProviderHealth, ChatMessage, ChatSession } from '../types/chat' import type { Board } from '../types/board' -import { normalizeChatRole } from '../utils/chat' +import { normalizeChatRole, extractParseHint } from '../utils/chat' +import type { ParsedHintMessage } from '../utils/chat' import { getErrorDisplay } from '../composables/useErrorMapper' import InputAssistField from '../components/common/InputAssistField.vue' import { buildInputAssistOptions } from '../utils/inputAssist' @@ -48,6 +49,7 @@ const newSessionBoardId = ref('') const selectedNewSessionBoardId = ref(null) const messageContent = ref('') const requestProposal = ref(false) +const expandedHintIds = ref>(new Set()) const boardOptions = computed(() => buildInputAssistOptions( @@ -412,6 +414,28 @@ function pushToReview(hash?: string) { }) } +function getParseHint(message: ChatMessage): ParsedHintMessage | null { + if (message.messageType !== 'parse-hint') { + return null + } + return extractParseHint(message.content) +} + +function toggleHintPatterns(messageId: string) { + const updated = new Set(expandedHintIds.value) + if (updated.has(messageId)) { + updated.delete(messageId) + } else { + updated.add(messageId) + } + expandedHintIds.value = updated +} + +function applyHintSuggestion(example: string) { + messageContent.value = example + requestProposal.value = true +} + function openReviewRoute() { pushToReview() } @@ -577,12 +601,62 @@ watch(
Degraded response{{ message.degradedReason ? `: ${message.degradedReason}` : '' }}
-
-
{{ message.content }}
+ +
Proposal: {{ message.proposalId }}