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:
Ruben Fiszel
2026-10-08 13:03:47 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 2758920f11
commit a85d0b76cc
15 changed files with 208 additions and 54 deletions
@@ -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'
+8
View File
@@ -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);
}
}