Skip to content

Commit 68befe0

Browse files
committed
fix(chat): unique table selection labels + correct file-selection icon
Cursor: - buildTableSelectionLabel now appends a short deterministic key (selectionKey over the row/column ids) so two distinct same-size selections don't share a label; guard insertContextChip against an already-selected label so a duplicate never inserts an orphan @token with no backing context. - file_selection chip icon strips the `:line` suffix before getDocumentIcon so it reads the real extension (`md`, not `md:12-40`) instead of the generic glyph.
1 parent 6a07cde commit 68befe0

5 files changed

Lines changed: 87 additions & 12 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@sim/emcn/icons'
1313
import { AgentSkillsIcon, McpIcon } from '@/components/icons'
1414
import { getDocumentIcon } from '@/components/icons/document-icons'
15+
import { fileNameFromSelectionLabel } from '@/lib/copilot/chat/selection-context'
1516
import type { ChatContextKind, ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
1617
import { getBareIconStyle } from '@/blocks/icon-color'
1718
import { getBlockRegistry } from '@/blocks/registry'
@@ -89,7 +90,9 @@ export const CHAT_CONTEXT_KIND_REGISTRY: Record<ChatContextKind, ChatContextKind
8990
file_selection: {
9091
label: 'File selection',
9192
renderIcon: ({ context, className }) => {
92-
const FileDocIcon = getDocumentIcon('', context.label)
93+
// Strip the `:line` suffix so `getDocumentIcon` reads the real extension
94+
// (e.g. `md`, not `md:12-40`) and shows the correct file glyph.
95+
const FileDocIcon = getDocumentIcon('', fileNameFromSelectionLabel(context.label))
9396
return <FileDocIcon className={className} />
9497
},
9598
},

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
useMentionTokens,
2222
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks'
2323
import {
24+
isContextAlreadySelected,
2425
restoreSkillTriggerText,
2526
SKILL_CHIP_TRIGGER,
2627
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
@@ -470,6 +471,13 @@ export function usePromptEditor({
470471
*/
471472
const insertContextChip = useCallback(
472473
(context: ChatContext) => {
474+
// A chip's `@label` token must be unique — `addContext` dedupes a matching
475+
// label, so inserting a token for an already-present context would orphan
476+
// it (a second token with no backing context). Skip and just focus.
477+
if (isContextAlreadySelected(context, contextManagementRef.current.selectedContexts)) {
478+
textareaRef.current?.focus()
479+
return
480+
}
473481
const textarea = textareaRef.current
474482
if (textarea) {
475483
const currentValue = valueRef.current

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
buildTableSelectionLabel,
1616
MAX_TABLE_SELECTION_COLUMNS,
1717
MAX_TABLE_SELECTION_ROWS,
18+
selectionKey,
1819
} from '@/lib/copilot/chat/selection-context'
1920
import { captureEvent } from '@/lib/posthog/client'
2021
import type {
@@ -2888,7 +2889,12 @@ export function TableGrid({
28882889
attachSelectionContextToClipboard(e.clipboardData, {
28892890
kind: 'table_selection',
28902891
tableId,
2891-
label: buildTableSelectionLabel(tableNameRef.current, rowIds.length),
2892+
label: buildTableSelectionLabel(
2893+
tableNameRef.current,
2894+
rowIds.length,
2895+
undefined,
2896+
selectionKey(rowIds)
2897+
),
28922898
rowIds,
28932899
})
28942900
}
@@ -2964,7 +2970,8 @@ export function TableGrid({
29642970
label: buildTableSelectionLabel(
29652971
tableNameRef.current,
29662972
rangeRowIds.length,
2967-
columnIds?.length
2973+
columnIds?.length,
2974+
selectionKey([...rangeRowIds, ...(columnIds ?? [])])
29682975
),
29692976
rowIds: rangeRowIds,
29702977
...(columnIds ? { columnIds } : {}),
@@ -3745,7 +3752,12 @@ export function TableGrid({
37453752
addToChat({
37463753
kind: 'table_selection',
37473754
tableId,
3748-
label: buildTableSelectionLabel(tableData?.name ?? 'Table', rowIds.length, columnIds?.length),
3755+
label: buildTableSelectionLabel(
3756+
tableData?.name ?? 'Table',
3757+
rowIds.length,
3758+
columnIds?.length,
3759+
selectionKey([...rowIds, ...(columnIds ?? [])])
3760+
),
37493761
rowIds,
37503762
...(columnIds && columnIds.length > 0 ? { columnIds } : {}),
37513763
})

apps/sim/lib/copilot/chat/selection-context.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5-
import { buildFileSelectionLabel, buildTableSelectionLabel } from './selection-context'
5+
import {
6+
buildFileSelectionLabel,
7+
buildTableSelectionLabel,
8+
fileNameFromSelectionLabel,
9+
selectionKey,
10+
tableNameFromSelectionLabel,
11+
} from './selection-context'
612

713
describe('buildFileSelectionLabel', () => {
814
it('renders a line range', () => {
@@ -32,4 +38,32 @@ describe('buildTableSelectionLabel', () => {
3238
expect(buildTableSelectionLabel('Sales', 5, 3)).toBe('Sales (5 rows, 3 cols)')
3339
expect(buildTableSelectionLabel('Sales', 2, 1)).toBe('Sales (2 rows, 1 col)')
3440
})
41+
42+
it('appends the disambiguation key when provided', () => {
43+
expect(buildTableSelectionLabel('Sales', 2, undefined, 'k3f9')).toBe('Sales (2 rows #k3f9)')
44+
expect(buildTableSelectionLabel('Sales', 2, 3, 'k3f9')).toBe('Sales (2 rows, 3 cols #k3f9)')
45+
})
46+
})
47+
48+
describe('selectionKey', () => {
49+
it('is deterministic and order-independent', () => {
50+
expect(selectionKey(['r1', 'r2', 'r3'])).toBe(selectionKey(['r3', 'r1', 'r2']))
51+
})
52+
53+
it('differs for distinct id sets of the same size', () => {
54+
expect(selectionKey(['r1', 'r2'])).not.toBe(selectionKey(['r3', 'r4']))
55+
})
56+
})
57+
58+
describe('selection-label name recovery', () => {
59+
it('strips the line suffix from a file selection label', () => {
60+
expect(fileNameFromSelectionLabel('notes.md:12-40')).toBe('notes.md')
61+
expect(fileNameFromSelectionLabel('notes.md')).toBe('notes.md')
62+
})
63+
64+
it('strips rows/cols/key suffix from a table selection label', () => {
65+
expect(tableNameFromSelectionLabel('Sales (2 rows)')).toBe('Sales')
66+
expect(tableNameFromSelectionLabel('Sales (2 rows, 3 cols #k3f9)')).toBe('Sales')
67+
expect(tableNameFromSelectionLabel('Sales (5 rows #ab12)')).toBe('Sales')
68+
})
3569
})

apps/sim/lib/copilot/chat/selection-context.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,37 @@ export function buildFileSelectionLabel(
5151
}
5252

5353
/**
54-
* Builds the chip label for a table selection, e.g. `Sales (5 rows)` or, for a
55-
* cell range, `Sales (5 rows, 3 cols)`. ASCII-only for the same reason as
56-
* {@link buildFileSelectionLabel}.
54+
* Short, deterministic key for a set of ids — same ids (any order) yield the
55+
* same key. Used to disambiguate table-selection labels so two distinct
56+
* selections of the same size don't collapse to one chip (chips are keyed by
57+
* their `@label`, and a collision would drop the second context).
58+
*/
59+
export function selectionKey(ids: string[]): string {
60+
const joined = [...ids].sort().join(',')
61+
let hash = 0
62+
for (let i = 0; i < joined.length; i++) {
63+
hash = (Math.imul(hash, 31) + joined.charCodeAt(i)) | 0
64+
}
65+
return (hash >>> 0).toString(36)
66+
}
67+
68+
/**
69+
* Builds the chip label for a table selection, e.g. `Sales (5 rows · k3f9)` or,
70+
* for a cell range, `Sales (5 rows, 3 cols · k3f9)`. The trailing `key` (from
71+
* {@link selectionKey}) keeps distinct same-size selections from sharing a
72+
* label. ASCII-only for the same reason as {@link buildFileSelectionLabel}.
5773
*/
5874
export function buildTableSelectionLabel(
5975
tableName: string,
6076
rowCount: number,
61-
columnCount?: number
77+
columnCount?: number,
78+
key?: string
6279
): string {
6380
const rows = `${rowCount} ${rowCount === 1 ? 'row' : 'rows'}`
64-
if (!columnCount) return `${tableName} (${rows})`
81+
const suffix = key ? ` #${key}` : ''
82+
if (!columnCount) return `${tableName} (${rows}${suffix})`
6583
const cols = `${columnCount} ${columnCount === 1 ? 'col' : 'cols'}`
66-
return `${tableName} (${rows}, ${cols})`
84+
return `${tableName} (${rows}, ${cols}${suffix})`
6785
}
6886

6987
/**
@@ -82,5 +100,5 @@ export function fileNameFromSelectionLabel(label: string): string {
82100
* builder for the same reason as {@link fileNameFromSelectionLabel}.
83101
*/
84102
export function tableNameFromSelectionLabel(label: string): string {
85-
return label.replace(/\s*\(\d+ rows?(?:, \d+ cols?)?\)$/, '')
103+
return label.replace(/\s*\(\d+ rows?(?:, \d+ cols?)?(?: #[0-9a-z]+)?\)$/, '')
86104
}

0 commit comments

Comments
 (0)