mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
feat: add Pi template to the AI sandbox picker (#11619)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2758920f11
commit
a85d0b76cc
@@ -105,6 +105,7 @@
|
||||
| 'powershell'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox'
|
||||
| 'wac_python'
|
||||
| 'wac_typescript'
|
||||
| 'ci_test_bun'
|
||||
|
||||
@@ -596,6 +596,7 @@
|
||||
| 'powershell'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox'
|
||||
| 'wac_python'
|
||||
| 'wac_typescript'
|
||||
| 'ci_test_bun'
|
||||
@@ -1543,10 +1544,12 @@
|
||||
(lang == script.language &&
|
||||
template != 'bunnative' &&
|
||||
template != 'docker' &&
|
||||
template != 'claudesandbox') ||
|
||||
template != 'claudesandbox' &&
|
||||
template != 'pisandbox') ||
|
||||
(template == 'bunnative' && lang == 'bunnative') ||
|
||||
(template == 'docker' && lang == 'docker') ||
|
||||
(template == 'claudesandbox' && lang == 'bun')}
|
||||
((template == 'claudesandbox' || template == 'pisandbox') &&
|
||||
lang == 'bun')}
|
||||
<Popover
|
||||
disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}
|
||||
>
|
||||
@@ -1597,6 +1600,21 @@
|
||||
>
|
||||
Claude Sandbox
|
||||
</Button>
|
||||
<Button
|
||||
unifiedSize="xs"
|
||||
variant="default"
|
||||
startIcon={{
|
||||
icon: LanguageIcon,
|
||||
props: { lang: 'pisandbox', width: 16, height: 16 }
|
||||
} as ButtonType.Icon}
|
||||
onclick={() => {
|
||||
template = 'pisandbox'
|
||||
script.language = 'bun'
|
||||
initContent('bun', script.kind, template)
|
||||
}}
|
||||
>
|
||||
Pi Sandbox
|
||||
</Button>
|
||||
<span title="Workflow-as-Code">
|
||||
<Button
|
||||
size="xs2"
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
| 'powershell'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox'
|
||||
| 'wac_python'
|
||||
| 'wac_typescript'
|
||||
| 'ci_test_bun'
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import RIcon from '$lib/components/icons/RIcon.svelte'
|
||||
import ClaudeIcon from '$lib/components/icons/ClaudeIcon.svelte'
|
||||
import DbtIcon from '$lib/components/icons/DbtIcon.svelte'
|
||||
import { Pi } from 'lucide-svelte'
|
||||
|
||||
interface Props {
|
||||
lang:
|
||||
@@ -41,6 +42,7 @@
|
||||
| 'powershell'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox'
|
||||
width?: number
|
||||
height?: number
|
||||
scale?: number
|
||||
@@ -50,7 +52,10 @@
|
||||
|
||||
let { lang, width = 30, height = 30, scale = 1, size = undefined, ...rest }: Props = $props()
|
||||
|
||||
const languageLabel: Record<Script['language'] | 'bunnative' | 'claudesandbox', String> = {
|
||||
const languageLabel: Record<
|
||||
Script['language'] | 'bunnative' | 'claudesandbox' | 'pisandbox',
|
||||
String
|
||||
> = {
|
||||
python3: 'Python',
|
||||
deno: 'TypeScript',
|
||||
go: 'Go',
|
||||
@@ -76,12 +81,21 @@
|
||||
ruby: 'Ruby',
|
||||
rlang: 'R',
|
||||
dbt: 'dbt',
|
||||
claudesandbox: 'Claude Sandbox'
|
||||
claudesandbox: 'Claude Sandbox',
|
||||
pisandbox: 'Pi Sandbox'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
const langToComponent: Record<
|
||||
SupportedLanguage | 'pgsql' | 'javascript' | 'fetch' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox',
|
||||
| SupportedLanguage
|
||||
| 'pgsql'
|
||||
| 'javascript'
|
||||
| 'fetch'
|
||||
| 'docker'
|
||||
| 'powershell'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox',
|
||||
any
|
||||
> = {
|
||||
go: GoIcon,
|
||||
@@ -114,7 +128,8 @@
|
||||
rlang: RIcon,
|
||||
duckdb: DuckDbIcon,
|
||||
dbt: DbtIcon,
|
||||
claudesandbox: TypeScriptIcon
|
||||
claudesandbox: TypeScriptIcon,
|
||||
pisandbox: TypeScriptIcon
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
@@ -166,4 +181,14 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if lang === 'pisandbox'}
|
||||
<div
|
||||
class="absolute -top-1.5 -right-1.5 bg-surface rounded-full flex items-center justify-center text-primary"
|
||||
style={`width: ${width * scale * subIconScale}px; height: ${
|
||||
height * scale * subIconScale
|
||||
}px;`}
|
||||
>
|
||||
<Pi size={width * scale * (subIconScale - 0.1)} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -291,6 +291,18 @@
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<FlowScriptPicker
|
||||
label="Pi"
|
||||
lang="pisandbox"
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: 'bun',
|
||||
kind,
|
||||
subkind: 'pisandbox',
|
||||
summary
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -267,28 +267,33 @@
|
||||
)
|
||||
let aiLength = $derived(showAiRows ? 2 : 0)
|
||||
|
||||
// Must match the heading and label rendered for the AI Sandbox section
|
||||
const aiSandboxHeading = 'AI Sandbox'
|
||||
const aiSandboxLabel = 'Claude Code'
|
||||
let matchesAiSandbox = $derived.by(() => {
|
||||
const aiSandboxes = [
|
||||
{ label: 'Claude Code', subkind: 'claudesandbox' },
|
||||
{ label: 'Pi', subkind: 'pisandbox' }
|
||||
] as const
|
||||
// A query matching the heading keeps every sandbox, otherwise only the ones it names.
|
||||
let matchingAiSandboxes = $derived.by(() => {
|
||||
const query = funcDesc?.trim().toLowerCase() ?? ''
|
||||
if (query.length == 0) {
|
||||
return true
|
||||
}
|
||||
return [aiSandboxHeading, aiSandboxLabel].some((term) => {
|
||||
const matches = (term: string) => {
|
||||
const lowered = term.toLowerCase()
|
||||
return lowered.startsWith(query) || lowered.split(' ').some((w) => w.startsWith(query))
|
||||
})
|
||||
}
|
||||
if (query.length == 0 || matches(aiSandboxHeading)) {
|
||||
return aiSandboxes
|
||||
}
|
||||
return aiSandboxes.filter((s) => matches(s.label))
|
||||
})
|
||||
// Gates the AI Sandbox row and its slot in the index space; both must agree or arrow keys land
|
||||
// on an index that renders nothing.
|
||||
let showAiSandbox = $derived(
|
||||
// Gates the AI Sandbox rows and their slots in the index space; both must agree or arrow keys
|
||||
// land on an index that renders nothing.
|
||||
let aiSandboxRows = $derived(
|
||||
selectedKind === 'script' &&
|
||||
preFilter === 'all' &&
|
||||
!selected &&
|
||||
customUi?.aiSandbox != false &&
|
||||
!$operatorBuilderFlows &&
|
||||
matchesAiSandbox
|
||||
!$operatorBuilderFlows
|
||||
? matchingAiSandboxes
|
||||
: []
|
||||
)
|
||||
|
||||
// Hub runnables carry code the workspace never reviewed, and the backend refuses them in a
|
||||
@@ -301,7 +306,7 @@
|
||||
let aiOffset = $derived(inlineOffset + (inlineScripts?.length ?? 0))
|
||||
let workspaceOffset = $derived(aiOffset + aiLength)
|
||||
let aiSandboxOffset = $derived(workspaceOffset + (filteredWorkspaceItems?.length ?? 0))
|
||||
let hubOffset = $derived(aiSandboxOffset + (showAiSandbox ? 1 : 0))
|
||||
let hubOffset = $derived(aiSandboxOffset + aiSandboxRows.length)
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
@@ -530,27 +535,29 @@
|
||||
{/await}
|
||||
<div class="pb-1"></div>
|
||||
{/if}
|
||||
{#if showAiSandbox}
|
||||
{#if aiSandboxRows.length > 0}
|
||||
<div class="pb-0 text-2xs font-normal text-secondary ml-2">{aiSandboxHeading}</div>
|
||||
<FlowScriptPickerQuick
|
||||
eeRestricted={false}
|
||||
selected={selectedByKeyboard === aiSandboxOffset}
|
||||
onHover={() => hover(aiSandboxOffset)}
|
||||
enterpriseLangs={[]}
|
||||
label={aiSandboxLabel}
|
||||
lang="claudesandbox"
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
kind: selectedKind,
|
||||
inlineScript: {
|
||||
language: 'bun',
|
||||
{#each aiSandboxRows as sandbox, i (sandbox.subkind)}
|
||||
<FlowScriptPickerQuick
|
||||
eeRestricted={false}
|
||||
selected={selectedByKeyboard === aiSandboxOffset + i}
|
||||
onHover={() => hover(aiSandboxOffset + i)}
|
||||
enterpriseLangs={[]}
|
||||
label={sandbox.label}
|
||||
lang={sandbox.subkind}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
kind: selectedKind,
|
||||
subkind: 'claudesandbox',
|
||||
summary
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
inlineScript: {
|
||||
language: 'bun',
|
||||
kind: selectedKind,
|
||||
subkind: sandbox.subkind,
|
||||
summary
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if showHub && selectedKind != 'preprocessor' && selectedKind != 'flow'}
|
||||
{#if (!selected || selected?.kind === 'integrations') && (preFilter === 'hub' || preFilter === 'all')}
|
||||
|
||||
@@ -108,7 +108,7 @@ export async function pickFlow(
|
||||
export async function createInlineScriptModule(
|
||||
language: RawScript['language'],
|
||||
kind: Script['kind'],
|
||||
subkind: 'pgsql' | 'flow' | 'claudesandbox' | undefined,
|
||||
subkind: 'pgsql' | 'flow' | 'claudesandbox' | 'pisandbox' | undefined,
|
||||
id: string,
|
||||
summary?: string
|
||||
): Promise<[FlowModule, FlowModuleState]> {
|
||||
|
||||
@@ -117,17 +117,21 @@
|
||||
logReusableAgentUsage('linked')
|
||||
dispatch('new', { kind: 'aiagent', agentPath })
|
||||
}
|
||||
function newClaudeSandbox() {
|
||||
function newAiSandbox(subkind: 'claudesandbox' | 'pisandbox') {
|
||||
dispatch('close')
|
||||
dispatch('new', {
|
||||
kind: 'script',
|
||||
inlineScript: {
|
||||
language: 'bun',
|
||||
kind: 'script',
|
||||
subkind: 'claudesandbox'
|
||||
subkind
|
||||
}
|
||||
})
|
||||
}
|
||||
const aiSandboxes = [
|
||||
{ label: 'Claude Code', subkind: 'claudesandbox' },
|
||||
{ label: 'Pi', subkind: 'pisandbox' }
|
||||
] as const
|
||||
|
||||
// Rows of the AI panes in render order. FlowInputsQuick owns the keyboard index for the other
|
||||
// panes and is unmounted while these render, so the two never fight over Enter or the arrows.
|
||||
@@ -137,7 +141,7 @@
|
||||
return [newBlankAgent, ...saved.map((a) => () => newLinkedAgent(a.path))]
|
||||
}
|
||||
if (selectedKind === 'aisandbox') {
|
||||
return [newClaudeSandbox]
|
||||
return aiSandboxes.map((s) => () => newAiSandbox(s.subkind))
|
||||
}
|
||||
return []
|
||||
})
|
||||
@@ -418,14 +422,16 @@
|
||||
</div>
|
||||
{:else if selectedKind === 'aisandbox'}
|
||||
<div class="h-full overflow-auto grow min-w-0 p-2 gap-1 flex flex-col">
|
||||
<TopLevelNode
|
||||
label="Claude Code"
|
||||
neutral
|
||||
returnIcon
|
||||
selected={aiSelected === 0}
|
||||
onSelect={newClaudeSandbox}
|
||||
onHover={() => (selectedByKeyboard = 0)}
|
||||
/>
|
||||
{#each aiSandboxes as sandbox, i (sandbox.subkind)}
|
||||
<TopLevelNode
|
||||
label={sandbox.label}
|
||||
neutral
|
||||
returnIcon
|
||||
selected={aiSelected === i}
|
||||
onSelect={() => newAiSandbox(sandbox.subkind)}
|
||||
onHover={() => (selectedByKeyboard = i)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<FlowInputsQuick
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
interface Props {
|
||||
disabled?: boolean
|
||||
label: string
|
||||
lang?: SupportedLanguage | 'docker' | 'javascript' | 'claudesandbox' | undefined
|
||||
lang?: SupportedLanguage | 'docker' | 'javascript' | 'claudesandbox' | 'pisandbox' | undefined
|
||||
id?: string | undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
lang?: SupportedLanguage | 'docker' | 'javascript' | 'claudesandbox' | undefined
|
||||
lang?: SupportedLanguage | 'docker' | 'javascript' | 'claudesandbox' | 'pisandbox' | undefined
|
||||
selected?: boolean
|
||||
eeRestricted: boolean
|
||||
enterpriseLangs?: string[]
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
'AI Decision': { icon: Scale, iconClass: 'text-ai' },
|
||||
'AI Sandbox': { icon: BotIcon, showChevron: true, iconClass: 'text-ai' },
|
||||
'Claude Code': { icon: BotIcon, iconClass: 'text-ai' },
|
||||
Pi: { icon: BotIcon, iconClass: 'text-ai' },
|
||||
MCP: { icon: Plug },
|
||||
'Web Search': { icon: Globe }
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export type InsertKind =
|
||||
export type InlineScript = {
|
||||
language: RawScript['language']
|
||||
kind: Script['kind']
|
||||
subkind: 'pgsql' | 'flow' | 'claudesandbox'
|
||||
subkind: 'pgsql' | 'flow' | 'claudesandbox' | 'pisandbox'
|
||||
summary?: string
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface ScriptBuilderProps {
|
||||
| 'docker'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox'
|
||||
| 'wac_python'
|
||||
| 'wac_typescript'
|
||||
| 'ci_test_bun'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type Script } from './gen'
|
||||
import type { SupportedLanguage } from './common'
|
||||
|
||||
import CLAUDE_SANDBOX_INIT_CODE from './templates/claude_sandbox.ts.template?raw'
|
||||
import PI_SANDBOX_INIT_CODE from './templates/pi_sandbox.ts.template?raw'
|
||||
import WAC_PYTHON_INIT_CODE from './templates/wac_python.py.template?raw'
|
||||
import WAC_TYPESCRIPT_INIT_CODE from './templates/wac_typescript.ts.template?raw'
|
||||
import CI_TEST_BUN_INIT_CODE from './templates/ci_test_bun.ts.template?raw'
|
||||
@@ -1500,6 +1501,9 @@ export const INITIAL_CODE = {
|
||||
claudesandbox: {
|
||||
script: CLAUDE_SANDBOX_INIT_CODE
|
||||
},
|
||||
pisandbox: {
|
||||
script: PI_SANDBOX_INIT_CODE
|
||||
},
|
||||
wac_python: {
|
||||
script: WAC_PYTHON_INIT_CODE
|
||||
},
|
||||
@@ -1563,6 +1567,7 @@ export function initialCode(
|
||||
| 'powershell'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox'
|
||||
| 'wac_python'
|
||||
| 'wac_typescript'
|
||||
| 'ci_test_bun'
|
||||
@@ -1670,6 +1675,8 @@ export function initialCode(
|
||||
} else if (language == 'bun' || language == 'bunnative') {
|
||||
if (subkind === 'claudesandbox') {
|
||||
return INITIAL_CODE.claudesandbox.script
|
||||
} else if (subkind === 'pisandbox') {
|
||||
return INITIAL_CODE.pisandbox.script
|
||||
} else if (kind == 'trigger') {
|
||||
return INITIAL_CODE.bun.trigger
|
||||
} else if (language == 'bunnative' || subkind === 'bunnative') {
|
||||
@@ -1711,6 +1718,7 @@ export function getResetCode(
|
||||
| 'powershell'
|
||||
| 'bunnative'
|
||||
| 'claudesandbox'
|
||||
| 'pisandbox'
|
||||
| 'wac_python'
|
||||
| 'wac_typescript'
|
||||
| 'ci_test_bun'
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// sandbox
|
||||
// volume: pi .pi
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
|
||||
import { AssistantEntry, createRegistry, Harness, watchEvents } from "@earendil-works/pi-durable";
|
||||
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
||||
import { openNodeJsonlStorage } from "@earendil-works/pi-durable/storage/jsonl/node";
|
||||
import { CodingTools } from "@earendil-works/pi-durable/tools";
|
||||
|
||||
export async function main(anthropic: RT.Anthropic, instructions?: string) {
|
||||
// Any pi-ai provider works: import it from "@earendil-works/pi-ai/providers/<name>",
|
||||
// set the API key env var it reads, and name it in `model` below.
|
||||
process.env.ANTHROPIC_API_KEY = anthropic.apiKey;
|
||||
const models = createModels();
|
||||
models.setProvider(anthropicProvider());
|
||||
const model = { provider: "anthropic", modelId: "claude-opus-5-5" };
|
||||
|
||||
// read, write, edit and bash, running in this job's sandbox
|
||||
const registry = createRegistry();
|
||||
registry.install(CodingTools);
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
||||
// The conversation is stored in the volume, so each run continues the previous one.
|
||||
const harness = await Harness.open(
|
||||
await openNodeJsonlStorage(".pi/session", context),
|
||||
{ models, registry, env: ({ cwd }) => new NodeExecutionEnv({ cwd: cwd ?? process.cwd() }) },
|
||||
context,
|
||||
);
|
||||
|
||||
try {
|
||||
const root = await harness.root(context);
|
||||
await root.configure({ model, instructions: instructions ?? null }, context);
|
||||
|
||||
const isResume = (await root.viewState(context)).value.entries.length > 0;
|
||||
|
||||
// you can hardcode the prompt or pass it as input.
|
||||
const prompt = !isResume ?
|
||||
"What is the fastest OSS workflow engine?" :
|
||||
"What did I ask you before?"
|
||||
|
||||
const events = await watchEvents(harness, root.id, context);
|
||||
events.start(async (batch) => {
|
||||
for (const event of batch) {
|
||||
if (event.type === "tool_execution_start") {
|
||||
console.log(`${event.toolName} ${JSON.stringify(event.args)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const submission = await root.submit({ type: "input", content: prompt }, context);
|
||||
const settled = await submission.wait(context);
|
||||
if (settled.status !== "done" || settled.type !== "input") {
|
||||
throw new Error(`Pi did not answer: ${JSON.stringify(settled)}`);
|
||||
}
|
||||
|
||||
const answer = await root.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
||||
const response = (answer?.model?.[0]?.content ?? [])
|
||||
.filter((b: any) => b.type === "text")
|
||||
.map((b: any) => b.text)
|
||||
.join("");
|
||||
|
||||
return {
|
||||
is_resume: isResume,
|
||||
prompt,
|
||||
response,
|
||||
usage: (await harness.usage(context)).models,
|
||||
};
|
||||
} finally {
|
||||
await harness.close(context);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user