From 9074de25ea730ca02653c9a2e2b8b99eda6f3137 Mon Sep 17 00:00:00 2001 From: AlexRV12 <71396855+AlexRV12@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:27:24 +0200 Subject: [PATCH] fix: resolve chat path links against the session's operating workspace (#10924) * fix: resolve chat path links against the session's operating workspace Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RGq2deVkz8qnpfssssKzn7 * fix: hide the chat link drawer button where nothing can open it Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RGq2deVkz8qnpfssssKzn7 * fix: hide the chat tool card open button where nothing can open it Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RGq2deVkz8qnpfssssKzn7 --------- Co-authored-by: Claude Opus 5 (1M context) --- .../copilot/chat/AIChatManager.svelte.ts | 4 +-- .../copilot/chat/AIChatMessage.svelte | 14 ++++++++- .../copilot/chat/AssistantMessage.svelte | 11 ++++--- .../copilot/chat/LinkRenderer.svelte | 12 +++++-- .../copilot/chat/ToolMessageActions.svelte | 31 ++++++++++++------- .../chat/createdResourceActions.svelte.ts | 9 ++++++ frontend/src/routes/kitchen_sink/+page.svelte | 3 +- 7 files changed, 61 insertions(+), 23 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index c1a4b32d3c..61ed493a66 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -713,8 +713,8 @@ export class AIChatManager { workspaceResolver: (() => string | undefined) | undefined = undefined // The workspace every workspace-scoped chat action targets — skills, tool - // loop, logging, user-message context, and commit. Session-resolved when a - // resolver is set, else the globally-active workspace. + // loop, logging, user-message context, message rendering, and commit. + // Session-resolved when a resolver is set, else the globally-active workspace. get operatingWorkspace(): string | undefined { return this.workspaceResolver?.() ?? get(workspaceStore) } diff --git a/frontend/src/lib/components/copilot/chat/AIChatMessage.svelte b/frontend/src/lib/components/copilot/chat/AIChatMessage.svelte index 61d9a47966..12553be961 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatMessage.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatMessage.svelte @@ -13,9 +13,21 @@ import { messageDraft, segments } from './chatDraft' import { lineCountLabel } from './pasteTokens' import ExpandableImage from '$lib/components/common/image/ExpandableImage.svelte' + import { workspaceStore } from '$lib/stores' const aiChatManager = getAiChatManager() + // Paths in a message name items the chat's tools reach, so they resolve against the + // operating workspace, never `workspaceStore`: a fork session leaves the store on the + // navigated workspace, where a fork-only item resolves to nothing and the rest resolve + // to a different copy. + const messageWorkspace = $derived.by(() => { + // Registers the dependency that `operatingWorkspace`'s own untracked + // `get(workspaceStore)` cannot. + void $workspaceStore + return aiChatManager.operatingWorkspace + }) + // Per-message expand/collapse state for paste chips shown in the bubble. let expandedPastes = $state>(new Set()) @@ -119,7 +131,7 @@ {:else}
{#if message.role === 'assistant'} -
+
{:else if message.role === 'tool'}
{ - const ws = $workspaceStore - if (ws && candidatePaths.length > 0) workspaceItemRegistry.ensureLoaded(ws) + if (workspace && candidatePaths.length > 0) workspaceItemRegistry.ensureLoaded(workspace) }) const plugins = $derived.by(() => { - const ws = $workspaceStore ?? '' + const ws = workspace ?? '' if (!ws || candidatePaths.length === 0) { return [gfmPlugin(), rendererPlugin] } diff --git a/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte b/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte index 5d79251fd2..491c2e11d5 100644 --- a/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte +++ b/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte @@ -3,7 +3,10 @@ import { ExternalLink, PanelRight } from 'lucide-svelte' import { Button } from '$lib/components/common' import RowIcon from '$lib/components/common/table/RowIcon.svelte' - import { runToolDisplayAction } from './createdResourceActions.svelte' + import { + hasToolDisplayActionHandler, + runToolDisplayAction + } from './createdResourceActions.svelte' import { workspaceItemAction, type WindmillItemKind, @@ -27,7 +30,12 @@ title }: Props = $props() - const drawerAction = $derived(workspaceItemAction(wmKind, wmPath, wmTargetKind)) + // The drawers ride with the docked chat, so a surface can render this pill with nothing + // able to open one. + const drawerAction = $derived.by(() => { + const action = workspaceItemAction(wmKind, wmPath, wmTargetKind) + return action && hasToolDisplayActionHandler(action.type) ? action : undefined + }) async function openDrawer(event?: Event) { event?.preventDefault() diff --git a/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte b/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte index 02612492a5..db2a0e22c4 100644 --- a/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte @@ -28,7 +28,10 @@ import MqttIcon from '$lib/components/icons/MqttIcon.svelte' import AmqpIcon from '$lib/components/icons/AmqpIcon.svelte' import NatsIcon from '$lib/components/icons/NatsIcon.svelte' - import { runToolDisplayAction } from './createdResourceActions.svelte' + import { + hasToolDisplayActionHandler, + runToolDisplayAction + } from './createdResourceActions.svelte' import type { CreatedResourceTriggerKind, ToolDisplayAction } from './shared' interface Props { @@ -122,17 +125,21 @@
{card.title}
{card.subtitle}
- + + {#if hasToolDisplayActionHandler(action.type)} + + {/if} {/each} diff --git a/frontend/src/lib/components/copilot/chat/createdResourceActions.svelte.ts b/frontend/src/lib/components/copilot/chat/createdResourceActions.svelte.ts index 725385d3ee..bd0f3b3d0e 100644 --- a/frontend/src/lib/components/copilot/chat/createdResourceActions.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/createdResourceActions.svelte.ts @@ -25,6 +25,15 @@ export function registerToolDisplayActionHandler( } } +/** + * Reactive: reads the `$state` registry, so a component re-renders when a page mounts or + * unmounts its handler. Offering an action without checking this yields an affordance whose + * only outcome is the unavailable-action toast. + */ +export function hasToolDisplayActionHandler(type: ToolDisplayAction['type']): boolean { + return toolDisplayActionHandlers[type] !== undefined +} + export async function runToolDisplayAction(action: ToolDisplayAction): Promise { const handler = toolDisplayActionHandlers[action.type] if (!handler) { diff --git a/frontend/src/routes/kitchen_sink/+page.svelte b/frontend/src/routes/kitchen_sink/+page.svelte index 8681fb78da..a001da222f 100644 --- a/frontend/src/routes/kitchen_sink/+page.svelte +++ b/frontend/src/routes/kitchen_sink/+page.svelte @@ -9,6 +9,7 @@ import type { DisplayMessage } from '$lib/components/copilot/chat/shared' import DraggableTabs, { type TabItem } from '$lib/components/common/tabs/DraggableTabs.svelte' import { Globe } from 'lucide-svelte' + import { workspaceStore } from '$lib/stores' let tab = $state('button') @@ -195,7 +196,7 @@ That's the full round-trip.` CodeDisplayHighlightCode), constrained to the chat panel width.
- +