nits(aichat): add keyboard navigation in context list (#6443)

* better flow module code peice

* better keyboard nav for availablecontextlist

* cleaning

* escape to close + cleaning

* nit tab handling

* fix module extraction

* remove code category

* fixes

* comment

* fix

* fix tool params display
This commit is contained in:
centdix
2025-08-23 03:50:17 +02:00
committed by GitHub
parent 919bf6bd5c
commit 4e8e938440
12 changed files with 320 additions and 172 deletions
+8 -2
View File
@@ -1237,7 +1237,13 @@
try {
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code ?? '', lang, automaticLayout, fixedOverflowWidgets, $relativeLineNumbers),
...editorConfig(
code ?? '',
lang,
automaticLayout,
fixedOverflowWidgets,
$relativeLineNumbers
),
model,
fontSize: !small ? 14 : 12,
lineNumbersMinChars,
@@ -1657,7 +1663,7 @@
files && model && untrack(() => onFileChanges())
})
$effect(() => {
editor?.updateOptions({
editor?.updateOptions({
lineNumbers: $relativeLineNumbers ? 'relative' : 'on'
})
})
@@ -150,7 +150,6 @@
addContextToSelection(element)
close()
}}
categorize
/>
</svelte:fragment>
</Popover>
@@ -3,37 +3,40 @@
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import type { FlowModule } from '$lib/gen/types.gen'
import { ContextIconMap, type ContextElement } from './context'
import { ArrowLeft, Diff, Database, Code, ChevronRight } from 'lucide-svelte'
import { ArrowLeft, Diff, Database, ChevronRight } from 'lucide-svelte'
interface Props {
availableContext: ContextElement[]
selectedContext: ContextElement[]
onSelect: (element: ContextElement) => void
setShowing?: (showing: boolean) => void
showAllAvailable?: boolean
stringSearch?: string
selectedIndex?: number
categorize?: boolean
onViewChange?: (newNumber: number) => void
}
const {
availableContext,
selectedContext,
onSelect,
setShowing,
showAllAvailable = false,
stringSearch = '',
selectedIndex = 0,
categorize = false
onViewChange
}: Props = $props()
// Current view state: 'categories' or specific category type
let currentView = $state<'categories' | 'diffs' | 'modules' | 'databases' | 'code'>('categories')
let currentView = $state<'categories' | 'diffs' | 'modules' | 'databases'>('categories')
// Selected index for keyboard navigation
let itemSelectedIndex = $state(0)
let categorySelectedIndex = $state(0)
// Category definitions
const categories = [
{ id: 'diffs', label: 'Diffs', icon: Diff },
{ id: 'modules', label: 'Modules', icon: BarsStaggered },
{ id: 'databases', label: 'Databases', icon: Database },
{ id: 'code', label: 'Code', icon: Code }
{ id: 'databases', label: 'Databases', icon: Database }
]
const filteredAvailableContext = $derived(
@@ -52,15 +55,13 @@
const grouped: Record<string, ContextElement[]> = {
diffs: [],
modules: [],
databases: [],
code: []
databases: []
}
filteredAvailableContext.forEach((context) => {
if (context.type === 'diff') grouped.diffs.push(context)
else if (context.type === 'flow_module') grouped.modules.push(context)
else if (context.type === 'db') grouped.databases.push(context)
else if (context.type === 'code') grouped.code.push(context)
})
return grouped
@@ -70,77 +71,140 @@
currentView !== 'categories' ? contextByCategory[currentView] : []
)
// Filter to only show categories with items
const availableCategories = $derived(
categories.filter((cat) => contextByCategory[cat.id].length > 0)
)
// Report view changes
$effect(() => {
if (onViewChange) {
if (currentView === 'categories') {
onViewChange(availableCategories.length)
} else {
onViewChange(currentCategoryItems.length + 1)
}
}
})
function handleCategoryClick(categoryId: string) {
currentView = categoryId as typeof currentView
}
function handleBackClick() {
currentView = 'categories'
itemSelectedIndex = 0
}
function handleKeyDown(e: KeyboardEvent) {
if (stringSearch.length > 0) {
// Navigation in search view (flat list)
if (e.key === 'ArrowDown') {
e.preventDefault()
e.stopPropagation()
if (filteredAvailableContext.length > 0) {
itemSelectedIndex = (itemSelectedIndex + 1) % filteredAvailableContext.length
}
} else if (e.key === 'ArrowUp') {
e.preventDefault()
e.stopPropagation()
if (filteredAvailableContext.length > 0) {
itemSelectedIndex =
(itemSelectedIndex - 1 + filteredAvailableContext.length) %
filteredAvailableContext.length
}
} else if (e.key === 'Enter' || e.key === 'Tab') {
if (e.key === 'Tab') e.preventDefault()
e.stopPropagation()
const selectedItem = filteredAvailableContext[itemSelectedIndex]
if (selectedItem) {
onSelect(selectedItem)
}
}
} else if (currentView === 'categories') {
// Navigation in categories view
if (e.key === 'ArrowDown') {
e.preventDefault()
e.stopPropagation()
categorySelectedIndex = (categorySelectedIndex + 1) % availableCategories.length
} else if (e.key === 'ArrowUp') {
e.preventDefault()
e.stopPropagation()
categorySelectedIndex =
(categorySelectedIndex - 1 + availableCategories.length) % availableCategories.length
} else if (e.key === 'Enter' || e.key === 'ArrowRight' || e.key === 'Tab') {
e.preventDefault()
e.stopPropagation()
const selectedCategory = availableCategories[categorySelectedIndex]
if (selectedCategory) {
handleCategoryClick(selectedCategory.id)
}
} else if (e.key === 'Escape' || e.key === 'ArrowLeft') {
e.preventDefault()
e.stopPropagation()
setShowing?.(false)
}
} else {
// Navigation in category items view
if (e.key === 'ArrowDown') {
e.preventDefault()
e.stopPropagation()
if (currentCategoryItems.length > 0) {
itemSelectedIndex = (itemSelectedIndex + 1) % currentCategoryItems.length
}
} else if (e.key === 'ArrowUp') {
e.preventDefault()
e.stopPropagation()
if (currentCategoryItems.length > 0) {
itemSelectedIndex =
(itemSelectedIndex - 1 + currentCategoryItems.length) % currentCategoryItems.length
}
} else if (e.key === 'Enter' || e.key === 'Tab') {
if (e.key === 'Tab') e.preventDefault()
e.stopPropagation()
const selectedItem = currentCategoryItems[itemSelectedIndex]
if (selectedItem) {
onSelect(selectedItem)
currentView = 'categories' // Go back to categories after selection
}
} else if (e.key === 'ArrowLeft' || e.key === 'Escape') {
e.preventDefault()
e.stopPropagation()
handleBackClick()
}
}
}
// Listen for keyboard events
$effect(() => {
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
})
$effect(() => {
if (stringSearch.length > 0) {
itemSelectedIndex = 0
}
})
</script>
<div class="flex flex-col gap-1 text-tertiary text-xs p-1 pr-0 min-w-24 max-h-48 overflow-y-scroll">
{#if categorize}
{#if currentView === 'categories'}
{#each categories as category}
{@const itemCount = contextByCategory[category.id].length}
{@const Icon = category.icon}
{#if itemCount > 0}
<button
class="hover:bg-surface-hover rounded-md p-1 pr-0 text-left flex flex-row gap-1 items-center font-normal transition-colors"
onclick={() => handleCategoryClick(category.id)}
>
<Icon size={16} />
<span class="flex-1">{category.label}</span>
<ChevronRight size={16} />
</button>
{/if}
{/each}
{#if categories.every((cat) => contextByCategory[cat.id].length === 0)}
<div class="text-center text-tertiary text-xs py-2">No available context</div>
{/if}
{:else}
<!-- Category items view -->
<button
class="hover:bg-surface-hover rounded-md text-left flex flex-row gap-1 items-center font-normal transition-colors mb-1"
onclick={handleBackClick}
>
<ArrowLeft size={12} />
<span class="text-xs">Go back</span>
</button>
{#if currentCategoryItems.length === 0}
<div class="text-center text-tertiary text-xs py-2">No items in this category</div>
{:else}
{#each currentCategoryItems as element}
{@const Icon = ContextIconMap[element.type]}
<button
class="hover:bg-surface-hover rounded-md p-1 text-left flex flex-row gap-1 items-center font-normal transition-colors"
onclick={() => {
onSelect(element)
currentView = 'categories' // Go back to categories after selection
}}
>
{#if element.type === 'flow_module'}
<FlowModuleIcon module={element as FlowModule} size={16} />
{:else if Icon}
<Icon size={16} />
{/if}
<span class="truncate">
{element.type === 'diff' || element.type === 'flow_module'
? element.title.replace(/_/g, ' ')
: element.title}
</span>
</button>
{/each}
{/if}
{/if}
{:else}
<div
class="flex flex-col gap-1 text-tertiary text-xs p-1 pr-0 min-w-24 max-h-48 overflow-y-scroll"
onmousedown={(e) =>
// avoids triggering onblur on the textinput and closing the tooltip
e.preventDefault()}
role="listbox"
tabindex={0}
>
{#if stringSearch.length > 0}
<!-- Search view - show flat list -->
{#each filteredAvailableContext as element, i}
{@const Icon = ContextIconMap[element.type]}
<button
class="hover:bg-surface-hover rounded-md p-1 text-left flex flex-row gap-1 items-center font-normal transition-colors {i ===
selectedIndex
itemSelectedIndex
? 'bg-surface-hover'
: ''}"
onclick={() => {
@@ -159,5 +223,63 @@
</span>
</button>
{/each}
{#if filteredAvailableContext.length === 0}
<div class="text-center text-tertiary text-xs py-2">No matching context</div>
{/if}
{:else if currentView === 'categories'}
<!-- Categories view -->
{#each availableCategories as category, i}
{@const Icon = category.icon}
<button
class="hover:bg-surface-hover rounded-md p-1 pr-0 text-left flex flex-row gap-1 items-center font-normal transition-colors {i ===
categorySelectedIndex
? 'bg-surface-hover'
: ''}"
onclick={() => handleCategoryClick(category.id)}
>
<Icon size={16} />
<span class="flex-1">{category.label}</span>
<ChevronRight size={16} />
</button>
{/each}
{#if availableCategories.length === 0}
<div class="text-center text-tertiary text-xs py-2">No available context</div>
{/if}
{:else}
<!-- Category items view -->
<button
class="hover:bg-surface-hover rounded-md text-left flex flex-row gap-1 items-center font-normal transition-colors mb-1"
onclick={handleBackClick}
>
<ArrowLeft size={12} />
<span class="text-xs">Go back</span>
</button>
{#if currentCategoryItems.length === 0}
<div class="text-center text-tertiary text-xs py-2">No items in this category</div>
{:else}
{#each currentCategoryItems as element, i}
{@const Icon = ContextIconMap[element.type]}
<button
class="hover:bg-surface-hover rounded-md p-1 text-left flex flex-row gap-1 items-center font-normal transition-colors {i ===
itemSelectedIndex
? 'bg-surface-hover'
: ''}"
onclick={() => {
onSelect(element)
currentView = 'categories' // Go back to categories after selection
}}
>
{#if element.type === 'flow_module'}
<FlowModuleIcon module={element as FlowModule} size={16} />
{:else if Icon}
<Icon size={16} />
{/if}
<span class="truncate">
{element.type === 'diff' ? element.title.replace(/_/g, ' ') : element.title}
</span>
</button>
{/each}
{/if}
{/if}
</div>
@@ -42,7 +42,7 @@
<button onclick={isDeletable ? onDelete : undefined} class:cursor-default={!isDeletable}>
{#if showDelete && isDeletable}
<X size={16} />
{:else if contextElement.type === 'flow_module'}
{:else if contextElement.type === 'flow_module' || contextElement.type === 'flow_module_code_piece'}
<FlowModuleIcon module={contextElement as FlowModule} size={16} />
{:else}
{@const SvelteComponent = icon}
@@ -50,7 +50,7 @@
{/if}
</button>
<span class="truncate">
{contextElement.type === 'diff' || contextElement.type === 'flow_module'
{contextElement.type === 'diff'
? contextElement.title.replace(/_/g, ' ')
: contextElement.title}
</span>
@@ -78,7 +78,7 @@
<div class="text-tertiary">Not loaded yet</div>
{/if}
</div>
{:else if contextElement.type === 'code' || contextElement.type === 'code_piece' || contextElement.type === 'diff'}
{:else if contextElement.type === 'code' || contextElement.type === 'code_piece' || contextElement.type === 'diff' || contextElement.type === 'flow_module_code_piece'}
<div class="max-w-96 max-h-[300px] text-xs overflow-auto">
<HighlightCode
language={contextElement.lang}
@@ -2,7 +2,7 @@ import { ResourceService, type Flow, type ListResourceResponse, type ScriptLang
import { scriptLangToEditorLang } from '$lib/scripts'
import { SQLSchemaLanguages, type DBSchemas } from '$lib/stores'
import { diffLines } from 'diff'
import type { ContextElement } from './context'
import type { ContextElement, FlowModuleElement } from './context'
import type { FlowModule } from '$lib/gen'
import type { DisplayMessage } from './shared'
@@ -109,7 +109,7 @@ export default class ContextManager {
newAvailableContext.push({
type: 'flow_module',
id: module.id,
title: `module_[${module.id}]`,
title: `${module.id}`,
value: {
language: 'language' in module.value ? module.value.language : 'bunnative',
path: 'path' in module.value ? module.value.path : '',
@@ -290,24 +290,51 @@ export default class ContextManager {
}
addSelectedLinesToContext(lines: string, startLine: number, endLine: number, moduleId?: string) {
const title = moduleId ? `[${moduleId}] L${startLine}-L${endLine}` : `L${startLine}-L${endLine}`
const title = moduleId ? `${moduleId} L${startLine}-L${endLine}` : `L${startLine}-L${endLine}`
if (
!this.scriptOptions ||
this.selectedContext.find((c) => c.type === 'code_piece' && c.title === title)
this.selectedContext.find(
(c) =>
(c.type === 'code_piece' && c.title === title) ||
(c.type === 'flow_module_code_piece' && c.id === moduleId && c.title === title)
)
) {
return
}
this.selectedContext = [
...this.selectedContext,
{
type: 'code_piece',
title: title,
startLine,
endLine,
content: lines,
lang: this.scriptOptions.lang
if (moduleId) {
const module = [...this.availableContext, ...this.selectedContext].find(
(c) => c.type === 'flow_module' && c.id === moduleId
) as FlowModuleElement
if (!module) {
console.error('Module not found', moduleId)
return
}
]
this.selectedContext = [
...this.selectedContext,
{
type: 'flow_module_code_piece',
id: moduleId,
title: title,
startLine,
endLine,
content: lines,
lang: this.scriptOptions.lang,
value: module.value
}
]
} else {
this.selectedContext = [
...this.selectedContext,
{
type: 'code_piece',
title: title,
startLine,
endLine,
content: lines,
lang: this.scriptOptions.lang
}
]
}
}
setFixContext() {
@@ -38,7 +38,7 @@
let tooltipPosition = $state({ x: 0, y: 0 })
let textarea = $state<HTMLTextAreaElement | undefined>(undefined)
let tooltipElement = $state<HTMLDivElement | undefined>(undefined)
let selectedSuggestionIndex = $state(0)
let tooltipCurrentViewNumber = $state(0)
// Properties to copy for caret position calculation
const properties = [
@@ -182,27 +182,19 @@
showContextTooltip = false
}
async function updateTooltipPosition(
availableContext: ContextElement[],
showContextTooltip: boolean,
contextTooltipWord: string
) {
if (!textarea || !showContextTooltip) return
async function updateTooltipPosition(currentViewItemsNumber: number) {
if (!textarea) return
try {
const coords = getCaretCoordinates(textarea, textarea.selectionEnd)
const rect = textarea.getBoundingClientRect()
const filteredAvailableContext = availableContext.filter(
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
)
const itemHeight = 28 // Estimated height of one item + gap (Button: p-1(8px) + text-xs(16px) = 24px; Parent: gap-1(4px) = 28px)
const containerPadding = 8 // p-1 top + p-1 bottom = 4px + 4px = 8px
const maxHeight = 192 + containerPadding // max-h-48 (192px) + containerPadding (8px)
// Calculate uncapped height, subtract gap from last item as it's not needed
const numItems = filteredAvailableContext.length
const numItems = currentViewItemsNumber
let uncappedHeight =
numItems > 0 ? numItems * itemHeight - 4 + containerPadding : containerPadding
// Ensure height is at least containerPadding even if no items
@@ -270,68 +262,33 @@
} else {
showContextTooltip = false
contextTooltipWord = ''
selectedSuggestionIndex = 0
}
}
function handleKeyPress(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
if (contextTooltipWord) {
const filteredContext = availableContext.filter(
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
)
const contextElement = filteredContext[selectedSuggestionIndex]
if (contextElement) {
const isInSelectedContext = selectedContext.find(
(c) => c.title === contextElement.title && c.type === contextElement.type
)
// If the context element is already in the selected context and the last word in the instructions is the same as the context element title, send request
if (isInSelectedContext && value.split(' ').pop() === '@' + contextElement.title) {
onSendRequest()
return
}
handleContextSelection(contextElement)
} else if (contextTooltipWord === '@' && availableContext.length > 0) {
handleContextSelection(availableContext[0])
}
} else {
onSendRequest()
}
}
}
function handleKeyDown(e: KeyboardEvent) {
// Pass to parent first if provided
if (onKeyDown) {
onKeyDown(e)
}
if (!showContextTooltip) return
const filteredContext = availableContext.filter(
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
)
if (e.key === 'Tab') {
e.preventDefault()
const contextElement = filteredContext[selectedSuggestionIndex]
if (contextElement) {
handleContextSelection(contextElement)
if (showContextTooltip) {
// avoid new line after Enter in the tooltip
if (e.key === 'Enter') {
e.preventDefault()
}
return
}
if (e.key === 'ArrowDown') {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
selectedSuggestionIndex = (selectedSuggestionIndex + 1) % filteredContext.length
} else if (e.key === 'ArrowUp') {
e.preventDefault()
selectedSuggestionIndex =
(selectedSuggestionIndex - 1 + filteredContext.length) % filteredContext.length
onSendRequest()
}
}
$effect(() => {
updateTooltipPosition(availableContext, showContextTooltip, contextTooltipWord)
if (showContextTooltip) {
updateTooltipPosition(tooltipCurrentViewNumber)
}
})
export function focus() {
@@ -352,7 +309,6 @@
</div>
<textarea
bind:this={textarea}
onkeypress={handleKeyPress}
onkeydown={handleKeyDown}
bind:value
use:autosize
@@ -388,7 +344,12 @@
}}
showAllAvailable={true}
stringSearch={contextTooltipWord.slice(1)}
selectedIndex={selectedSuggestionIndex}
onViewChange={(newNumber) => {
tooltipCurrentViewNumber = newNumber
}}
setShowing={(showing) => {
showContextTooltip = showing
}}
/>
</div>
</Portal>
@@ -25,6 +25,14 @@
return obj
}
}
for (const key in obj) {
try {
const parsed = JSON.parse(obj[key])
obj[key] = parsed
} catch (e) {
console.error('Failed to parse JSON:', e)
}
}
return JSON.stringify(obj, null, 2)
} catch {
return String(obj)
@@ -81,7 +89,9 @@
<div
class="bg-surface-secondary border border-gray-200 dark:border-gray-700 rounded p-3 overflow-x-auto max-h-64 overflow-y-auto"
>
<pre class="text-2xs text-primary whitespace-pre-wrap">{formatJson(content)}</pre>
<pre class="text-2xs text-primary whitespace-pre-wrap"
>{formatJson($state.snapshot(content))}</pre
>
</div>
{:else}
<div
@@ -25,8 +25,8 @@
<!-- Collapsible Header -->
<button
class={twMerge(
"w-full p-3 bg-surface-secondary hover:bg-surface-hover transition-colors flex items-center justify-between text-left border-b border-gray-200 dark:border-gray-700",
message.needsConfirmation ? "opacity-80" : ""
'w-full p-3 bg-surface-secondary hover:bg-surface-hover transition-colors flex items-center justify-between text-left border-b border-gray-200 dark:border-gray-700',
message.needsConfirmation ? 'opacity-80' : ''
)}
onclick={() => (isExpanded = !isExpanded)}
disabled={!message.showDetails}
@@ -57,7 +57,7 @@
{#if isExpanded}
<div class="p-3 bg-surface space-y-3">
<!-- Parameters Section -->
<div class={message.needsConfirmation ? "opacity-80" : ""}>
<div class={message.needsConfirmation ? 'opacity-80' : ''}>
<ToolContentDisplay title="Parameters" content={message.parameters} />
</div>
@@ -48,7 +48,7 @@ export interface CodePieceElement {
lang: ScriptLang | 'bunnative'
}
export interface FlowModule {
export interface FlowModuleElement {
type: 'flow_module'
id: string
title: string
@@ -61,13 +61,20 @@ export interface FlowModule {
}
}
export interface FlowModuleCodePieceElement extends Omit<CodePieceElement, 'type'> {
type: 'flow_module_code_piece'
id: string
value: FlowModuleElement['value']
}
export type ContextElement = (
| CodeElement
| ErrorElement
| DBElement
| DiffElement
| CodePieceElement
| FlowModule
| FlowModuleElement
| FlowModuleCodePieceElement
) & {
deletable?: boolean
}
@@ -860,7 +860,7 @@ If the user wants a specific resource as step input, you should set the step val
export function prepareFlowUserMessage(
instructions: string,
flowAndSelectedId?: { flow: ExtendedOpenFlow; selectedId: string },
selectedContext?: ContextElement[]
selectedContext: ContextElement[] = []
): ChatCompletionUserMessageParam {
const flow = flowAndSelectedId?.flow
const selectedId = flowAndSelectedId?.selectedId
@@ -877,7 +877,7 @@ ${instructions}`
}
}
const codePieces = selectedContext?.filter((c) => c.type === 'code_piece') ?? []
const codePieces = selectedContext.filter((c) => c.type === 'flow_module_code_piece')
const flowModulesYaml = applyCodePiecesToFlowModules(codePieces, flow.value.modules)
let flowContent = `## FLOW:
@@ -4,7 +4,7 @@ import type {
ChatCompletionTool
} from 'openai/resources/chat/completions.mjs'
import { get } from 'svelte/store'
import type { CodePieceElement, ContextElement } from './context'
import type { CodePieceElement, ContextElement, FlowModuleCodePieceElement } from './context'
import { copilotSessionModel, workspaceStore } from '$lib/stores'
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
import type { FunctionParameters } from 'openai/resources/shared.mjs'
@@ -23,6 +23,24 @@ export interface ContextStringResult {
hasFlowModule: boolean
}
export const extractAllModules = (modules: FlowModule[]): FlowModule[] => {
return modules.flatMap((m) => {
if (m.value.type === 'forloopflow' || m.value.type === 'whileloopflow') {
return [m, ...extractAllModules(m.value.modules)]
}
if (m.value.type === 'branchall') {
return [m, ...extractAllModules(m.value.branches.flatMap((b) => b.modules))]
}
if (m.value.type === 'branchone') {
return [
m,
...extractAllModules([...m.value.branches.flatMap((b) => b.modules), ...m.value.default])
]
}
return [m]
})
}
export const findModuleById = (modules: FlowModule[], moduleId: string): FlowModule | undefined => {
for (const module of modules) {
if (module.id === moduleId) {
@@ -68,22 +86,16 @@ const applyCodePieceToCodeContext = (codePieces: CodePieceElement[], codeContext
}
export function applyCodePiecesToFlowModules(
codePieces: CodePieceElement[],
codePieces: FlowModuleCodePieceElement[],
flowModules: FlowModule[]
): string {
// Parse code piece titles to extract module IDs
// Format: "[id] L3-L5"
const moduleCodePieces = new Map<string, CodePieceElement[]>()
const moduleCodePieces = new Map<string, FlowModuleCodePieceElement[]>()
for (const codePiece of codePieces) {
const match = codePiece.title.match(/\[([^\]]+)\]\s+L\d+-L\d+/)
if (match) {
const moduleId = match[1]
if (!moduleCodePieces.has(moduleId)) {
moduleCodePieces.set(moduleId, [])
}
moduleCodePieces.get(moduleId)!.push(codePiece)
const moduleId = codePiece.id
if (!moduleCodePieces.has(moduleId)) {
moduleCodePieces.set(moduleId, [])
}
moduleCodePieces.get(moduleId)!.push(codePiece)
}
// Clone modules to avoid mutation
@@ -93,7 +105,10 @@ export function applyCodePiecesToFlowModules(
for (const [moduleId, pieces] of moduleCodePieces) {
const module = findModuleById(modifiedModules, moduleId)
if (module && module.value.type === 'rawscript' && module.value.content) {
module.value.content = applyCodePieceToCodeContext(pieces, module.value.content)
module.value.content = applyCodePieceToCodeContext(
pieces as unknown as CodePieceElement[],
module.value.content
)
}
}
@@ -19,6 +19,7 @@
import type { ModulesTestStates } from '../modulesTest.svelte'
import type { StateStore } from '$lib/utils'
import type { FlowOptions } from '../copilot/chat/ContextManager.svelte'
import { extractAllModules } from '../copilot/chat/shared'
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
interface Props {
@@ -110,7 +111,7 @@
lastDeployedFlow: savedFlow,
lastSavedFlow: savedFlow?.draft,
path: savedFlow?.path,
modules: flowStore.val.value.modules
modules: extractAllModules(flowStore.val.value.modules)
}
aiChatManager.flowOptions = options
})