スキル一覧に戻る
johnlindquist

script-kit-executor

by johnlindquist

Script Kit rewritten in Rust using GPUI (Zed's UI framework)

16🍴 2📅 2026年1月23日
GitHubで見るManusで実行

SKILL.md


name: script-kit-executor description: Script execution engine and builtins for script-kit-gpui

script-kit-executor

The executor module is responsible for running TypeScript/JavaScript scripts with bidirectional JSONL communication. It handles process lifecycle management, SDK preloading, scriptlet execution, error handling, and selected text operations.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        Script Execution Flow                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   User selects script                                                │
│          │                                                           │
│          ▼                                                           │
│   ┌──────────────────┐                                               │
│   │ execute_script_  │  1. Find SDK path (~/.scriptkit/sdk/)         │
│   │ interactive()    │  2. Find bun/node executable                  │
│   │ [runner.rs]      │  3. Spawn with process_group(0)               │
│   └────────┬─────────┘                                               │
│            │                                                         │
│            ▼                                                         │
│   ┌──────────────────┐     ┌──────────────────┐                      │
│   │  ScriptSession   │────▶│  SplitSession    │                      │
│   │  (unified)       │     │  (for threading) │                      │
│   └────────┬─────────┘     └──────────────────┘                      │
│            │                        │                                │
│            │ split()                │                                │
│            ▼                        ▼                                │
│   ┌──────────────────┐     ┌──────────────────┐                      │
│   │  Writer Thread   │     │  Reader Thread   │                      │
│   │  (stdin)         │     │  (stdout/stderr) │                      │
│   └──────────────────┘     └──────────────────┘                      │
│            │                        │                                │
│            │  JSONL Messages        │                                │
│            ▼                        ▼                                │
│   ┌────────────────────────────────────────────┐                     │
│   │          Script Process (bun/node)          │                    │
│   │  ┌──────────────────────────────────────┐  │                     │
│   │  │         SDK Preload (kit-sdk.ts)     │  │                     │
│   │  │  - Global functions: arg, div, md    │  │                     │
│   │  │  - IPC message handling              │  │                     │
│   │  └──────────────────────────────────────┘  │                     │
│   └────────────────────────────────────────────┘                     │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Execution Flow

1. Script Discovery and Validation

When a script is selected, the executor:

  1. Validates the path exists and has proper encoding
  2. Determines file type (TypeScript .ts or JavaScript .js)
  3. Locates the SDK for preloading globals

2. Runtime Selection

The executor tries runtimes in order of preference:

1. bun with SDK preload (preferred for TypeScript)
   └── Command: bun run --preload ~/.scriptkit/sdk/kit-sdk.ts script.ts

2. bun without preload (fallback)
   └── Command: bun run script.ts

3. node (for JavaScript files)
   └── Command: node script.js

3. Process Spawning

Scripts are spawned with:

  • Piped stdio: stdin, stdout, stderr all captured
  • Process groups (Unix): process_group(0) creates new PGID equal to PID
  • Process tracking: Registered with global PROCESS_MANAGER
// Key spawn configuration
let mut command = Command::new(&executable);
command
    .args(args)
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped());

#[cfg(unix)]
command.process_group(0);  // PID becomes PGID

4. Session Splitting

After spawn, the session is split for concurrent I/O:

pub struct SplitSession {
    pub stdin: ChildStdin,           // Writer thread owns this
    pub stdout_reader: JsonlReader,   // Reader thread owns this
    pub stderr: Option<ChildStderr>,  // Captured for error reporting
    pub child: Child,
    pub process_handle: ProcessHandle, // MUST stay alive until script exits
}

IPC Communication

JSONL Protocol

All communication uses newline-delimited JSON (JSONL):

Script → App:  {"type":"ARG","prompt":"Enter name"}\n
App → Script:  {"type":"SUBMIT","id":"abc123","value":"John"}\n

