Files
windmill/frontend/src/lib/components/copilot/chat/AssistantFilesSection.svelte
T
fda7b3f086 feat(ai-sessions): replace the context panel with an assistant settings modal (#10919)
* feat(ai-chat): make reusable skills ai_skill resources you select per workspace

* chore: pin the ee ref to the skill telemetry counters

* fix: address review findings on skill authoring, import and migration

* fix: enforce skill selection in read_skill and stop imports clobbering resources

* feat: carry format_extension from the hub into synced resource types

* fix: let an edit set or clear a resource type's format_extension

* fix: regenerate the sqlx cache and close the review round findings

* fix: close the round-2 findings on folder ACLs, cached sync and truncation

* refactor: make the skills migration non-destructive and use design-system inputs

* feat(ai-sessions): add a context panel listing what the chat can use

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmdCVM1ZvTcpatv78jN8Ed

* fix: track the prompt rebuild signal and trim the review round's nits

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmdCVM1ZvTcpatv78jN8Ed

* fix: keep the panel from perturbing an in-flight turn

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmdCVM1ZvTcpatv78jN8Ed

* fix: count a folder by its readable children

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmdCVM1ZvTcpatv78jN8Ed

* feat(ai-sessions): replace the context panel with an assistant settings modal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* feat(ai-sessions): page-based MCP editing and fuzzy tool search

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* feat(ai-sessions): tool detail page and a shared list row

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* feat(ai-sessions): add a files & folders section to the assistant settings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* chore: point the ee ref at the merged ee branch

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: keep hidden sections from answering keys and swallowing a failed save

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: restore the staged-fork write guard and narrow the round-2 findings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: restore the workspace-race guards and extend them to MCP

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: keep an in-flight settings read from overwriting typed instructions

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* docs: describe the tool row as the one line it renders

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: keep the prompt entries on the home composer, which has no settings modal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: create the editor with the gutter its caller asked for

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* test: restore the attachment status label guard dropped in the merge

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: surface a refused mcp selection write instead of painting the switch

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: refuse instruction writes to a staged fork's parent, and read the target's role

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: pin the instructions role and field to the target workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld1m9NAGLdPPrNBiu5PSQK

* fix: retry a deferred instructions reload, and use Button for the row label

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGnKFvaX61wiMU1CzQp6XG

* fix: leave the arrows to a control that answered them, and say when a role read failed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGnKFvaX61wiMU1CzQp6XG

* chore: update ee-repo-ref to a2776856c50e80c9dbcf6e689a66ce86567c03fa

This commit updates the EE repository reference after PR #765 was merged in windmill-ee-private.

Previous ee-repo-ref: dd7466e749753568a23c91ba5e165020769206b8

New ee-repo-ref: a2776856c50e80c9dbcf6e689a66ce86567c03fa

Automated by sync-ee-ref workflow.

* fix: withhold the page navigator from a parked section

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGnKFvaX61wiMU1CzQp6XG

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Guilhem Lemouel <guilhemlemouel@gmail.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-09-04 10:53:02 +02:00

159 lines
5.3 KiB
Svelte

<!--
@component
The Files & folders section of the assistant settings modal: everything linked to this
chat, whether it is readable, and the way to unlink it.
Attaching happens in the composer — this is where what is already attached is accounted
for, which is the one place the difference between "attached" and "readable" is visible.
-->
<script lang="ts">
import { Button, ListRow, Section } from '$lib/components/common'
import EmptyState from '$lib/components/common/emptyState/EmptyState.svelte'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import { FileText, Folder, Paperclip, Unlink } from 'lucide-svelte'
import { getAiChatManager } from './aiChatManagerContext'
import { attachmentStatusLabel, countReadyAttachments } from './agentContext'
let {
count = $bindable(),
blocksClose = $bindable()
}: {
/** Attachments the assistant can actually read, for the sidebar badge. */
count: number
/** True while the confirmation is up, so the modal leaves the keys to it. */
blocksClose: boolean
} = $props()
const aiChatManager = getAiChatManager()
let folders = $derived(aiChatManager.attachedFiles.folders)
// Files dropped on a past message are listed alongside the session's own: the
// composer's bar hides them because their chip lives on that message, but the
// assistant reads them like any other row.
let files = $derived([
...aiChatManager.attachedFiles.standalone,
...aiChatManager.attachedFiles.messageAttached
])
let messageScoped = $derived(
new Set(aiChatManager.attachedFiles.messageAttached.map((f) => f.id))
)
let total = $derived(folders.length + files.length)
let ready = $derived(countReadyAttachments(folders, files))
let pendingDetach = $state<{ kind: 'file' | 'folder'; name: string } | undefined>(undefined)
$effect(() => {
count = ready
})
$effect(() => {
blocksClose = pendingDetach !== undefined
})
function detach(target: { kind: 'file' | 'folder'; name: string }) {
if (target.kind === 'folder') aiChatManager.attachedFiles.removeFolder(target.name)
else aiChatManager.attachedFiles.removeFile(target.name)
pendingDetach = undefined
}
</script>
<Section
label="Files & folders"
description="Files and folders linked to this chat, which the assistant can open and search."
class="flex flex-col gap-4"
>
{#snippet action()}
{#if total > 0}
<!-- Both numbers when they differ: one count under a heading about what the
assistant can use would hide the rows it cannot open. -->
<span class="shrink-0 text-2xs text-secondary tabular-nums">
{ready === total ? `${total} usable` : `${ready} of ${total} usable`}
</span>
{/if}
{/snippet}
{#if total === 0}
<EmptyState
icon={Paperclip}
title="Nothing attached"
description="Drop a file or a folder on the chat, or use the paperclip in the composer, and the assistant can open and search it."
/>
{:else}
<div class="flex flex-col gap-0.5">
{#each folders as folder (folder.name)}
{#snippet icon()}
<Folder size={16} class="text-tertiary" />
{/snippet}
{#snippet title()}
<span class="truncate leading-5">{folder.name}</span>
{@const label = attachmentStatusLabel(folder.status)}
{#if label}
<span class="shrink-0 text-2xs font-normal text-hint">{label}</span>
{/if}
{/snippet}
{#snippet subtitle()}
{folder.files.length}
{folder.files.length === 1 ? 'file' : 'files'}
{/snippet}
{#snippet trailing()}
<Button
unifiedSize="sm"
variant="subtle"
startIcon={{ icon: Unlink }}
iconOnly
title="Unlink folder"
onClick={() => (pendingDetach = { kind: 'folder', name: folder.name })}
/>
{/snippet}
<ListRow {icon} {title} {subtitle} {trailing} />
{/each}
{#each files as file (file.id ?? file.name)}
{#snippet icon()}
<FileText size={16} class="text-tertiary" />
{/snippet}
{#snippet title()}
<span class="truncate leading-5">{file.name}</span>
{@const label = attachmentStatusLabel(file.status)}
{#if label}
<span class="shrink-0 text-2xs font-normal text-hint">{label}</span>
{/if}
{/snippet}
{#snippet subtitle()}
Attached to a message
{/snippet}
{#snippet trailing()}
<Button
unifiedSize="sm"
variant="subtle"
startIcon={{ icon: Unlink }}
iconOnly
title="Unlink file"
onClick={() => (pendingDetach = { kind: 'file', name: file.name })}
/>
{/snippet}
<!-- A message-scoped row has no unlink here: those are rebuilt from the
transcript on every load, so `removeFile` refuses them and the chip on the
message is what actually drops one. -->
<ListRow
{icon}
{title}
subtitle={messageScoped.has(file.id) ? subtitle : undefined}
trailing={messageScoped.has(file.id) ? undefined : trailing}
/>
{/each}
</div>
{/if}
</Section>
<ConfirmationModal
open={pendingDetach !== undefined}
title={pendingDetach?.kind === 'folder' ? 'Unlink folder' : 'Unlink file'}
confirmationText="Unlink"
onConfirmed={() => pendingDetach && detach(pendingDetach)}
onCanceled={() => (pendingDetach = undefined)}
>
<span class="text-xs text-primary">
The assistant loses access to <span class="font-semibold">{pendingDetach?.name}</span>. Nothing
is deleted from your disk, and you can attach it again from the composer.
</span>
</ConfirmationModal>