From f6fcdb5599c28b4890d6f775f657bfafeea1d380 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 20 May 2026 12:00:16 +0200 Subject: [PATCH] feat: open ai chat path links in drawers (#9220) * feat(ai-chat): link workspace paths and show tool item references Detect Windmill paths (u/..., f/...) in assistant messages and render them as clickable pills with the right icon, resolved against a per- workspace cache. Tool execution headers now list the script/flow/app paths referenced in tool parameters as external links. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(ai-chat): linkify inline-code paths, refine pill styling - Inline-code spans whose value is exactly a Windmill path now render as a link pill (paths inside larger inline code or fenced blocks stay as code). - Tool-header chips moved to their own row to avoid overflow clipping when the title wraps. - Borderless pills, no default background (hover only), kind icons use the home-page palette (script blue, flow teal, app orange), and the external-link indicator only appears on hover. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(ai-chat): linkify variables/resources/triggers + inline drawer - Workspace item registry now also lists variables, resources, schedules, and all 10 trigger kinds; resource wins over variable on path collisions (Windmill auto-creates a companion variable for every resource). - Pill icons delegated to the canonical RowIcon component so each kind matches the home-page styling (script blue, flow teal, app orange, resource boxes, schedule calendar, etc.). - Pill href includes the hash fragment each list page already consumes (#/resource/, # for variables/schedules/triggers), so opening the link puts the user on the list page with the matching editor drawer already open. - For variable and resource pills, a hover-revealed side-panel button opens (or toggles closed) the editor drawer inline next to the chat, without navigating away. VariableEditor and ResourceEditorDrawer gain a closeDrawer() export and forward their close event so the host can drive toggling. Co-Authored-By: Claude Opus 4.7 (1M context) * refactor: simplify ai chat workspace item links * refactor: keep ai chat path linkification only * perf: avoid eager ai chat path cache loads * refactor: simplify ai chat path linking * feat: open ai chat path links in drawers * refactor: homogenize workspace item kinds * fix: toggle ai chat item drawer * refactor: trim ai chat path cache * fix: cancel ai chat drawer reopen --------- Co-authored-by: Guilhem Lemouel Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Ruben Fiszel --- frontend/package-lock.json | 2 + frontend/package.json | 2 + .../copilot/chat/AssistantMessage.svelte | 57 ++- .../chat/CreatedResourceActionDrawers.svelte | 18 +- .../copilot/chat/LinkRenderer.svelte | 74 +++- .../copilot/chat/ToolMessageActions.svelte | 4 +- .../src/lib/components/copilot/chat/shared.ts | 1 + .../copilot/chat/workspaceItems.svelte.ts | 306 ++++++++++++++++ .../copilot/chat/workspaceItems.test.ts | 342 ++++++++++++++++++ .../src/lib/components/offboarding-utils.ts | 6 + 10 files changed, 794 insertions(+), 18 deletions(-) create mode 100644 frontend/src/lib/components/copilot/chat/workspaceItems.svelte.ts create mode 100644 frontend/src/lib/components/copilot/chat/workspaceItems.test.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7f2c10aa6c..2ee59752f4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -51,6 +51,7 @@ "jszip": "^3.10.1", "lru-cache": "^11.1.0", "lucide-svelte": "^0.540.0", + "mdast-util-find-and-replace": "^3.0.2", "minimatch": "^10.0.1", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@=25.0.0", "monaco-languageclient": "10.6.0", @@ -71,6 +72,7 @@ "svelte-exmarkdown": "^5.0.0", "svelte-infinite-loading": "^1.4.0", "tailwind-merge": "^1.13.2", + "unist-util-visit": "^5.0.0", "vscode": "npm:@codingame/monaco-vscode-extension-api@=25.0.0", "vscode-languageclient": "~9.0.1", "vscode-uri": "~3.1.0", diff --git a/frontend/package.json b/frontend/package.json index c379388fd6..c7f0c548e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -124,6 +124,8 @@ "jszip": "^3.10.1", "lru-cache": "^11.1.0", "lucide-svelte": "^0.540.0", + "mdast-util-find-and-replace": "^3.0.2", + "unist-util-visit": "^5.0.0", "minimatch": "^10.0.1", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@=25.0.0", "monaco-languageclient": "10.6.0", diff --git a/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte b/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte index 7d39f97274..186306fa23 100644 --- a/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte +++ b/frontend/src/lib/components/copilot/chat/AssistantMessage.svelte @@ -4,12 +4,56 @@ import type { DisplayMessage } from './shared' import CodeDisplay from './script/CodeDisplay.svelte' import LinkRenderer from './LinkRenderer.svelte' + import { workspaceStore } from '$lib/stores' + import { + extractCandidatePaths, + remarkWindmillPaths, + workspaceItemRegistry + } from './workspaceItems.svelte' interface Props { message: DisplayMessage } let { message }: Props = $props() + + const candidatePaths = $derived(extractCandidatePaths(message.content)) + const rendererPlugin = { + renderer: { + pre: CodeDisplay, + a: LinkRenderer + } + } + + // Only populate the registry for messages that contain path-shaped tokens. The + // registry still dedups concurrent calls across messages and workspaces. + $effect(() => { + const ws = $workspaceStore + if (ws && candidatePaths.length > 0) workspaceItemRegistry.ensureLoaded(ws) + }) + + const plugins = $derived.by(() => { + const ws = $workspaceStore ?? '' + if (!ws || candidatePaths.length === 0) { + return [gfmPlugin(), rendererPlugin] + } + + if (!workspaceItemRegistry.isLoaded(ws)) { + return [gfmPlugin(), rendererPlugin] + } + + return [ + gfmPlugin(), + { + remarkPlugin: remarkWindmillPaths({ + resolve: (path) => workspaceItemRegistry.resolve(ws, path), + workspace: ws || undefined + }), + renderer: {} + }, + rendererPlugin + ] + })
- +
diff --git a/frontend/src/lib/components/copilot/chat/CreatedResourceActionDrawers.svelte b/frontend/src/lib/components/copilot/chat/CreatedResourceActionDrawers.svelte index 7d3b8a950c..b73c8dc6bc 100644 --- a/frontend/src/lib/components/copilot/chat/CreatedResourceActionDrawers.svelte +++ b/frontend/src/lib/components/copilot/chat/CreatedResourceActionDrawers.svelte @@ -81,6 +81,10 @@ azure: { label: 'Azure Event Grid trigger', load: () => import('$lib/components/triggers/azure/AzureTriggerEditorInner.svelte') + }, + email: { + label: 'Email trigger', + load: () => import('$lib/components/triggers/email/EmailTriggerEditorInner.svelte') } } @@ -139,13 +143,25 @@ throw new Error('Missing trigger kind') } + if (activeDrawer?.key === key && activeDrawer.path === action.path && drawer?.isOpen()) { + activeDrawer = undefined + editor = undefined + drawer.closeDrawer() + return + } + const config = drawerConfigs[key] const promise = activeDrawer?.key === key ? activeDrawer.promise : config.load() if (activeDrawer?.key !== key) { editor = undefined } - const request: ActiveDrawerState = { id: nextActiveDrawerId++, key, path: action.path, promise } + const request: ActiveDrawerState = { + id: nextActiveDrawerId++, + key, + path: action.path, + promise + } activeDrawer = request drawer?.openDrawer() diff --git a/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte b/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte index 87d131e03e..5d79251fd2 100644 --- a/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte +++ b/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte @@ -1,15 +1,81 @@ {#if href} - - {@render children?.()} - + {#if wmKind} + + + + + + {@render children?.()} + + + + + {#if drawerAction} +