Message Handling Categories

  1. Prompt Messages → Sent to UI via async_channel
  2. Direct Handlers → Processed in reader thread:
    • GetSelectedText, SetSelectedText
    • CheckAccessibility, RequestAccessibility
    • Clipboard, ClipboardHistory
    • WindowList, WindowAction

Writer Thread

// Bounded channel prevents OOM from slow scripts
let (response_tx, response_rx) = mpsc::sync_channel::<Message>(100);

// Writer thread serializes and sends responses
loop {
    match response_rx.recv() {
        Ok(response) => {
            let json = serialize_message(&response)?;
            writeln!(stdin, "{}", json)?;
            stdin.flush()?;
        }
        Err(_) => break,
    }
}

Reader Thread

// Event-driven message reading
loop {
    match stdout_reader.next_message_graceful_with_handler(|issue| {
        // Handle protocol parse issues
    }) {
        Ok(Some(msg)) => {
            // Handle message based on type
            match &msg {
                Message::GetSelectedText { .. } => handle_directly(),
                _ => send_to_ui(tx.send_blocking(msg)),
            }
        }
        Ok(None) => break,  // EOF
        Err(e) => break,    // Error
    }
}

Builtins

The builtins.rs module provides built-in features that appear in the main search alongside scripts:

Feature Categories

CategoryFeatures
CoreClipboard History, Window Switcher, AI Chat, Notes
System ActionsLock Screen, Sleep, Restart, Shut Down, Volume controls
Window ActionsTile Left/Right/Top/Bottom, Maximize, Minimize
Script CommandsNew Script, New Scriptlet
PermissionsCheck Permissions, Request Accessibility
UtilitiesScratch Pad, Quick Terminal

Built-in Entry Structure

pub struct BuiltInEntry {
    pub id: String,           // "builtin-clipboard-history"
    pub name: String,         // "Clipboard History"
    pub description: String,  // "View and manage clipboard history"
    pub keywords: Vec<String>,// ["clipboard", "paste", "copy"]
    pub feature: BuiltInFeature,
    pub icon: Option<String>, // Emoji icon
    pub group: BuiltInGroup,  // Core or MenuBar
}

Configuration

Builtins are toggled via BuiltInConfig:

pub struct BuiltInConfig {
    pub clipboard_history: bool,  // default: true
    pub app_launcher: bool,       // default: true (apps in main search)
    pub window_switcher: bool,    // default: true
}

Scriptlet Execution

Scriptlets are small scripts embedded in markdown, supporting various tool types:

Supported Tools

ToolExtensionRuntime
bash, sh, zsh, fish.shShell
python.pypython3
ruby.rbruby
node, js.jsnode
kit, ts, bun, deno.tsbun with SDK
applescript-osascript
transform.tsbun (get/set selected text)
open-open/xdg-open
paste, type, submit-Accessibility (macOS only)

Scriptlet Execution Flow

pub fn run_scriptlet(scriptlet: &Scriptlet, options: ScriptletExecOptions) {
    // 1. Process conditionals (if/else based on flags)
    let content = process_conditionals(&content, &options.flags);
    
    // 2. Variable substitution
    let content = format_scriptlet(&content, &options.inputs, &options.positional_args);
    
    // 3. Apply prepend/append
    let content = build_final_content(&content, &options.prepend, &options.append);
    
    // 4. Execute based on tool type
    match tool {
        "bash" | "sh" | "zsh" => execute_shell_scriptlet(),
        "python" => execute_with_interpreter("python3"),
        "kit" | "ts" => execute_typescript(),
        "transform" => execute_transform(),  // macOS only
        // ...
    }
}

Error Handling

Stack Trace Parsing

pub fn parse_stack_trace(stderr: &str) -> Option<String> {
    // Looks for patterns:
    // - Lines starting with "at "
    // - Error:, TypeError:, ReferenceError:, SyntaxError:
    // Returns up to 20 lines of stack trace
}

