Use when adding a SLICC shell command, dedicated tool, provider, scoop capability, UI panel, interactive approval, or runtime skill wiring; when `command not found`, a tool/skill is undiscovered, a follower action is dropped, OAuth reports `session expired` or `401 invalid x-api-key`, or `playwright-cli-sync` reports a command gap. Covers exact file paths, code interfaces, registration patterns, and the cross-reference checklist (test, SKILL.md update, follower handler, AGENTS.md).
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Use when adding a SLICC shell command, dedicated tool, provider, scoop capability, UI panel, interactive approval, or runtime skill wiring; when `command not found`, a tool/skill is undiscovered, a follower action is dropped, OAuth reports `session expired` or `401 invalid x-api-key`, or `playwright-cli-sync` reports a command gap. Covers exact file paths, code interfaces, registration patterns, and the cross-reference checklist (test, SKILL.md update, follower handler, AGENTS.md).
adding-slicc-features
Use this procedure to extend SLICC. Prefer shell commands over dedicated tools, preserve CLI and extension parity, and update every agent-facing reference that exposes changed behavior.
# Validate TypeScript and behavior
npm run typecheck
npm test# Validate all repository and skill lint gates
npm run lint
# Check playwright-cli command parity after adding/changing a subcommand
node packages/dev-tools/tools/playwright-cli-sync.mjs
# Build the extension for dual-mode verification
npm run build -w @slicc/chrome-extension
Use these common extension points:
Capability
Primary path
Supplemental shell command
packages/webapp/src/shell/supplemental-commands/
Executable .jsh skill command
packages/vfs-root/workspace/skills/<skill>/
Core agent tool
packages/webapp/src/tools/ and packages/webapp/src/scoops/scoop-context.ts
http, browser, skill, cli, color, time, fmt, pool, usb / serial / hid — see packages/vfs-root/workspace/skills/skill-authoring/jsh-runtime-extensions.md.
require(id)
Synchronous CJS require (require('sliccy:<name>'), require('fs'), or installed packages).
module, exports
Available for CJS module pattern (e.g., a .jsh consumed by require('./helper.jsh')).
Discovery:
The shell auto-discovers *.jsh files from /workspace/skills/ (priority) and anywhere on the VFS. Call by basename:
my-script arg1 arg2
Execution modes:
CLI mode: Uses AsyncFunction constructor, full Node.js-like globals
Extension mode: Routes through sandbox iframe (CSP-compliant), via postMessage for VFS operations
Test pattern:
JSH scripts cannot be unit-tested in Node because they rely on extension mode detection. Test the logic separately:
When: To add or change browser automation behavior, tab workflows, or preview-serving commands.
Files to modify:
Add a handler under packages/webapp/src/shell/supplemental-commands/playwright/handlers/ (one module per subcommand family) and register it in playwright/handlers/index.ts.
Shared helpers live in playwright/ (state.ts, snapshot.ts, session-log.ts, teleport.ts, teleport-storage.ts, discover.ts, help.ts); playwright-command.ts is just the thin dispatcher + public re-exports.
Update: packages/vfs-root/workspace/skills/playwright-cli/SKILL.md — this file is injected into the agent's system prompt. Every new or changed command MUST be reflected here or the agent will not know about it.
Implementation:
Keep browser automation shell-first through playwright-cli / playwright / puppeteer.
A handler is a PlaywrightHandler — (ctx: { browser, fs, state, positional, flags }) => Promise<CmdResult>; add the subcommand name (and any alias) to the playwrightHandlers map.
Reuse shared preview helpers for VFS URLs instead of manually constructing /preview/... paths.
Use serve <dir> for app directories (default index.html, optional --entry) and open for single files, URLs, downloads, or inline image viewing.
Preserve the current tab + snapshot model (the shared PlaywrightState in playwright/state.ts) when adding stateful browser actions.
Test pattern:
Add tests in packages/webapp/tests/ mirroring the command's src/ path (for example tests/shell/supplemental-commands/playwright-command.test.ts).
Put pure helper coverage in shared.test.ts.
Prefer focused command-level assertions over large integration fixtures.
Alignment with official playwright-cli:
When adding a new playwright-cli subcommand, also update
packages/webapp/src/shell/supplemental-commands/playwright/slicc-commands.json
and run node packages/dev-tools/tools/playwright-cli-sync.mjs to confirm the gap
is closed. If you're implementing a command that the official CLI already has, cross-
reference its args and flags in help.json first. Full workflow: docs/playwright-cli-sync.md.
When: To add a new tab or section in the UI (e.g., a settings panel, network monitor).
Architecture note: The legacy Layout/ChatPanel UI was removed during the
web-components migration. The current UI shell is built on @slicc/webcomponents
(see packages/webcomponents/). New panels are web components mounted via the
wc-shell.ts / wc-live.ts controllers in packages/webapp/src/ui/wc/.
Modify: packages/webcomponents/src/index.ts (register the element)
Modify: packages/webapp/src/ui/wc/wc-live.ts (mount and wire the panel)
The @slicc/webcomponents library provides the UI primitives (Storybook +
@vitest/browser for testing). The packages/webapp/src/ui/wc/ controllers
handle mounting, scoop lifecycle events, and leader/follower behavior.
Test pattern: Web component tests use @vitest/browser (real Chromium):
npm test -w @slicc/webcomponents # browser-mode Vitest
Reference files:
packages/webcomponents/src/ — existing web component implementations
---
name: my-skill
description: Teaches the agent how to do X
---# My Skill
You are an expert in [domain]. Your role is to [responsibility].
## Key Principles1. Always [principle 1]
2. Consider [principle 2]
## Example
When the user asks for X, follow this approach:
- Step 1: [description]
- Step 2: [description]
- Step 3: [description]
Use the `bash` tool to run commands. Use `read_file` to inspect files.
## Output Format
Always provide:
- A brief summary
- Code blocks (when applicable)
- Relevant file paths
How it works:
Skills are auto-discovered from native /workspace/skills/ plus any accessible .agents/skills/*/SKILL.md and .claude/skills/*/SKILL.md directories anywhere in the reachable VFS during scoop initialization. Headers are shown by default; full content is loaded on demand.
With executable script:
# packages/vfs-root/workspace/skills/my-skill/SKILL.md## Command: my-skill-cmd
Run `my-skill-cmd arg1` to process files:
During ScoopContext.init(), SLICC starts from /workspace/skills/ (cone) or /scoops/{folder}/workspace/skills/ (scoop), then also considers any accessible .agents/skills/*/SKILL.md and .claude/skills/*/SKILL.md roots elsewhere in that runtime's reachable VFS. The agent's system prompt includes discovered skill headers and can request full content via read_file.
Only native /workspace/skills/ entries are install-managed by SLICC. Compatibility-discovered .agents and .claude skills remain read-only unless you explicitly copy/package them into the native skills directory.
Test pattern:
Skills are narrative instructions; test by verifying they load correctly:
Pi-ai auto-discovery: getProviders() returns all pi-ai providers automatically — no files needed. Filtered by packages/dev-tools/providers.build.json (include: ["*"] = all, exclude: ["*"] = none).
Built-in extensions: packages/webapp/src/providers/built-in/*.ts — only for providers needing custom register() functions (e.g., bedrock-camp). Also filtered by packages/dev-tools/providers.build.json.
External: packages/webapp/providers/*.ts (gitignored within the webapp package) — always included, never filtered. For custom OAuth providers, corporate proxies, etc. Some providers (e.g., adobe.ts) are explicitly un-gitignored and tracked in version control.
Built-in and external modules export config: ProviderConfig and optionally register(): void.
8a. Add an API-Key Provider
When: To support a new LLM provider that uses an API key (e.g., Groq, Hugging Face).
Most providers need no files at all. Pi-ai auto-discovers its providers via getProviders(), and provider-settings.ts generates a fallback config (display name derived from ID, requiresApiKey: true, requiresBaseUrl: false). The provider appears in the Settings UI automatically.
Only create a file in packages/webapp/src/providers/built-in/ if the provider needs a custom register() function (e.g., custom stream functions). See packages/webapp/src/providers/built-in/bedrock-camp.ts for an example.
For external providers (typically gitignored), create packages/webapp/providers/my-provider.ts:
The CLI redirect URI uses the sliccy.ai relay which decodes the OAuth state parameter to find the localhost port. Encode {port, path, nonce} as base64 JSON in the state param. See packages/webapp/providers/adobe.ts for the pattern.
Type:
interfaceProviderConfig {
id: string;
name: string;
description: string;
requiresApiKey: boolean;
apiKeyPlaceholder?: string;
apiKeyEnvVar?: string;
requiresBaseUrl: boolean; // shown for non-OAuth; also shown for OAuth providers when truebaseUrlPlaceholder?: string;
baseUrlDescription?: string;
isOAuth?: boolean;
onOAuthLogin?: (launcher: OAuthLauncher, onSuccess: () => void) =>Promise<void>;
onOAuthLogout?: () =>Promise<void>;
/** Static per-model capability overrides. */modelOverrides?: Record<string, ModelMetadata>;
/** Return model IDs with optional metadata (resolved against Anthropic registry). */getModelIds?: () =>Array<{ id: string; name?: string } & ModelMetadata>;
}
/** Wire format for model capabilities (snake_case, merged into camelCase Model objects). */interfaceModelMetadata {
api?: 'anthropic' | 'openai'; // stream function routingcontext_window?: number; // context window in tokensmax_tokens?: number; // max output tokensreasoning?: boolean; // supports thinking/reasoninginput?: string[]; // input modalities (['text', 'image'])
}
typeOAuthLauncher = (authorizeUrl: string) =>Promise<string | null>;
requiresBaseUrl for OAuth providers: By default, the base URL field is hidden for OAuth providers. Set requiresBaseUrl: true to show it — useful for providers where the proxy endpoint is configurable at runtime. The base URL is saved to the account before onOAuthLogin is called, so the provider can read it via getBaseUrlForProvider(). The saveOAuthAccount() function preserves the existing baseUrl through re-logins.
getModelIds: When present, getProviderModels() uses this instead of returning all Anthropic models. Each ID is resolved against the Anthropic model registry; unknown IDs get fallback model objects with sensible defaults. Can return optional ModelMetadata fields per model — these override pi-ai defaults. Set api: 'openai' to route a model through streamOpenAICompletions instead of streamAnthropic.
modelOverrides: Static per-model overrides applied to all models for this provider. Useful for config-only providers (like Azure AI Foundry) that can't implement getModelIds() but need custom context windows. Example: modelOverrides: { 'claude-opus-4-6': { context_window: 1000000 } }.
refreshModels (optional, (accessToken?) => Promise<void>): the async populate step for a dynamic model list. getModelIds() is synchronous and only reads caches; refreshModels is where the provider fetches its /v1/models-style list, caches it, and persists the enriched result to localStorage (so cold consumers — notably the cloud cone's kernel worker, which reads localStorage — see the full set + metadata on first resolve). Normally this runs inside onOAuthLogin. Floats that inject an account without an interactive login (the cloud cone, via applyHostedAccounts) must call it explicitly — prewarmHostedModels in ui/hosted-config-apply.ts does this before applying the account (the account write triggers the worker's model resolution, so the list must be warm first). The optional accessToken lets callers pre-warm before the account is persisted. Without it, an OAuth model id pi-ai's registry doesn't know (e.g. claude-opus-4-8) still routes through the provider (see resolution note below) but with default metadata until the list warms.
OAuth-safe resolution: resolveModelById / resolveCurrentModel (ui/provider-settings.ts) never fall back to a native Anthropic model for an OAuth/custom provider. An unknown model id is routed through the provider (api: '${providerId}-anthropic') via buildProviderRoutedModel — otherwise the provider's token (e.g. an Adobe IMS token) would be sent to api.anthropic.com and rejected with 401 invalid x-api-key.
Three-layer merge: Model capabilities resolve as pi-ai registry (defaults) → modelOverrides (static overrides) → getModelIds() metadata (dynamic, highest priority). Each layer only overrides fields it provides.
Model ID pitfall: Use pi-ai alias IDs (e.g., claude-opus-4-6) not dated IDs (e.g., claude-opus-4-6-20250626). In the browser bundle, getModel() returns undefined for unknown IDs instead of throwing, and { ...undefined } silently produces {}. The alias resolves to a full model from the registry with all required fields.
Base URL validation: When requiresBaseUrl: true is set on an OAuth provider and no build-time default exists (empty proxyEndpoint in config), the login button validates that a URL was entered. Users cannot proceed without providing a proxy endpoint.
Test pattern:
OAuth flow is runtime-dependent (browser popups, chrome.identity). Test the provider's token extraction and account saving logic in isolation:
Test file in packages/*/tests/ mirroring the src/ structure
Pure-logic tests added (avoid DOM/chrome.* testing in vitest unless necessary)
Extension mode compatibility verified (CSP, chrome.runtime.getURL, sandbox iframe if needed)
Dual-mode tested (CLI + extension)
Logging added (createLogger('namespace'))
Agent-facing SKILL.md updated when a shell command or workflow changes
Matching follower handler and UI action added for leader broadcasts
Root/package AGENTS.md or CLAUDE.md updated if the architectural pattern is new
No sensitive data logged or stored in localStorage unencrypted
Build & Test
# Type-check both browser and CLI
npm run typecheck
# Run tests
npm run test# Standalone dev
npm run dev
# Extension dev
npm run build -w @slicc/chrome-extension
# Then load dist/extension in chrome://extensions
When: A shell command or tool needs user interaction before proceeding (e.g., permission approval, file picker, form input). Tool UI solves the "user gesture" problem — browser APIs like showDirectoryPicker() require a user click, but agent-driven tool calls have no gesture context. For the broader gate-pattern context (sudo, device gates, OS capture gates), see docs/approvals.md.
Files to modify:
Your command file (e.g., packages/webapp/src/fs/mount-commands.ts)
Import from: packages/webapp/src/tools/tool-ui.ts
How it works:
Tool execution sets up a context with onUpdate callback (handled automatically by tool-adapter.ts)
Shell commands call showToolUIFromContext() to render interactive HTML in the chat
User clicks a button → callback runs with user gesture context → can call restricted APIs
Promise resolves with user's action/data
Implementation (from mount command):
import { getToolExecutionContext, showToolUIFromContext } from'../tools/tool-ui.js';
asyncfunctionexecute(args: string[]): Promise<ShellResult> {
// Check if running in agent context (no user gesture)const toolContext = getToolExecutionContext();
if (toolContext) {
// Agent-driven: show approval UI.// IMPORTANT: escape dynamic values before interpolating into HTML to prevent// injection — a crafted path could spoof the approval surface.const safePath = targetPath.replace(
/[&<>"']/g,
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c
);
const result = awaitshowToolUIFromContext({
html: `
<div class="tool-ui">
<p>The agent wants to access <code>${safePath}</code></p>
<div class="tool-ui__actions">
<button class="tool-ui__btn tool-ui__btn--primary" data-action="approve">
Approve
</button>
<button class="tool-ui__btn tool-ui__btn--secondary" data-action="deny">
Deny
</button>
</div>
</div>
`,
onAction: async (action) => {
if (action === 'approve') {
// Runs with user gesture! Can call showDirectoryPicker(), etc.const handle = awaitwindow.showDirectoryPicker();
return { approved: true, handle };
}
return { approved: false };
},
});
if (!result?.approved) {
return { stdout: '', stderr: 'User denied', exitCode: 1 };
}
// Use result.handle...
} else {
// Terminal/user-driven: has gesture, call API directlyconst handle = awaitwindow.showDirectoryPicker();
}
}
HTML conventions:
Wrap content in <div class="tool-ui">
Use data-action="actionName" on buttons for click handling
Use data-action-data='{"key":"value"}' for additional data (JSON)
Available button classes: .tool-ui__btn--primary, .tool-ui__btn--secondary
Forms: add data-action="submit" to form, fields become action data
// Get current tool execution context (null if not in a tool)getToolExecutionContext(): ToolExecutionContext | null// Show UI and wait for user action (returns null if no context)showToolUIFromContext(request: {
html: string;
onAction?: (action: string, data?: unknown) =>Promise<unknown> | unknown;
}): Promise<unknown | null>
// Lower-level: show UI with explicit onUpdate callbackshowToolUI(request: ToolUIRequest, onUpdate: OnUpdateCallback): Promise<unknown>
User clicks button with data-action → onAction callback fires with gesture context
Callback return value resolves the showToolUIFromContext() promise
UI is automatically cleaned up when tool execution ends
Extension vs CLI mode:
CLI mode: HTML rendered directly in DOM with click handlers
Extension mode: HTML rendered in CSP-exempt sandbox iframe, actions posted via postMessage
Both modes handle data-action clicks and form submissions identically.
Common Patterns
Error handling: Wrap async operations in try/catch. Return { content: errorMsg, isError: true } for tools.
Logging: Import createLogger('namespace') from packages/webapp/src/core/logger.js. Logs are filtered by level (DEBUG in dev, ERROR in prod).
VFS access: All core layers have access to VirtualFS. Scoops get RestrictedFS (path-based ACL).
Shell commands: Prefer shell commands (bash tool) for new capabilities. Dedicated tools only if the capability needs binary data (browser screenshots, network recording).
Browser automation: Use playwright-cli / playwright / puppeteer for tab control. Use serve <dir> for app directories and open for single preview files.