Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ const categories: ShortcutCategory[] = [
</h3>
<div class="space-y-2 ml-3">
<div
v-for="(shortcut, index) in category.shortcuts"
:key="index"
v-for="shortcut in category.shortcuts"
:key="`${category.title}-${shortcut.description}`"
class="flex items-center justify-between py-2 px-3 rounded hover:bg-gray-50 transition-colors"
>
<span class="text-gray-700">{{ shortcut.description }}</span>
<div class="flex items-center gap-1">
<template v-for="(key, keyIndex) in shortcut.keys" :key="keyIndex">
<template v-for="(key, keyIndex) in shortcut.keys" :key="`${shortcut.description}-${keyIndex}-${key}`">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Including keyIndex (the array index) in the :key binding defeats the purpose of establishing a stable key, as any changes to the shortcut.keys array would still cause the key to change. Since each key string (e.g., 'j', 'ArrowDown') is unique within the shortcut.keys array, you can simply use key as the :key value. This provides a truly stable and unique key for each sibling element without relying on the index.

                    <template v-for="(key, keyIndex) in shortcut.keys" :key="key">

<kbd
class="px-2 py-1 text-sm font-semibold text-gray-800 bg-gray-100 border border-gray-300 rounded shadow-sm min-w-[2rem] text-center"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ const emit = defineEmits<{
aria-label="Supported instruction patterns"
>
<li
v-for="(pattern, index) in hint.hint.supportedPatterns"
:key="index"
v-for="pattern in hint.hint.supportedPatterns"
:key="pattern"
>
<code>{{ pattern }}</code>
</li>
Expand Down
15 changes: 15 additions & 0 deletions frontend/taskdeck-web/src/tests/utils/chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ describe('extractParseHint', () => {
expect(extractParseHint(content)).toBeNull()
})

it('de-duplicates repeated supportedPatterns so list keys stay unique', () => {
const payload = {
supportedPatterns: ['create card "title"', 'archive card {id}', 'create card "title"'],
exampleInstruction: 'create card "test"',
closestPattern: 'create card "title"',
detectedIntent: null,
}
const content = `Text[PARSE_HINT]${JSON.stringify(payload)}`

const result = extractParseHint(content)

expect(result).not.toBeNull()
expect(result!.hint.supportedPatterns).toEqual(['create card "title"', 'archive card {id}'])
})

it('trims trailing whitespace from text before hint', () => {
const payload = {
supportedPatterns: ['create card "title"'],
Expand Down
3 changes: 3 additions & 0 deletions frontend/taskdeck-web/src/utils/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export function extractParseHint(content: string): ParsedHintMessage | null {
if (!hint.supportedPatterns || !Array.isArray(hint.supportedPatterns)) {
return null
}
// De-duplicate so the rendered list (keyed on the pattern string) can never
// emit duplicate Vue keys regardless of what an LLM-produced payload contains.
hint.supportedPatterns = [...new Set(hint.supportedPatterns)]
return { textBeforeHint, hint }
} catch {
return null
Expand Down
Loading