Suggestion Generation

Based on error patterns and exit codes:

PatternSuggestion
"cannot find module"Run 'bun install'
"syntaxerror"Check for syntax errors
"referenceerror"Check imports/definitions
Exit code 127Command not found
Exit code 137 (SIGKILL)Out of memory or killed
Exit code 139 (SIGSEGV)Memory access violation

Stderr Buffer

A ring buffer captures stderr for post-mortem analysis:

pub struct StderrBuffer {
    lines: Arc<Mutex<VecDeque<String>>>,
    max_lines: usize,    // default: 500
    max_bytes: usize,    // default: 4KB
}

// Usage
let capture = spawn_stderr_reader(stderr, script_path);
// ... script runs ...
let stderr_text = capture.get_contents_with_timeout(Duration::from_millis(100));

Process Cleanup

ProcessHandle

Tracks process lifetime and ensures cleanup:

pub struct ProcessHandle {
    pid: u32,
    script_path: String,
    killed: bool,
}

impl Drop for ProcessHandle {
    fn drop(&mut self) {
        PROCESS_MANAGER.unregister_process(self.pid);
        self.kill();  // SIGTERM → wait 250ms → SIGKILL
    }
}

Kill Escalation Protocol

1. Send SIGTERM to process group
2. Poll every 50ms for up to 250ms
3. If still alive, send SIGKILL

Auto-Submit Mode

For autonomous testing, enable via environment variables:

VariableDescription
AUTO_SUBMIT=trueEnable auto-submit
AUTO_SUBMIT_DELAY_MS=200Delay before submit (default: 100)
AUTO_SUBMIT_VALUE=fooOverride value to submit
AUTO_SUBMIT_INDEX=2Select choice by index
pub struct AutoSubmitConfig {
    pub enabled: bool,
    pub delay: Duration,
    pub value_override: Option<String>,
    pub index: usize,
}

Anti-patterns

1. Dropping ProcessHandle Early

// WRONG: ProcessHandle dropped, kills the script immediately
let session = execute_script_interactive(&path)?;
drop(session);  // Script is killed!

// CORRECT: Keep session alive until script completes
let session = execute_script_interactive(&path)?;
// ... use session ...
let exit_code = session.wait()?;

2. Blocking on stdin/stdout in Same Thread

// WRONG: Deadlock risk
loop {
    let msg = session.receive_message()?;  // Blocks
    session.send_message(&response)?;       // Can't reach if blocked
}

// CORRECT: Split into separate threads
let split = session.split();
std::thread::spawn(move || { /* writer */ });
std::thread::spawn(move || { /* reader */ });

3. Ignoring Stderr

// WRONG: Stderr lost, no error context
let output = command.output()?;
println!("{}", String::from_utf8_lossy(&output.stdout));

// CORRECT: Capture stderr for error reporting
let stderr_capture = spawn_stderr_reader(child.stderr.take().unwrap(), path);
// ... on error ...
let stderr = stderr_capture.get_contents_with_timeout(Duration::from_millis(100));

4. Not Using process_group(0)

// WRONG: Child processes become orphans
command.spawn()?;

// CORRECT: All children share PGID, can be killed together
#[cfg(unix)]
command.process_group(0);
command.spawn()?;

5. Unbounded Channels

// WRONG: OOM risk if script doesn't consume messages
let (tx, rx) = async_channel::unbounded();

// CORRECT: Backpressure via bounded channel
let (tx, rx) = async_channel::bounded(100);

Key Files

FilePurpose
runner.rsCore execution, process spawning, SDK loading
scriptlet.rsScriptlet parsing and tool-specific execution
errors.rsStack trace parsing, suggestion generation
selected_text.rsGetSelectedText/SetSelectedText handlers
stderr_buffer.rsRing buffer for stderr capture
auto_submit.rsTesting automation configuration
mod.rsModule exports and re-exports

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です