- {#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()}
-
- actions.onManage(section)}>
- Manage
-
-
-{/snippet}
-
-
- {#if summary.empty}
-
- {summary.emptyText}
- {#if summary.emptyAction}
- actions.onManage(section)}>
- {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}
-
(expandedInstructions[block.key] = !expandedInstructions[block.key])}
- >{expandedInstructions[block.key] ? 'Show less' : 'Show more'}
- {/if}
- {:else}
-
None
- {/if}
-
- {/each}
-
- actions.onManage(section)}>
- Edit
-
-
- {/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]}
-
(open[s.id] = !open[s.id])}
- >
-
-
- {s.label}
- {s.count ?? ''}
-
- {s.names.length ? s.names.join(', ') : s.emptyText}
-
-
- {#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}
+
+ state.setAll(true)}>All
+ state.setAll(false)}>None
+
+
+
+
+ {#each state.scopes as scope (scope.id)}
+ {@const Icon = scopeIcon(scope)}
+ {@const recap = scopeRecap(scope)}
+
+
+ state.toggleExpanded(scope.id)}
+ >
+
+
+ {scope.name}
+ {recap}
+
+
+ state.setSelected(scope.id, event.currentTarget.checked)}
+ />
+
+
+ {#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}
+
+
Change
+
+
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
-
-
-
-
-
-
-{/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 }
-]