From e52e3d1bf797fa23b61e4d708cd87fcf0b272e41 Mon Sep 17 00:00:00 2001 From: Omkar Tripathi <38633467+triggeredcode@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:41:54 +0530 Subject: [PATCH] fix(context): resolve ordered components (#47) --- packages/kit/src/outline/context.test.ts | 24 +++++++++++++++++++++++ packages/kit/src/outline/context.ts | 25 ++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/outline/context.test.ts b/packages/kit/src/outline/context.test.ts index 2421f0e..d23d330 100644 --- a/packages/kit/src/outline/context.test.ts +++ b/packages/kit/src/outline/context.test.ts @@ -117,6 +117,30 @@ describe("targeted document context", () => { expect(missing.text).toContain("TARGET_STATUS: missing b99"); }); + it("expands an explicitly ordered component without treating it as ambiguous", () => { + const source = doc( + fixture("table", "fx-table-1", "table-a"), + fixture("table", "fx-table-1", "table-b"), + fixture("table", "fx-table-1", "table-c"), + ); + + for (const [utterance, expected] of [ + ["Change a cell in the first table", "table-a"], + ["Change a cell in the second table", "table-b"], + ["Change a cell in the last table", "table-c"], + ] as const) { + const bundle = buildDocumentContext({ doc: source, utterance }); + expect(bundle.selection.fullBlockIds).toEqual([expected]); + expect(bundle.selection.ambiguities).toEqual([]); + } + + const ambiguous = buildDocumentContext({ doc: source, utterance: "Change the table" }); + expect(ambiguous.selection.fullBlockIds).toEqual([]); + expect(ambiguous.selection.ambiguities).toEqual([ + { kind: "table", blockIds: ["table-a", "table-b", "table-c"] }, + ]); + }); + it("expands document-wide requests and remains deterministic", () => { const source = sectioned(); const request = { doc: source, utterance: "Review the entire document" }; diff --git a/packages/kit/src/outline/context.ts b/packages/kit/src/outline/context.ts index 9ef354c..134813b 100644 --- a/packages/kit/src/outline/context.ts +++ b/packages/kit/src/outline/context.ts @@ -40,6 +40,12 @@ export interface DocumentContextRequest { const WHOLE_DOCUMENT = /\b(?:whole|entire)\s+(?:document|page|note|notes)\b|\b(?:all|every)\s+(?:section|sections|block|blocks|heading|headings|paragraph|paragraphs|table|tables|diagram|diagrams|chart|charts|item|items)\b|\b(?:summari[sz]e|rewrite|reorganize|review)\s+(?:the\s+)?(?:document|page|whole thing)\b/i; const COMPONENT_KINDS = ["table", "diagram", "chart"] as const; +const COMPONENT_ORDINALS: Record = { + first: 0, + second: 1, + third: 2, + fourth: 3, +}; const STOP_WORDS = new Set([ "about", "after", @@ -193,14 +199,29 @@ export function buildDocumentContext(request: DocumentContextRequest): DocumentC for (const kind of COMPONENT_KINDS) { if (!new RegExp(`\\b${kind}s?\\b`, "i").test(utterance)) continue; const candidates = index.components.filter((component) => component.kind === kind); + if (scope === "document") { + for (const candidate of candidates) roots.add(candidate.blockId); + continue; + } + const ordinalMatch = new RegExp( + `\\b(first|second|third|fourth|last)\\s+${kind}s?\\b`, + "i", + ).exec(utterance); + if (ordinalMatch) { + const word = ordinalMatch[1]?.toLowerCase() ?? ""; + const position = word === "last" ? candidates.length - 1 : COMPONENT_ORDINALS[word]; + const candidate = position === undefined ? undefined : candidates[position]; + if (candidate) roots.add(candidate.blockId); + else missingRefs.push(`${word} ${kind}`); + continue; + } const focused = candidates.find( (component) => component.blockId === request.cursorBlockId || roots.has(component.blockId) || recent.includes(component.blockId), ); - if (scope === "document") for (const candidate of candidates) roots.add(candidate.blockId); - else if (focused) roots.add(focused.blockId); + if (focused) roots.add(focused.blockId); else if (candidates.length === 1) roots.add(candidates[0]?.blockId ?? ""); else if (candidates.length > 1) ambiguities.push({ kind, blockIds: candidates.map((component) => component.blockId) });