mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
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) <noreply@anthropic.com> 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) <noreply@anthropic.com> 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGq2deVkz8qnpfssssKzn7 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5d5ad4e897
commit
9074de25ea
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<Set<number>>(new Set())
|
||||
|
||||
@@ -119,7 +131,7 @@
|
||||
{:else}
|
||||
<div class={twMerge('text-sm py-1 px-2', message.role === 'tool' && 'text-primary py-0')}>
|
||||
{#if message.role === 'assistant'}
|
||||
<div class="px-[1px]"><AssistantMessage {message} /></div>
|
||||
<div class="px-[1px]"><AssistantMessage {message} workspace={messageWorkspace} /></div>
|
||||
{:else if message.role === 'tool'}
|
||||
<div class="px-[1px]"
|
||||
><ToolExecutionDisplay message={message as ToolDisplayMessage} /></div
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { thinkingPreferences } from './thinkingPreferences.svelte'
|
||||
import CodeDisplay from './script/CodeDisplay.svelte'
|
||||
import LinkRenderer from './LinkRenderer.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
extractCandidatePaths,
|
||||
remarkWindmillPaths,
|
||||
@@ -16,9 +15,12 @@
|
||||
|
||||
interface Props {
|
||||
message: DisplayMessage
|
||||
// Workspace the message's paths are resolved against: the one the chat
|
||||
// operates on, which is not always the one being navigated.
|
||||
workspace: string | undefined
|
||||
}
|
||||
|
||||
let { message }: Props = $props()
|
||||
let { message, workspace }: Props = $props()
|
||||
|
||||
const reasoning = $derived(
|
||||
message.role === 'assistant' ? message.reasoning?.trim() || undefined : undefined
|
||||
@@ -69,12 +71,11 @@
|
||||
// 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)
|
||||
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]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 @@
|
||||
<div class="truncate text-xs font-semibold text-primary">{card.title}</div>
|
||||
<div class="truncate text-2xs text-secondary">{card.subtitle}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
title={action.label}
|
||||
loading={runningActionId === action.id}
|
||||
disabled={runningActionId !== undefined && runningActionId !== action.id}
|
||||
startIcon={{ icon: card.buttonIcon }}
|
||||
onClick={() => handleAction(action)}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
<!-- open_created_resource is serviced solely by the docked chat's drawers, so
|
||||
elsewhere the card stands alone as a record of what the tool created. -->
|
||||
{#if hasToolDisplayActionHandler(action.type)}
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
title={action.label}
|
||||
loading={runningActionId === action.id}
|
||||
disabled={runningActionId !== undefined && runningActionId !== action.id}
|
||||
startIcon={{ icon: card.buttonIcon }}
|
||||
onClick={() => handleAction(action)}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -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<void> {
|
||||
const handler = toolDisplayActionHandlers[action.type]
|
||||
if (!handler) {
|
||||
|
||||
@@ -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.`
|
||||
<code>CodeDisplay</code> → <code>HighlightCode</code>), constrained to the chat panel width.
|
||||
</div>
|
||||
<div class="border border-border-light rounded-lg p-3 bg-surface" style="max-width: 420px;">
|
||||
<AssistantMessage message={chatMessage} />
|
||||
<AssistantMessage message={chatMessage} workspace={$workspaceStore} />
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="scrollbar" class="p-4">
|
||||
|
||||
Reference in New Issue
Block a user