From e721e6663dd5f0c1dae049757f71cf78ff667fdf Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 22 Sep 2026 11:05:41 +0200 Subject: [PATCH] feat: organize AI session context by folder --- .../copilot/chat/AIChatDisplay.svelte | 39 ++- .../chat/AssistantSettingsModal.svelte | 176 +++++++--- .../chat/contextSummary/AccessManifest.svelte | 72 ++++ .../contextSummary/AgentContextSummary.svelte | 154 --------- .../ContextSectionDetail.svelte | 152 -------- .../contextSummary/ContextSummaryRows.svelte | 57 --- .../contextSummary/FolderAccessTree.svelte | 93 +++++ .../chat/contextSummary/WorkingFolders.svelte | 40 +++ .../chat/contextSummary/contextSummary.ts | 116 ------- .../chat/contextSummary/folderAccess.ts | 326 ++++++++++++++++++ .../folderAccessState.svelte.ts | 42 +++ .../chat/contextSummary/sectionIcons.ts | 10 - .../contextSummary/selectedEditableFolders.ts | 8 + .../(logged)/dev/agent-context/+page.svelte | 77 ----- .../dev/agent-context/mockContexts.ts | 187 ---------- 15 files changed, 734 insertions(+), 815 deletions(-) create mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/AccessManifest.svelte delete mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/AgentContextSummary.svelte delete mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/ContextSectionDetail.svelte delete mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/ContextSummaryRows.svelte create mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/FolderAccessTree.svelte create mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/WorkingFolders.svelte delete mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/contextSummary.ts create mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/folderAccess.ts create mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/folderAccessState.svelte.ts delete mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/sectionIcons.ts create mode 100644 frontend/src/lib/components/copilot/chat/contextSummary/selectedEditableFolders.ts delete mode 100644 frontend/src/routes/(root)/(logged)/dev/agent-context/+page.svelte delete mode 100644 frontend/src/routes/(root)/(logged)/dev/agent-context/mockContexts.ts diff --git a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte index eda29e85bb..8f43afcc6e 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte @@ -38,7 +38,9 @@ import AIChatModelSettings from './AIChatModelSettings.svelte' import ScrollFade from '$lib/components/ScrollFade.svelte' import AssistantSettingsModal from './AssistantSettingsModal.svelte' - import AgentContextSummary from './contextSummary/AgentContextSummary.svelte' + import WorkingFolders from './contextSummary/WorkingFolders.svelte' + import FolderAccessTree from './contextSummary/FolderAccessTree.svelte' + import { FolderAccessState } from './contextSummary/folderAccessState.svelte' import { SkillsMenu } from './skills/skillsMenu.svelte' import { McpMenu } from '$lib/components/mcp/mcpMenu.svelte' import ChatMode from './ChatMode.svelte' @@ -69,6 +71,7 @@ import { sendUserToast } from '$lib/toast' import Alert from '$lib/components/common/alert/Alert.svelte' import { copilotInfo } from '$lib/aiStore' + import { workspaceStore } from '$lib/stores' import { base } from '$lib/base' const MAX_YOLO_TOOLTIP_TOOLS = 8 @@ -231,6 +234,9 @@ let aiChatInput: AIChatInput | undefined = $state() let assistantSettings: AssistantSettingsModal | undefined = $state() + const folderAccessState = new FolderAccessState( + () => aiChatManager.operatingWorkspace ?? $workspaceStore ?? '' + ) // The "+" menu's skill and MCP rows: enough state to check and flip one, with // everything else about them behind the assistant settings modal. const skillsMenu = new SkillsMenu(aiChatManager, () => assistantSettings?.open('skills')) @@ -585,6 +591,9 @@ ? `w-full max-w-3xl mx-auto ${agentGutter ? 'px-8' : 'px-7'}` : `w-full max-w-2xl mx-auto ${agentGutter ? 'px-8' : 'px-3'}` ) + const showEmptySessionContext = $derived( + messages.length === 0 && chatHost.mode === AIMode.GLOBAL && chatHost.isSessionChat + ) const waitingForUserAction = $derived(chatHost.loading && !!pendingUserAction(messages)) @@ -687,7 +696,11 @@
@@ -886,7 +900,11 @@ the panel, or the Escape-to-stop focus check would wrongly reject them. --> -
+
{#if showFlowPendingActionControls}
- {#if messages.length === 0 && chatHost.mode === AIMode.GLOBAL && chatHost.isSessionChat} -
+ {#if showEmptySessionContext} +
- assistantSettings?.open(section)} /> +
{/if} diff --git a/frontend/src/lib/components/copilot/chat/AssistantSettingsModal.svelte b/frontend/src/lib/components/copilot/chat/AssistantSettingsModal.svelte index 7694be3827..a9aebd6836 100644 --- a/frontend/src/lib/components/copilot/chat/AssistantSettingsModal.svelte +++ b/frontend/src/lib/components/copilot/chat/AssistantSettingsModal.svelte @@ -14,7 +14,17 @@ callers that already know which section they mean, such as the "+" menu's Manage + +
+ {#each ACCESS_ENTRY_KIND_ORDER as kind, kindIndex (kind)} + {@const entries = scope.entries.filter((entry) => entry.kind === kind)} + {@const KindIcon = kindIcons[kind]} + {#if entries.length} +
+
+ + {sectionLabels[kind]} +
+ +
+ {/if} + {/each} +
diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/AgentContextSummary.svelte b/frontend/src/lib/components/copilot/chat/contextSummary/AgentContextSummary.svelte deleted file mode 100644 index 84c85c3a4a..0000000000 --- a/frontend/src/lib/components/copilot/chat/contextSummary/AgentContextSummary.svelte +++ /dev/null @@ -1,154 +0,0 @@ - - -{#await listsPromise then lists} - -{/await} diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/ContextSectionDetail.svelte b/frontend/src/lib/components/copilot/chat/contextSummary/ContextSectionDetail.svelte deleted file mode 100644 index 99108e45a8..0000000000 --- a/frontend/src/lib/components/copilot/chat/contextSummary/ContextSectionDetail.svelte +++ /dev/null @@ -1,152 +0,0 @@ - - -{#snippet toolChips(tools: ContextTool[])} -
- {#each tools as tool (tool.name)} - {tool.name} - {/each} -
-{/snippet} - -{#snippet scope()} - Applies to all your sessions in {ctx.workspace} -{/snippet} - -{#snippet footer()} -
- -
-{/snippet} - -
- {#if summary.empty} -
- {summary.emptyText} - {#if summary.emptyAction} - - {/if} -
- {:else if section === 'tools'} - {#each [{ key: 'read', label: 'Read', tools: ctx.tools.filter((t) => t.readOnly) }, { key: 'write', label: 'Write', tools: ctx.tools.filter((t) => !t.readOnly) }] as group (group.key)} - {#if group.tools.length} -
- {group.label} · {group.tools.length} - {@render toolChips(group.tools)} -
- {/if} - {/each} - {@render footer()} - {:else if section === 'skills'} - {@render scope()} -
- {#each ctx.skills as skill (skill.path)} -
-
- {skill.name} - {#if skill.description} - {skill.description} - {/if} -
- actions.onToggleSkill(skill.path, e.detail)} - /> -
- {/each} -
- {@render footer()} - {:else if section === 'mcp'} - {@render scope()} -
- {#each ctx.mcpServers as server (server.path)} -
-
- {server.name} - {#if server.description} - {server.description} - {/if} -
- actions.onToggleMcp(server.path, e.detail)} - /> -
- {/each} -
- {@render footer()} - {:else if section === 'instructions'} - {#each [{ key: 'workspace', label: 'Workspace', text: ctx.instructions.workspace }, { key: 'user', label: 'Personal', text: ctx.instructions.user }] as block (block.key)} -
- {block.label} - {#if block.text?.trim()} -

{block.text.trim()}

- {#if block.text.trim().split('\n').length > 3 || block.text.length > 240} - - {/if} - {:else} - None - {/if} -
- {/each} -
- -
- {/if} -
diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/ContextSummaryRows.svelte b/frontend/src/lib/components/copilot/chat/contextSummary/ContextSummaryRows.svelte deleted file mode 100644 index 5546047c38..0000000000 --- a/frontend/src/lib/components/copilot/chat/contextSummary/ContextSummaryRows.svelte +++ /dev/null @@ -1,57 +0,0 @@ - - -
- Context - {#each sections as s (s.id)} - {@const Icon = CONTEXT_SECTION_ICONS[s.id]} - - {#if open[s.id]} -
- -
- {/if} - {/each} -
diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/FolderAccessTree.svelte b/frontend/src/lib/components/copilot/chat/contextSummary/FolderAccessTree.svelte new file mode 100644 index 0000000000..c0a4c36bef --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/contextSummary/FolderAccessTree.svelte @@ -0,0 +1,93 @@ + + +
+
+ {title} +
+ + +
+
+ +
+ {#each state.scopes as scope (scope.id)} + {@const Icon = scopeIcon(scope)} + {@const recap = scopeRecap(scope)} +
+
+ + +
+ {#if state.expanded[scope.id]} +
+ +
+ {/if} +
+ {/each} +
+
diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/WorkingFolders.svelte b/frontend/src/lib/components/copilot/chat/contextSummary/WorkingFolders.svelte new file mode 100644 index 0000000000..ecf4860242 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/contextSummary/WorkingFolders.svelte @@ -0,0 +1,40 @@ + + +
+
+
+

Working folders

+

{selectionSummary}

+
+ +
+
diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/contextSummary.ts b/frontend/src/lib/components/copilot/chat/contextSummary/contextSummary.ts deleted file mode 100644 index 7cba3a5966..0000000000 --- a/frontend/src/lib/components/copilot/chat/contextSummary/contextSummary.ts +++ /dev/null @@ -1,116 +0,0 @@ -export type ContextSectionId = 'tools' | 'skills' | 'instructions' | 'mcp' - -export type ContextTool = { name: string; description: string; readOnly: boolean } -export type ContextSkill = { path: string; name: string; description: string; enabled: boolean } -export type ContextMcpServer = { - path: string - name: string - description?: string - enabled: boolean -} -/** What a session's agent is given before the first message. `tools` is every tool - * definition the chat loop sends, the MCP bridge tools included — a server's own tools - * are discovered at call time and are not part of the prompt. */ -export type AgentContext = { - workspace: string - tools: ContextTool[] - skills: ContextSkill[] - mcpServers: ContextMcpServer[] - instructions: { workspace?: string; user?: string } -} - -export type ContextSummaryActions = { - onToggleSkill: (path: string, enabled: boolean) => void - onToggleMcp: (path: string, enabled: boolean) => void - /** Opens the assistant settings modal on that section. */ - onManage: (section: ContextSectionId) => void -} - -export type ContextSectionSummary = { - id: ContextSectionId - label: string - /** Undefined for Instructions, which has no count. */ - count: string | undefined - /** Names of what reaches the agent, in display order. */ - names: string[] - empty: boolean - /** Shown when the section is empty, or when everything in it is turned off. */ - emptyText: string - /** Label of the action that opens the settings modal on an empty section. */ - emptyAction?: string -} - -export const CONTEXT_SECTION_ORDER: ContextSectionId[] = [ - 'tools', - 'skills', - 'instructions', - 'mcp' -] - -function partCount(on: number, total: number): string { - return on === total ? `${total}` : `${on} of ${total}` -} - -export function summarizeContextSection( - ctx: AgentContext, - id: ContextSectionId -): ContextSectionSummary { - switch (id) { - case 'tools': { - const read = ctx.tools.filter((t) => t.readOnly).length - return { - id, - label: 'Tools', - count: `${ctx.tools.length}`, - // The split rather than the names: which tools can change something is the one - // thing worth reading off a collapsed row. - names: ctx.tools.length ? [`${read} read`, `${ctx.tools.length - read} write`] : [], - empty: ctx.tools.length === 0, - emptyText: 'No tools' - } - } - case 'skills': { - const on = ctx.skills.filter((s) => s.enabled) - return { - id, - label: 'Skills', - count: ctx.skills.length ? partCount(on.length, ctx.skills.length) : '0', - names: on.map((s) => s.name), - empty: ctx.skills.length === 0, - emptyText: ctx.skills.length ? 'All turned off' : 'No skills', - emptyAction: 'Add skill' - } - } - case 'instructions': { - const names = [ - ctx.instructions.workspace?.trim() ? 'workspace' : undefined, - ctx.instructions.user?.trim() ? 'personal' : undefined - ].filter((n) => n !== undefined) - return { - id, - label: 'Instructions', - count: undefined, - names, - empty: names.length === 0, - emptyText: 'No instructions', - emptyAction: 'Write instructions' - } - } - case 'mcp': { - const on = ctx.mcpServers.filter((s) => s.enabled) - return { - id, - label: 'MCP', - count: ctx.mcpServers.length ? partCount(on.length, ctx.mcpServers.length) : '0', - names: on.map((s) => s.name), - empty: ctx.mcpServers.length === 0, - emptyText: ctx.mcpServers.length ? 'All turned off' : 'No servers', - emptyAction: 'Connect' - } - } - } -} - -export function summarizeContext(ctx: AgentContext): ContextSectionSummary[] { - return CONTEXT_SECTION_ORDER.map((id) => summarizeContextSection(ctx, id)) -} diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/folderAccess.ts b/frontend/src/lib/components/copilot/chat/contextSummary/folderAccess.ts new file mode 100644 index 0000000000..133be0b481 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/contextSummary/folderAccess.ts @@ -0,0 +1,326 @@ +export type AccessEntryKind = + | 'instruction' + | 'skill' + | 'mcp' + | 'script' + | 'flow' + | 'app' + | 'resource' + | 'variable' + +export type AccessEntry = { + id: string + kind: AccessEntryKind + name: string + path: string + description: string +} + +export type AccessScope = { + id: string + kind: 'personal' | 'workspace' | 'folder' + name: string + path?: string + description: string + entries: AccessEntry[] +} + +export const ACCESS_ENTRY_KIND_ORDER: AccessEntryKind[] = [ + 'mcp', + 'instruction', + 'skill', + 'script', + 'flow', + 'app', + 'resource', + 'variable' +] + +export const ACCESS_ENTRY_LABELS: Record = { + instruction: 'Instruction', + skill: 'Skill', + mcp: 'MCP server', + script: 'Script', + flow: 'Flow', + app: 'App', + resource: 'Resource', + variable: 'Variable' +} + +const folderSpecs = [ + { + name: 'Finance', + slug: 'finance', + subject: 'financial records', + scriptCount: 8, + flowCount: 5, + appCount: 2 + }, + { + name: 'Customer support', + slug: 'customer_support', + subject: 'support tickets', + scriptCount: 7, + flowCount: 6, + appCount: 2 + }, + { + name: 'Data platform', + slug: 'data_platform', + subject: 'datasets', + scriptCount: 9, + flowCount: 5, + appCount: 2 + }, + { + name: 'Engineering', + slug: 'engineering', + subject: 'releases', + scriptCount: 8, + flowCount: 5, + appCount: 2 + }, + { + name: 'Growth', + slug: 'growth', + subject: 'campaigns', + scriptCount: 6, + flowCount: 4, + appCount: 2 + }, + { + name: 'Operations', + slug: 'operations', + subject: 'orders', + scriptCount: 9, + flowCount: 6, + appCount: 3 + }, + { + name: 'People', + slug: 'people', + subject: 'employee onboarding', + scriptCount: 5, + flowCount: 4, + appCount: 1 + } +] as const + +function entry( + folder: string, + kind: AccessEntryKind, + name: string, + path: string, + description: string +): AccessEntry { + return { id: `${folder}:${kind}:${path}`, kind, name, path, description } +} + +function folderScope(spec: (typeof folderSpecs)[number], index: number): AccessScope { + const { name, slug, subject, scriptCount, flowCount, appCount } = spec + const entries: AccessEntry[] = [ + entry( + slug, + 'instruction', + `${name} working agreement`, + `f/${slug}/instructions`, + `How the agent should handle ${subject} work.` + ), + entry( + slug, + 'skill', + `${name} specialist`, + `f/${slug}/skills/specialist`, + `Domain guidance and repeatable procedures for ${subject}.` + ) + ] + + const scriptNames = [ + `Import ${subject}`, + `Normalize ${subject}`, + `Sync ${subject} to warehouse`, + `Validate ${subject}`, + `Export ${subject} report`, + `Archive stale ${subject}`, + `Backfill ${subject}`, + `Notify ${subject} owners`, + `Reconcile ${subject}` + ] + const flowNames = [ + `${name} intake`, + `${name} approval`, + `${name} daily sync`, + `${name} escalation`, + `${name} weekly review`, + `${name} lifecycle` + ] + const appNames = [`${name} dashboard`, `${name} operations console`, `${name} admin`] + + entries.push( + ...scriptNames + .slice(0, scriptCount) + .map((label, itemIndex) => + entry( + slug, + 'script', + label, + `f/${slug}/scripts/${itemIndex + 1}`, + `Processes ${subject} for the ${name.toLowerCase()} team.` + ) + ), + ...flowNames + .slice(0, flowCount) + .map((label, itemIndex) => + entry( + slug, + 'flow', + label, + `f/${slug}/flows/${itemIndex + 1}`, + `Coordinates ${subject} across the team.` + ) + ), + ...appNames + .slice(0, appCount) + .map((label, itemIndex) => + entry( + slug, + 'app', + label, + `f/${slug}/apps/${itemIndex + 1}`, + `Operational view for ${subject}.` + ) + ) + ) + if (index % 3 !== 1) { + entries.push( + entry( + slug, + 'resource', + `${name} database`, + `$res:f/${slug}/database`, + `Connection configuration used by this folder.` + ) + ) + } + if (index % 3 === 0) { + entries.push( + entry( + slug, + 'mcp', + `${name} gateway`, + `$res:f/${slug}/mcp_gateway`, + `External tools scoped to ${subject}.` + ) + ) + } + if (index % 4 !== 2) { + entries.push( + entry( + slug, + 'variable', + `${slug.toUpperCase()}_REGION`, + `$var:f/${slug}/region`, + `Default region for ${subject} operations.` + ) + ) + } + + return { + id: `folder:${slug}`, + kind: 'folder', + name, + path: `f/${slug}`, + description: `Everything the agent can use for ${subject}.`, + entries: entries.sort((a, b) => a.name.localeCompare(b.name)) + } +} + +const virtualScopes: AccessScope[] = [ + { + id: 'personal', + kind: 'personal', + name: 'Personal', + description: 'Preferences and guidance that apply only to you.', + entries: [ + entry( + 'personal', + 'instruction', + 'My working preferences', + 'personal/instructions', + 'Favor concise explanations and show proposed changes before applying them.' + ), + entry( + 'personal', + 'skill', + 'My review checklist', + 'personal/skills/review_checklist', + 'A personal checklist for reviewing generated work.' + ) + ] + }, + { + id: 'workspace', + kind: 'workspace', + name: 'Workspace-wide', + description: 'Shared context available across every workspace folder.', + entries: [ + entry( + 'workspace', + 'instruction', + 'Workspace conventions', + 'workspace/instructions', + 'Shared naming, testing, and deployment conventions.' + ), + entry( + 'workspace', + 'skill', + 'Windmill conventions', + 'workspace/skills/windmill', + 'Standard patterns for scripts, flows, apps, and resources.' + ), + entry( + 'workspace', + 'mcp', + 'Documentation', + '$res:u/admin/documentation_mcp', + 'Searches internal product and engineering documentation.' + ), + entry( + 'workspace', + 'resource', + 'Shared object storage', + '$res:u/admin/object_storage', + 'Workspace-wide object storage configuration.' + ) + ].sort((a, b) => a.name.localeCompare(b.name)) + } +] + +export const ACCESS_SCOPES: AccessScope[] = [...virtualScopes, ...folderSpecs.map(folderScope)] + +export const WORKSPACE_ITEM_KINDS = new Set([ + 'script', + 'flow', + 'app', + 'resource', + 'variable' +]) + +export function scopeRecap(scope: AccessScope): string { + const counts = ACCESS_ENTRY_KIND_ORDER.map((kind) => ({ + kind, + count: scope.entries.filter((entry) => entry.kind === kind).length + })).filter(({ count }) => count > 0) + const itemCounts = counts.filter(({ kind }) => WORKSPACE_ITEM_KINDS.has(kind)) + const collapseItems = itemCounts.length > 2 + const parts = counts + .filter(({ kind }) => !collapseItems || !WORKSPACE_ITEM_KINDS.has(kind)) + .map(({ kind, count }) => { + const label = kind === 'mcp' ? 'MCP' : ACCESS_ENTRY_LABELS[kind].toLowerCase() + return `${count} ${label}${count === 1 ? '' : 's'}` + }) + if (collapseItems) { + parts.push(`${itemCounts.reduce((total, { count }) => total + count, 0)} items`) + } + return parts.join(' · ') +} diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/folderAccessState.svelte.ts b/frontend/src/lib/components/copilot/chat/contextSummary/folderAccessState.svelte.ts new file mode 100644 index 0000000000..ec495c82ca --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/contextSummary/folderAccessState.svelte.ts @@ -0,0 +1,42 @@ +import { ACCESS_SCOPES, type AccessScope } from './folderAccess' +import { isEditableFolderSelected, setEditableFolderSelected } from './selectedEditableFolders' + +export class FolderAccessState { + private selectionVersion = $state(0) + expanded = $state>({}) + + constructor(private readonly workspace: () => string) {} + + get scopes(): AccessScope[] { + return ACCESS_SCOPES + } + + isSelected(id: string): boolean { + this.selectionVersion + return isEditableFolderSelected(this.workspace(), id) + } + + get selectedScopes(): AccessScope[] { + return this.scopes.filter((scope) => this.isSelected(scope.id)) + } + + get selectedEntries() { + return this.selectedScopes.flatMap((scope) => scope.entries) + } + + setSelected(id: string, value: boolean) { + if (setEditableFolderSelected(this.workspace(), id, value)) this.selectionVersion++ + } + + setAll(value: boolean) { + let changed = false + for (const scope of this.scopes) { + changed = setEditableFolderSelected(this.workspace(), scope.id, value) || changed + } + if (changed) this.selectionVersion++ + } + + toggleExpanded(id: string) { + this.expanded[id] = !this.expanded[id] + } +} diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/sectionIcons.ts b/frontend/src/lib/components/copilot/chat/contextSummary/sectionIcons.ts deleted file mode 100644 index 6c767d8ce2..0000000000 --- a/frontend/src/lib/components/copilot/chat/contextSummary/sectionIcons.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BookOpen, Boxes, Plug, ScrollText } from 'lucide-svelte' -import type { ContextSectionId } from './contextSummary' - -/** The settings modal's section icons, so both surfaces read as the same thing. */ -export const CONTEXT_SECTION_ICONS = { - tools: Boxes, - skills: BookOpen, - instructions: ScrollText, - mcp: Plug -} satisfies Record diff --git a/frontend/src/lib/components/copilot/chat/contextSummary/selectedEditableFolders.ts b/frontend/src/lib/components/copilot/chat/contextSummary/selectedEditableFolders.ts new file mode 100644 index 0000000000..d327e4a7d2 --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/contextSummary/selectedEditableFolders.ts @@ -0,0 +1,8 @@ +import { createPathsPreference } from '../enabledPathsPreference' + +// No agent tool or prompt builder reads this preference: it only controls the +// context-summary presentation and must not be interpreted as an access boundary. +const preference = createPathsPreference('wm_ai_editable_folders', true) + +export const isEditableFolderSelected = preference.isEnabled +export const setEditableFolderSelected = preference.setEnabled diff --git a/frontend/src/routes/(root)/(logged)/dev/agent-context/+page.svelte b/frontend/src/routes/(root)/(logged)/dev/agent-context/+page.svelte deleted file mode 100644 index 7218591536..0000000000 --- a/frontend/src/routes/(root)/(logged)/dev/agent-context/+page.svelte +++ /dev/null @@ -1,77 +0,0 @@ - - -{#if import.meta.env.DEV} -
-
-
-

Agent context summary

-

- Dev-only mock of what an empty session chat shows about its agent's context. -

-
- - {#snippet children({ item })} - {#each MOCK_CONTEXTS as m (m.id)} - - {/each} - {/snippet} - - -
-
Untitled session
- -
-
- -
-
-
-
- Ask anything… - - -
-
-
-
-
-{/if} diff --git a/frontend/src/routes/(root)/(logged)/dev/agent-context/mockContexts.ts b/frontend/src/routes/(root)/(logged)/dev/agent-context/mockContexts.ts deleted file mode 100644 index 2ad6d56a0f..0000000000 --- a/frontend/src/routes/(root)/(logged)/dev/agent-context/mockContexts.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { - AgentContext, - ContextTool -} from '$lib/components/copilot/chat/contextSummary/contextSummary' - -const BUILTIN_TOOL_NAMES = [ - 'askUserQuestion', - 'cancel_job', - 'close_page', - 'create_artifact', - 'create_folder', - 'delete_workspace_item', - 'deploy_workspace_item', - 'diff', - 'discard_local_draft', - 'edit_script', - 'get_instructions', - 'get_preview_status', - 'get_run', - 'get_schedule_schema', - 'get_trigger_schema', - 'list_artifacts', - 'list_runs', - 'list_workers', - 'list_workspace_items', - 'open_page', - 'open_preview', - 'patch_flow_json', - 'read_artifact', - 'read_dom', - 'read_flow_module_code', - 'read_skill', - 'read_workspace_item', - 'rebase_draft', - 'run_flow', - 'run_script', - 'search_dom', - 'search_resource_types', - 'set_flow_module_code', - 'take_screenshot', - 'test_run_flow', - 'test_run_script', - 'test_run_step', - 'update_user_instructions', - 'write_resource', - 'write_schedule', - 'write_trigger', - 'write_variable' -] - -// Read vs write the way the real summary decides it: a tool that only reads is the one -// plan mode admits. -const WRITE_PREFIXES = ['write_', 'delete_', 'deploy_', 'patch_', 'init_', 'test_run_', 'run_'] - -function tools(names: string[]): ContextTool[] { - return names.map((name) => ({ - name, - description: `Mock description of ${name}.`, - readOnly: !WRITE_PREFIXES.some((p) => name.startsWith(p)) - })) -} - -const empty: AgentContext = { - workspace: 'admins', - tools: tools(BUILTIN_TOOL_NAMES.slice(0, 38)), - skills: [], - mcpServers: [], - instructions: {} -} - -const typical: AgentContext = { - workspace: 'acme', - tools: tools(BUILTIN_TOOL_NAMES.slice(0, 38)), - skills: [ - { - path: 'f/ai/sql_style', - name: 'sql_style', - description: 'How we write SQL against the warehouse', - enabled: true - }, - { - path: 'f/ai/pr_conventions', - name: 'pr_conventions', - description: 'Branch naming and PR description format', - enabled: false - }, - { - path: 'f/ai/deploy_checklist', - name: 'deploy_checklist', - description: 'Checks to run before deploying a flow to prod', - enabled: true - } - ], - mcpServers: [ - { - path: 'u/admin/linear', - name: 'Linear', - enabled: true - }, - { - path: 'u/admin/notion', - name: 'Notion', - enabled: false - } - ], - instructions: { - workspace: - 'Scripts go under f//. Always use the postgres resource f/data/warehouse for analytics queries.', - user: 'Answer briefly. Prefer Python over TypeScript.' - } -} - -const heavy: AgentContext = { - workspace: 'platform-prod', - tools: tools([ - ...BUILTIN_TOOL_NAMES, - 'write_app_file', - 'read_app_file', - 'patch_app_file', - 'delete_app_file', - 'init_app', - 'search_app', - 'list_app_runs', - 'get_app_runtime_logs', - 'write_app_runnable', - 'delete_app_runnable', - 'test_run_app_runnable', - 'update_artifact', - 'audit_logs', - 'workspace_settings', - 'resources', - 'variables', - 'schedules', - 'triggers' - ]), - skills: Array.from({ length: 12 }, (_, i) => ({ - path: `f/ai/skill_${i + 1}`, - name: [ - 'sql_style', - 'pr_conventions', - 'deploy_checklist', - 'incident_runbook', - 'naming_rules', - 'python_deps', - 'retry_policy', - 'slack_alerts', - 'datatable_migrations', - 'hub_usage', - 'secrets_handling', - 'flow_patterns' - ][i], - description: 'Mock skill description that is long enough to be truncated in a narrow column', - enabled: i % 4 !== 3 - })), - mcpServers: [ - { - path: 'f/mcp/linear', - name: 'Linear', - enabled: true - }, - { - path: 'f/mcp/github', - name: 'GitHub', - enabled: true - }, - { path: 'f/mcp/notion', name: 'Notion', enabled: false }, - { path: 'f/mcp/sentry', name: 'Sentry', enabled: true }, - { path: 'f/mcp/datadog', name: 'Datadog', enabled: false } - ], - instructions: { - workspace: [ - 'This is the production platform workspace.', - 'Never deploy directly: stage every change in a fork and ask for review.', - 'Scripts go under f//, one folder per team.', - 'Use f/data/warehouse for analytics and f/data/app_db for application data.', - 'Every flow must set an error handler pointing to f/ops/alert_oncall.', - 'Schedules run in UTC.' - ].join('\n'), - user: 'Answer briefly.\nPrefer Python.\nWhen unsure about a resource, ask before creating one.\nI am on the data team.' - } -} - -export const MOCK_CONTEXTS: { id: string; label: string; ctx: AgentContext }[] = [ - { id: 'empty', label: 'Fresh workspace', ctx: empty }, - { id: 'typical', label: 'Typical', ctx: typical }, - { id: 'heavy', label: 'Heavy', ctx: heavy } -]