Compare commits

..

6 Commits

Author SHA1 Message Date
Dominic Cooney eab96e6a8a Simplify symbol formatting: omit () from all symbol names
Appending () is language-specific and can mislead for TypeScript,
Obj-C, etc. The kind suffix (— function, — class, etc.) already
communicates the symbol type clearly.
2026-03-27 15:15:48 +09:00
Dominic Cooney 00526721d7 Gate telemetry behind isCategoryEnabled('code_intelligence')
Without this, the code_intelligence category in the telemetry defaults
map had no effect — captureToolUsage doesn't check per-category gating.
Now telemetry is only emitted when the category is enabled, matching the
pattern used by browser, checkpoints, skills, focus_chain, and subagents.
2026-03-27 15:03:36 +09:00
Dominic Cooney 9852d459ba Address review feedback: fix validation ordering and symbol formatting
- Move consecutiveMistakeCount reset after parseQueries validation so
  empty-but-present queries (only comments/whitespace) correctly
  increment the mistake counter instead of silently resetting it
- Move say() call after query parsing so no dangling tool-start UI
  message appears when queries are invalid
- Only append () to callable symbol kinds (function, method, constructor)
  instead of unconditionally on all symbols, preventing misleading
  output like MyClass() or MY_CONSTANT()
2026-03-27 14:53:33 +09:00
Dominic Cooney 9153f36989 Fix cross-platform path handling in CodeIntelligenceToolHandler
Use toPosix() to normalize Windows backslash paths before splitting
in shortenPath(), ensuring consistent display on all platforms (Windows
JetBrains included).
2026-03-27 14:04:51 +09:00
Dominic Cooney 08047d6efd Add code intelligence settings UI, tool gating, and tests
- Add codeIntelligenceEnabled setting to state-keys, proto, and updateSettings
- Gate code_intelligence tool behind codeIntelligenceAvailable context flag
- Add settings toggle in Experimental section, only visible when PSI available
- Wire codeIntelligenceEnabled/Available through Controller -> webview state
- Add telemetry capture for code-intelligence tool usage
- Add CodeIntelligenceToolHandler unit tests (9 tests)
- Add code-intelligence context variation to system prompt integration tests
- Generate 12 new snapshots for code-intelligence across all model families
2026-03-27 13:51:27 +09:00
Dominic Cooney b970af74ca Add a code intelligence service. 2026-03-27 13:51:27 +09:00
3650 changed files with 408575 additions and 417689 deletions
-208
View File
@@ -1,208 +0,0 @@
---
name: cline-sdk
description: Comprehensive Cline SDK skill for building AI agents. Covers the Agent runtime, ClineCore sessions, custom tools, plugins, events, LLM providers, scheduling, multi-agent teams, and production deployment. Use for any task involving @cline/sdk or its sub-packages.
metadata:
references: agent, clinecore
---
# Cline SDK Skill
Consolidated skill for building AI agents with the Cline SDK. Use the decision trees below to find the right entry point and API surface, then load detailed references.
## Critical Rules
Follow these rules in all Cline SDK code:
1. Install with `npm install @cline/sdk`. The `@cline/sdk` package re-exports everything from `@cline/core`, `@cline/agents`, `@cline/llms`, and `@cline/shared`.
2. Requires Node.js 22 or later.
3. Use `createTool()` from `@cline/sdk` (or `@cline/shared`) to define tools. Tool names must be `snake_case`.
4. Return errors as structured data from tool `execute` functions. Throwing counts as a "mistake" against the agent's mistake limit.
5. Use `lifecycle: { completesRun: true }` on tools that should end the agent loop (e.g. a "submit answer" tool).
6. When using `ClineCore`, always call `dispose()` when done to clean up resources.
7. The standalone `Agent` and `ClineCore` have different event systems. For `Agent`: use `agent.subscribe()` to get `AgentRuntimeEvent` types (text streaming is `"assistant-text-delta"`, result text is `result.outputText`). For `ClineCore`: use `cline.subscribe()` to get `CoreSessionEvent` types (text streaming is `"chunk"` with `payload.type === "text"`, result text is `result.text`). There is no top-level `onEvent` field on `AgentRuntimeConfig` -- use `agent.subscribe()` or `hooks.onEvent` instead. Do not use event types like `"content_update"` or `"content_start"` with `agent.subscribe()` -- those are internal legacy types from the ClineCore adapter layer.
## How to Use This Skill
### Reference File Structure
The two main API surfaces (`Agent` and `ClineCore`) follow a 4-file pattern. Cross-cutting concepts are single-file guides.
Each main API surface in `./references/<api>/` contains:
| File | Purpose | When to Read |
|------|---------|--------------|
| `REFERENCE.md` | Overview, when to use, quick start | Always read first |
| `api.md` | Full API: classes, methods, config, types | Writing code |
| `patterns.md` | Common patterns, best practices | Implementation guidance |
| `gotchas.md` | Pitfalls, limitations, debugging | Troubleshooting |
Cross-cutting concepts in `./references/<concept>/` have `REFERENCE.md` as the entry point.
### Reading Order
1. Start with `REFERENCE.md` for your chosen API surface
2. Then read additional files relevant to your task:
- Writing agent code -> `api.md`
- Common patterns -> `patterns.md`
- Creating tools -> `tools/REFERENCE.md`
- Adding plugins/hooks -> `plugins/REFERENCE.md`
- Configuring LLM providers -> `providers/REFERENCE.md`
- Streaming events -> `events/REFERENCE.md`
- Deploying to production -> `production/REFERENCE.md`
- Scheduling agents -> `scheduling/REFERENCE.md`
- Multi-agent orchestration -> `multi-agent/REFERENCE.md`
- Debugging -> `gotchas.md`
### Example Paths
```
./references/agent/REFERENCE.md # Start here for lightweight agents
./references/clinecore/REFERENCE.md # Start here for full runtime
./references/agent/api.md # Agent class, config, methods
./references/tools/REFERENCE.md # Creating and using tools
./references/plugins/REFERENCE.md # Plugin system
./references/providers/REFERENCE.md # LLM provider configuration
```
## Quick Decision Trees
### "Which API surface should I use?"
```
Which API?
+-- I want a simple, stateless agent with custom tools
| +-- agent/ (Agent class from @cline/agents)
+-- I need session persistence, built-in tools, config discovery
| +-- clinecore/ (ClineCore from @cline/core)
+-- I want built-in file/shell/search/web tools
| +-- clinecore/ (has built-in tools; Agent does not)
+-- I want scheduled or recurring agents
| +-- clinecore/ (automation API)
+-- I need multi-process or multi-client session sharing
| +-- clinecore/ (hub-backed runtime)
+-- I'm building a browser-compatible agent
| +-- agent/ (no Node.js dependencies)
```
### "I need to create tools"
```
Tools?
+-- Define a custom tool with schema -> tools/REFERENCE.md
+-- Use built-in tools (bash, editor, read_files) -> tools/REFERENCE.md (built-in section)
+-- Control tool approval/policies -> tools/REFERENCE.md (policies section)
+-- Tool that ends the agent loop -> tools/REFERENCE.md (completion tools)
+-- Package tools as a reusable plugin -> plugins/REFERENCE.md
```
### "I need to handle events"
```
Events?
+-- Stream text/reasoning in real time -> events/REFERENCE.md
+-- Track token usage and costs -> events/REFERENCE.md
+-- Watch tool calls -> events/REFERENCE.md
+-- Detect completion/errors -> events/REFERENCE.md
+-- Hook into lifecycle stages -> plugins/REFERENCE.md
```
### "I need to configure a model provider"
```
Providers?
+-- Anthropic (Claude) -> providers/REFERENCE.md
+-- OpenAI (GPT) -> providers/REFERENCE.md
+-- Google (Gemini/Vertex) -> providers/REFERENCE.md
+-- AWS Bedrock -> providers/REFERENCE.md
+-- Mistral -> providers/REFERENCE.md
+-- OpenAI-compatible (vLLM, Together, etc.) -> providers/REFERENCE.md
+-- Custom/self-hosted provider -> providers/REFERENCE.md
```
### "I need plugins or hooks"
```
Plugins?
+-- Package tools + hooks together -> plugins/REFERENCE.md
+-- Observe tool calls (logging, metrics) -> plugins/REFERENCE.md
+-- Intercept lifecycle events -> plugins/REFERENCE.md
+-- Add system prompt rules -> plugins/REFERENCE.md
+-- Distribute via npm/git -> plugins/REFERENCE.md
```
### "I need multi-agent coordination"
```
Multi-agent?
+-- Spawn one-off background agents -> multi-agent/REFERENCE.md (sub-agents)
+-- Persistent cross-session teams -> multi-agent/REFERENCE.md (teams)
+-- Parent-child delegation -> multi-agent/REFERENCE.md (sub-agents)
+-- Peer-to-peer task board -> multi-agent/REFERENCE.md (teams)
```
### "I need scheduling or automation"
```
Scheduling?
+-- Recurring cron jobs -> scheduling/REFERENCE.md
+-- One-off scheduled tasks -> scheduling/REFERENCE.md
+-- Event-driven triggers -> scheduling/REFERENCE.md
+-- CLI schedule management -> scheduling/REFERENCE.md
```
### "I need to go to production"
```
Production?
+-- Error handling and status checks -> production/REFERENCE.md
+-- Cost control and token limits -> production/REFERENCE.md
+-- Observability (OpenTelemetry) -> production/REFERENCE.md
+-- Security and sandboxing -> production/REFERENCE.md
+-- Deployment patterns -> production/REFERENCE.md
```
### Troubleshooting Index
- Agent loop not stopping -> `tools/REFERENCE.md` (completion tools)
- Tool errors crashing the agent -> `agent/gotchas.md` or `clinecore/gotchas.md`
- Provider auth failures -> `providers/REFERENCE.md`
- Session not persisting -> `clinecore/gotchas.md`
- Token usage too high -> `production/REFERENCE.md` (cost control)
- Hub connection issues -> `clinecore/gotchas.md`
- Plugin not loading -> `plugins/REFERENCE.md`
- Events not firing -> `events/REFERENCE.md`
## Product Index
### API Surfaces
| API | Entry File | Description |
|-----|------------|-------------|
| Agent | `./references/agent/REFERENCE.md` | Lightweight stateless agent loop |
| ClineCore | `./references/clinecore/REFERENCE.md` | Full runtime with sessions, persistence, built-in tools |
### Cross-Cutting Concepts
| Concept | Entry File | Description |
|---------|------------|-------------|
| Tools | `./references/tools/REFERENCE.md` | Built-in and custom tool creation |
| Plugins | `./references/plugins/REFERENCE.md` | Extension system with hooks |
| Events | `./references/events/REFERENCE.md` | Real-time streaming events |
| Providers | `./references/providers/REFERENCE.md` | LLM provider configuration |
| Production | `./references/production/REFERENCE.md` | Deployment, security, observability |
| Scheduling | `./references/scheduling/REFERENCE.md` | Cron jobs and automation |
| Multi-Agent | `./references/multi-agent/REFERENCE.md` | Teams and sub-agents |
### Package Map
| Package | Purpose |
|---------|---------|
| `@cline/sdk` | Everything you need, install this one |
| `@cline/core` | Sessions, persistence, built-in tools, config, hub |
| `@cline/agents` | Stateless agent loop, tool orchestration, streaming |
| `@cline/llms` | LLM provider gateway |
| `@cline/shared` | Types, tool helpers, hook engine |
## Resources
Repository: https://github.com/cline/cline
SDK Source: https://github.com/cline/cline/tree/main/sdk
Documentation: https://docs.cline.bot/sdk/overview
Discord: https://discord.gg/cline
@@ -1,107 +0,0 @@
# Agent Runtime
The `Agent` class (also exported as `AgentRuntime`) is the lightweight, stateless agent loop from `@cline/agents`. It handles the core iteration cycle: send messages to an LLM, execute tool calls, collect results, and repeat until the task is done.
## When to Use Agent
| Use Agent when... | Use ClineCore instead when... |
|---|---|
| You want a simple agent with custom tools | You need built-in tools (bash, editor, etc.) |
| You want minimal dependencies | You need session persistence |
| You need browser compatibility | You need config discovery from `.cline/` |
| You're building a stateless worker | You need multi-process session sharing |
| You want full control over the runtime | You want batteries-included setup |
## Quick Start
```typescript
import { Agent } from "@cline/sdk"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
const result = await agent.run("What is the capital of France?")
console.log(result.outputText)
```
## Core Concepts
The Agent operates in a loop:
1. Accept user input (string, message, or array of messages)
2. Build turn context (system prompt, messages, tools)
3. Call the LLM provider
4. If the model returns tool calls, execute them and loop back to step 3
5. If the model returns text without tool calls, the run completes
6. Emit events throughout for streaming
The agent is stateless in the sense that it does not persist anything to disk. Conversation history is held in memory and can be accessed via `snapshot()`.
## Key APIs
- `new Agent(config)` or `createAgent(config)` - Create an agent
- `agent.run(input)` - Start a run with user input
- `agent.continue(input?)` - Continue an existing conversation
- `agent.abort(reason?)` - Cancel an active run
- `agent.subscribe(listener)` - Listen to streaming events
- `agent.snapshot()` - Get current runtime state
- `agent.restore(messages)` - Replace message history
See `api.md` for full API details.
## Multi-Turn Conversations
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [],
})
const first = await agent.run("What is 2 + 2?")
console.log(first.outputText)
const second = await agent.continue("Now multiply that by 3")
console.log(second.outputText)
```
Use `agent.hasRun` to check if a run has already been executed, which determines whether to call `run()` or `continue()`.
## Event Streaming
Use `agent.subscribe()` to stream events in real time. Register the listener before calling `run()` to avoid missing early events.
There is no top-level `onEvent` field on the Agent config. For an async alternative, use `hooks.onEvent` (see `api.md` and `gotchas.md`).
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [],
})
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
const result = await agent.run("What is the capital of France?")
```
See `events/REFERENCE.md` for the full event type catalog.
## Next Steps
- `api.md` - Full Agent API reference
- `patterns.md` - Common patterns and best practices
- `gotchas.md` - Pitfalls and debugging
- `../tools/REFERENCE.md` - Creating custom tools
- `../events/REFERENCE.md` - Event system details
- `../providers/REFERENCE.md` - Provider configuration
@@ -1,231 +0,0 @@
# Agent API Reference
## Constructor
```typescript
import { Agent } from "@cline/sdk"
const agent = new Agent(config: AgentRuntimeConfig)
```
Also available via factory function:
```typescript
import { createAgent } from "@cline/sdk"
const agent = createAgent(config)
```
## AgentRuntimeConfig
Two config forms exist as a discriminated union:
### With Provider ID (recommended)
```typescript
interface AgentRuntimeConfigWithProvider {
providerId: string // e.g. "anthropic", "openai", "gemini"
modelId: string // e.g. "claude-sonnet-4-6", "gpt-5.5"
apiKey?: string // provider API key
baseUrl?: string // custom endpoint
headers?: Record<string, string>
systemPrompt?: string
tools?: AgentTool[]
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: AgentPlugin[]
}
```
### With Pre-built Model
```typescript
interface AgentRuntimeConfigWithModel {
model: AgentModel // pre-built model from gateway
systemPrompt?: string
tools?: AgentTool[]
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: AgentPlugin[]
}
```
Note: there is no top-level `onEvent` field on `AgentRuntimeConfig`. For event streaming, use `agent.subscribe()` or `hooks.onEvent` (see AgentRuntimeHooks below).
## Methods
### run(input)
Start the agent with user input. Returns when the agent loop completes.
```typescript
const result: AgentRunResult = await agent.run("Build a REST API")
```
Input can be a string, an `AgentMessage`, or an array of `AgentMessage[]`.
### continue(input?)
Continue an existing conversation with optional new input.
```typescript
const result = await agent.continue("Now add authentication")
```
### abort(reason?)
Cancel the currently active run.
```typescript
agent.abort("User cancelled")
```
### subscribe(listener)
Register a listener for streaming events.
```typescript
const unsubscribe = agent.subscribe((event: AgentRuntimeEvent) => {
// handle event
})
// Later: stop listening
unsubscribe()
```
### snapshot()
Get the current runtime state including message history.
```typescript
const state: AgentRuntimeStateSnapshot = agent.snapshot()
```
### restore(messages)
Replace the agent's message history.
```typescript
agent.restore(previousMessages)
```
### hasRun
Boolean property indicating whether `run()` has been called at least once.
```typescript
if (agent.hasRun) {
await agent.continue(input)
} else {
await agent.run(input)
}
```
## AgentRunResult
Returned by `run()` and `continue()`.
```typescript
interface AgentRunResult {
agentId: string
agentRole?: string
runId: string
status: "completed" | "aborted" | "failed"
iterations: number
outputText: string
messages: readonly AgentMessage[]
usage: AgentUsage
error?: Error
}
```
### Status Values
- `"completed"` - Agent finished normally
- `"aborted"` - Cancelled via `abort()`
- `"failed"` - Unrecoverable error
## AgentMessage
```typescript
interface AgentMessage {
id: string
role: "user" | "assistant" | "tool"
content: AgentMessagePart[]
createdAt: number
metadata?: Record<string, unknown>
modelInfo?: { id: string; provider: string; family?: string }
metrics?: {
inputTokens: number
outputTokens: number
cacheReadTokens?: number
cacheWriteTokens?: number
cost?: number
}
}
```
## AgentUsage
```typescript
interface AgentUsage {
inputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
totalInputTokens: number
totalOutputTokens: number
totalCost?: number
}
```
## AgentRuntimeHooks
```typescript
interface AgentRuntimeHooks {
beforeRun?(context): AgentStopControl | undefined
afterRun?(context): void
beforeModel?(context): AgentBeforeModelResult | undefined
afterModel?(context): AgentStopControl | undefined
beforeTool?(context): AgentBeforeToolResult | undefined
afterTool?(context): AgentAfterToolResult | undefined
onEvent?(event: AgentRuntimeEvent): void | Promise<void>
}
```
Hooks can intercept and modify behavior at each stage. Return a stop control from `beforeRun`, `afterModel`, or `beforeTool` to halt the agent loop.
`hooks.onEvent` receives the same `AgentRuntimeEvent` types as `agent.subscribe()`, but hook callbacks are awaited (can be async), while `subscribe()` listeners are called synchronously. Use `subscribe()` for UI streaming and `hooks.onEvent` for async side effects like logging to an external service.
## AgentRuntimeStateSnapshot
```typescript
interface AgentRuntimeStateSnapshot {
messages: readonly AgentMessage[]
usage: AgentUsage
iterations: number
status: string
}
```
## Factory: createAgentRuntime
Lower-level factory that returns the same `Agent` class:
```typescript
import { createAgentRuntime } from "@cline/sdk"
const runtime = createAgentRuntime(config)
```
## See Also
- `REFERENCE.md` - Overview and quick start
- `patterns.md` - Common patterns
- `../tools/REFERENCE.md` - Tool creation
- `../events/REFERENCE.md` - Event types
- `../providers/REFERENCE.md` - Provider setup
@@ -1,134 +0,0 @@
# Agent Gotchas
## Agent Loop Never Stops
If the agent keeps iterating without completing:
- Make sure at least one tool has `lifecycle: { completesRun: true }` if you want the agent to explicitly finish.
- Without any tools, the agent will complete after the model returns text without tool calls.
- If using tools, ensure the system prompt guides the model toward calling the completion tool when done.
- Check that `completesRun` tools return successfully (not throwing errors).
## Tool Errors Count as Mistakes
When a tool's `execute` function throws an exception, the SDK counts it as a "mistake." After too many mistakes, the agent stops with a `mistake_limit` finish reason.
Instead, return errors as structured data:
```typescript
// Bad: throwing
execute: async (input) => {
throw new Error("File not found")
}
// Good: returning error data
execute: async (input) => {
return { error: "File not found", path: input.path }
}
```
## run() vs continue()
- Call `run()` for the first interaction. It sets up the conversation.
- Call `continue()` for subsequent messages. It appends to the existing conversation.
- Calling `run()` a second time resets the conversation history.
- Use `agent.hasRun` to check which method to call.
## Browser Compatibility
`@cline/agents` (and by extension, the `Agent` class) is browser-safe with no Node.js dependencies. However, `@cline/core` and `ClineCore` require Node.js 22+. If you import from `@cline/sdk`, you get everything including the Node-only code. For browser usage, import directly from `@cline/agents`:
```typescript
import { Agent } from "@cline/agents"
```
## No Top-Level onEvent on Agent Config
`AgentRuntimeConfig` does not have a top-level `onEvent` field. Passing `onEvent` to `new Agent({ onEvent: ... })` has no effect. There are two ways to receive events:
```typescript
// Option 1: subscribe() - synchronous, best for UI streaming
const agent = new Agent({ ...config })
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
// Option 2: hooks.onEvent - awaited, best for async side effects
const agent = new Agent({
...config,
hooks: {
onEvent: async (event) => {
if (event.type === "assistant-text-delta") {
await logToService(event.text)
}
},
},
})
```
Both receive the same `AgentRuntimeEvent` types. Prefer `subscribe()` for streaming UI.
## Event Listener Timing
Register event listeners via `subscribe()` before calling `run()`:
```typescript
// Good: subscribe before run
agent.subscribe(handler)
const result = await agent.run(input)
// Bad: subscribing after run starts loses early events
const promise = agent.run(input)
agent.subscribe(handler) // may miss events
```
## Tool Input Schema Matters
The model uses the tool's `inputSchema` to decide what arguments to pass. A vague or missing schema leads to incorrect tool calls.
- Use `z.enum()` for fixed value sets, not free-form strings
- Describe every property with `.describe()` in Zod or `description` in JSON Schema
- Include constraints (rate limits, max values) in the tool description
## Memory and Long Conversations
The Agent holds all messages in memory. For long-running conversations, memory usage grows with each turn. Consider:
- Using `ClineCore` with compaction for long sessions
- Periodically creating a new agent with a summary of the conversation
- Monitoring `result.usage.totalInputTokens` to track context growth
## Abort Signal Handling in Tools
Long-running tools should respect the abort signal:
```typescript
execute: async (input, context) => {
for (const item of items) {
if (context.abortSignal?.aborted) {
return { partial: results, aborted: true }
}
results.push(await process(item))
}
return { results }
}
```
## Provider API Key
If you get authentication errors, check:
- `apiKey` is set in the config or via environment variables
- The key matches the `providerId` (e.g., Anthropic key for `providerId: "anthropic"`)
- For OpenAI-compatible providers, both `apiKey` and `baseUrl` are set
See `../providers/REFERENCE.md` for provider-specific setup.
## See Also
- `api.md` - Full API reference
- `patterns.md` - Common patterns
- `../tools/REFERENCE.md` - Tool creation
- `../clinecore/REFERENCE.md` - Use ClineCore for persistence
@@ -1,258 +0,0 @@
# Agent Patterns
## Interactive CLI Agent
A multi-turn conversational agent in the terminal with streaming output:
```typescript
import { Agent } from "@cline/sdk"
import * as readline from "node:readline"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant. Keep responses concise.",
tools: [],
})
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
function prompt(): void {
rl.question("\nYou: ", async (input) => {
const trimmed = input.trim()
if (!trimmed || trimmed === "exit") {
rl.close()
return
}
process.stdout.write("\nAssistant: ")
if (agent.hasRun) {
await agent.continue(trimmed)
} else {
await agent.run(trimmed)
}
process.stdout.write("\n")
prompt()
})
}
prompt()
```
## Conversational Agent (Slack Bot, Chat App)
Maintain per-thread agents with conversation memory:
```typescript
import { Agent } from "@cline/sdk"
const agents = new Map<string, Agent>()
async function handleMessage(threadId: string, message: string) {
let agent = agents.get(threadId)
if (!agent) {
agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a concise assistant.",
tools: [],
})
agents.set(threadId, agent)
}
const result = agent.hasRun
? await agent.continue(message)
: await agent.run(message)
return result.outputText
}
```
## Streaming UI
Build a real-time UI by handling events via `subscribe()`:
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [myTool],
})
agent.subscribe((event) => {
switch (event.type) {
case "assistant-text-delta":
ui.appendText(event.text)
break
case "assistant-message":
ui.endText()
break
case "turn-started":
ui.startTurn(event.iteration)
break
case "turn-finished":
if (event.toolCallCount > 0) ui.showToolCount(event.toolCallCount)
break
case "usage-updated":
ui.updateUsage(event.usage.inputTokens, event.usage.outputTokens)
break
}
})
const result = await agent.run("Hello!")
```
## Structured Output via Completion Tool
Use a tool with `completesRun: true` to extract structured data:
```typescript
import { Agent, createTool } from "@cline/sdk"
import { z } from "zod"
const submitReview = createTool({
name: "submit_review",
description: "Submit the final code review with structured feedback.",
inputSchema: z.object({
summary: z.string(),
issues: z.array(z.object({
file: z.string(),
line: z.number(),
severity: z.enum(["error", "warning", "info"]),
message: z.string(),
})),
approved: z.boolean(),
}),
lifecycle: { completesRun: true },
execute: async (input) => input,
})
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "Review the code diff and submit structured feedback.",
tools: [submitReview],
})
const result = await agent.run(diffContent)
const review = result.toolCalls.find(tc => tc.name === "submit_review")
console.log(review?.output)
```
## Agent with Abort/Timeout
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "Analyze this data.",
tools: [],
})
const timeout = setTimeout(() => agent.abort("Timeout"), 30_000)
try {
const result = await agent.run(data)
if (result.status === "aborted") {
console.log("Agent was aborted")
} else {
console.log(result.outputText)
}
} finally {
clearTimeout(timeout)
}
```
## Agent with Plugins
```typescript
import { Agent } from "@cline/sdk"
import type { AgentPlugin } from "@cline/sdk"
const loggingPlugin: AgentPlugin = {
name: "logging",
manifest: { capabilities: ["hooks"] },
setup() {},
hooks: {
beforeTool({ toolCall }) {
console.log(`Calling tool: ${toolCall.toolName}`)
},
afterRun({ result }) {
console.log(`Completed in ${result.iterations} iterations`)
},
},
}
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
systemPrompt: "You are a helpful assistant.",
tools: [myTool],
plugins: [loggingPlugin],
})
```
## Restoring State Across Sessions
Save and restore agent state manually:
```typescript
// Save state
const snapshot = agent.snapshot()
const serialized = JSON.stringify(snapshot.messages)
// Later: restore
const agent2 = new Agent({ ...config })
const messages = JSON.parse(serialized)
agent2.restore(messages)
const result = await agent2.continue("Continue where we left off")
```
For automatic persistence, use `ClineCore` instead.
## Pre-Built Model via Gateway
For advanced provider configuration:
```typescript
import { Agent } from "@cline/sdk"
import { createGateway } from "@cline/llms"
const gateway = createGateway({
providerConfigs: [
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
{ providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
],
})
const model = gateway.createAgentModel({
providerId: "anthropic",
modelId: "claude-opus-4-7",
})
const agent = new Agent({
model,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
```
## See Also
- `api.md` - Full API reference
- `gotchas.md` - Common pitfalls
- `../tools/REFERENCE.md` - Creating tools
- `../plugins/REFERENCE.md` - Plugin system
@@ -1,131 +0,0 @@
# ClineCore Runtime
`ClineCore` is the full-featured runtime from `@cline/core`. It wraps the `Agent` loop with session persistence, built-in tools (bash, editor, file reading, search, web fetch), config discovery, plugin loading, and optional hub-backed multi-process support.
## When to Use ClineCore
| Use ClineCore when... | Use Agent instead when... |
|---|---|
| You need built-in tools (bash, editor, etc.) | You only need custom tools |
| You want session persistence to disk | Stateless is fine |
| You need config discovery from `.cline/` dirs | You handle config yourself |
| You want scheduled/automated agents | You don't need scheduling |
| You need multi-client session sharing | Single-process is fine |
| You're building a full application | You want minimal dependencies |
## Quick Start
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Set up CI with GitHub Actions",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: "/path/to/project",
enableTools: true,
},
})
console.log(session.result?.text)
await cline.dispose()
```
## Core Concepts
### Sessions
Every `cline.start()` call creates a session with a unique ID. Sessions persist their messages and metadata to SQLite. You can list, read, resume, and delete sessions.
### Built-in Tools
ClineCore provides these tools automatically when `enableTools: true`:
| Tool | Description |
|------|-------------|
| `bash` | Execute shell commands |
| `editor` | Edit files |
| `read_files` | Read file contents |
| `apply_patch` | Apply unified diffs |
| `search` | Search file contents and structure |
| `fetch_web` | HTTP requests and web content |
### Config Discovery
ClineCore watches `.cline/` directories for:
- Rules (system prompt additions)
- Skills (domain knowledge)
- Workflows (multi-step procedures)
- Hooks (lifecycle logic)
- Plugins (tool + hook bundles)
- MCP servers (external tool providers)
### Backend Modes
| Mode | Description |
|------|-------------|
| `"auto"` (default) | Tries to connect to a local hub; falls back to in-process if unavailable |
| `"local"` | In-process execution, local SQLite storage, no hub |
| `"hub"` | Requires a compatible local WebSocket hub; fails if unavailable |
| `"remote"` | Connects to an explicit remote hub endpoint |
The default mode is `"auto"`. For simple scripts and CLI tools, `"local"` avoids hub discovery overhead. Hub mode enables multi-client session sharing (e.g., a dashboard watching a running session from another process).
## Key APIs
- `ClineCore.create(options)` - Create and initialize
- `cline.start(input)` - Start a new session
- `cline.send({ sessionId, prompt })` - Send follow-up message
- `cline.subscribe(listener)` - Listen to session events
- `cline.list()` - List sessions
- `cline.get(sessionId)` - Get session metadata
- `cline.readMessages(sessionId)` - Read persisted messages
- `cline.getAccumulatedUsage(sessionId)` - Token/cost totals
- `cline.abort(sessionId)` - Abort a session
- `cline.delete(sessionId)` - Delete a session
- `cline.dispose()` - Clean up resources
See `api.md` for full API details.
## Event Streaming
`cline.subscribe()` emits `CoreSessionEvent` types. These are different from the `AgentRuntimeEvent` types emitted by the standalone `Agent` class -- see `../events/REFERENCE.md` for the full comparison.
```typescript
cline.subscribe((event) => {
switch (event.type) {
case "chunk":
if (event.payload.type === "text") {
process.stdout.write(event.payload.text)
}
break
case "ended":
console.log(`Session ended: ${event.payload.finishReason}`)
break
}
})
```
ClineCore results use `AgentResult` with `.text` (not `.outputText` like the standalone Agent's `AgentRunResult`).
## Session Persistence
Sessions are stored at:
```
~/.cline/data/sessions/
sessions.db # SQLite database
[session-id].json # Message history
```
## Next Steps
- `api.md` - Full ClineCore API reference
- `patterns.md` - Common patterns and best practices
- `gotchas.md` - Pitfalls and debugging
- `../tools/REFERENCE.md` - Custom tool creation
- `../plugins/REFERENCE.md` - Plugin system
- `../scheduling/REFERENCE.md` - Scheduled agents
@@ -1,304 +0,0 @@
# ClineCore API Reference
## Creating ClineCore
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create(options: ClineCoreOptions)
```
### ClineCoreOptions
```typescript
interface ClineCoreOptions {
clientName: string // identifies your app
distinctId?: string // user/instance identifier
backendMode?: "auto" | "local" | "hub" | "remote"
hub?: HubOptions
remote?: RemoteOptions
capabilities?: RuntimeCapabilities
toolPolicies?: Record<string, ToolPolicy>
automation?: boolean | ClineCoreAutomationOptions
fetch?: typeof fetch
}
```
### RuntimeCapabilities
```typescript
interface RuntimeCapabilities {
requestToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResult>
// ... other capability callbacks
}
```
## Starting Sessions
### start(input)
```typescript
const session = await cline.start(input: ClineCoreStartInput)
```
Returns a `StartSessionResult`:
```typescript
interface StartSessionResult {
sessionId: string
manifest: SessionManifest
manifestPath: string
messagesPath: string
result?: AgentResult
}
```
### ClineCoreStartInput
```typescript
interface ClineCoreStartInput {
prompt: string
config: CoreSessionConfig
source?: string
interactive?: boolean
sessionMetadata?: Record<string, unknown>
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
capabilities?: RuntimeCapabilities
}
```
### CoreSessionConfig
```typescript
interface CoreSessionConfig {
cwd?: string // working directory
providerId: string // LLM provider
modelId: string // model identifier
apiKey?: string // provider API key
systemPrompt?: string // custom system prompt
tools?: readonly AgentTool[] // additional custom tools
enableTools?: boolean // enable built-in tools
hooks?: Partial<AgentRuntimeHooks> // runtime hooks
extensions?: AgentPlugin[] // plugins loaded inline
pluginPaths?: string[] // paths to plugin packages
extensionLoading?: "isolated" | "direct"
extensionContext?: { // context passed to plugin setup()
workspace?: { rootPath: string; cwd: string }
}
checkpointConfig?: CoreCheckpointConfig
compactionConfig?: CoreCompactionConfig
telemetry?: ITelemetryService
logger?: BasicLogger
enableSpawnAgent?: boolean // enable sub-agent spawning
enableAgentTeams?: boolean // enable team coordination
teamName?: string // team identifier
}
```
`extensions` passes plugin objects directly. `pluginPaths` points to directories with `package.json` containing a `cline.plugins` field. Set `extensionContext.workspace` so plugins receive `ctx.workspaceInfo` in their `setup()` call -- without it, `ctx.workspaceInfo` is undefined.
## Follow-Up Messages
### send({ sessionId, prompt })
Send a follow-up message to an existing session:
```typescript
const result = await cline.send({
sessionId: session.sessionId,
prompt: "Now add authentication",
})
```
Returns `AgentResult | undefined`.
## Event Subscription
### subscribe(listener, options?)
```typescript
const unsubscribe = cline.subscribe(
(event: CoreSessionEvent) => {
// handle events
},
{ sessionId: "optional-filter" }
)
```
### CoreSessionEvent
```typescript
type CoreSessionEvent =
| { type: "chunk"; payload: SessionChunkEvent }
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent } }
| { type: "ended"; payload: SessionEndedEvent }
| { type: "team_progress"; payload: SessionTeamProgressEvent }
| { type: "status"; payload: { sessionId: string, status: string } }
| { type: "hook"; payload: SessionToolEvent }
```
## Session Management
### list(limit?, options?)
```typescript
const sessions: SessionRecord[] = await cline.list(50)
```
### get(sessionId)
```typescript
const session: SessionRecord = await cline.get(sessionId)
```
### readMessages(sessionId)
```typescript
const messages: AgentMessage[] = await cline.readMessages(sessionId)
```
### getAccumulatedUsage(sessionId)
```typescript
const usage = await cline.getAccumulatedUsage(sessionId)
// usage.usage - root agent only
// usage.aggregateUsage - root + subagents/teammates
```
### update(sessionId, updates)
```typescript
await cline.update(sessionId, { title: "New title" })
```
### abort(sessionId, reason?)
```typescript
await cline.abort(sessionId, "User cancelled")
```
### stop(sessionId)
```typescript
await cline.stop(sessionId)
```
### delete(sessionId)
```typescript
await cline.delete(sessionId)
```
### restore(input)
Restore a session from a checkpoint:
```typescript
await cline.restore({ sessionId, checkpointId })
```
### dispose(reason?)
Clean up all resources. Always call this when done:
```typescript
await cline.dispose("Shutting down")
```
## AgentResult
Returned by session operations:
```typescript
interface AgentResult {
text: string
usage: LegacyAgentUsage
messages: MessageWithMetadata[]
toolCalls: ToolCallRecord[]
iterations: number
finishReason: "completed" | "max_iterations" | "aborted" | "mistake_limit" | "error"
model: { id: string; provider: string; info?: ModelInfo }
startedAt: Date
endedAt: Date
durationMs: number
}
```
## Tool Policies
Control tool access at the session level:
```typescript
const session = await cline.start({
prompt: "Review the code",
config: { ... },
toolPolicies: {
read_files: { autoApprove: true },
bash: { autoApprove: false },
editor: { enabled: false },
},
})
```
### ToolPolicy
```typescript
interface ToolPolicy {
enabled?: boolean // false = tool is hidden from the model
autoApprove?: boolean // false = requires approval callback
}
```
## Interactive Approval
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
capabilities: {
requestToolApproval: async (request) => {
console.log(`Tool: ${request.toolName}, Input: ${JSON.stringify(request.input)}`)
const approved = await askUser(`Allow ${request.toolName}?`)
return { approved }
},
},
})
```
## Automation API
When `automation` is enabled in `ClineCore.create()`:
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
automation: true,
})
// Access automation methods
cline.automation.start()
cline.automation.stop()
cline.automation.reconcile(specs)
cline.automation.ingestEvent(event)
cline.automation.listEvents()
cline.automation.listSpecs()
cline.automation.listRuns()
```
## Settings API
```typescript
// Read settings
const settings = await cline.settings.list()
// Toggle tools, plugins, MCP servers
await cline.settings.toggle({ type: "tool", name: "bash", enabled: true })
```
## See Also
- `REFERENCE.md` - Overview and quick start
- `patterns.md` - Common patterns
- `gotchas.md` - Pitfalls
- `../tools/REFERENCE.md` - Tool creation
- `../plugins/REFERENCE.md` - Plugin system
@@ -1,148 +0,0 @@
# ClineCore Gotchas
## Always Call dispose()
`ClineCore` holds resources (file watchers, database connections, hub connections). Failing to call `dispose()` can leave orphan processes and file locks.
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
try {
// ... use cline
} finally {
await cline.dispose()
}
```
## Node.js 22 Required
ClineCore and `@cline/core` require Node.js 22 or later. If you're on an older version, you'll get runtime errors. Check with `node --version`.
## Session Config vs Global Config
Tool policies can be set at two levels:
- Global: in `ClineCore.create({ toolPolicies })` -- applies to all sessions
- Per-session: in `cline.start({ toolPolicies })` -- overrides global for that session
Per-session policies take precedence.
## enableTools Must Be Explicit
Built-in tools (bash, editor, read_files, etc.) are not available unless you set `enableTools: true` in the session config:
```typescript
await cline.start({
prompt: "Read package.json",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
enableTools: true, // required for built-in tools
},
})
```
Without this, the agent only has access to custom tools you provide via `config.tools`.
## cwd Matters for Built-in Tools
Built-in tools like `bash`, `editor`, and `read_files` operate relative to `config.cwd`. If not set, they use the process working directory. Always set it explicitly for predictable behavior:
```typescript
config: {
cwd: "/absolute/path/to/project",
// ...
}
```
## Hub Startup Latency
With `backendMode: "auto"`, the first session may be slow if a hub daemon needs to be spawned. For immediate responsiveness:
- Use `backendMode: "local"` for in-process execution (fastest startup)
- Pre-warm the hub with `cline hub ensure` CLI command
- Accept the one-time startup cost and let subsequent sessions reuse the hub
## Session Storage Location
Sessions are stored at `~/.cline/data/sessions/`. This includes:
- `sessions.db` - SQLite database with session metadata
- `[session-id].json` - Individual message history files
If you're running in a container or ephemeral environment, these paths may not persist across restarts.
## requestToolApproval Blocks Execution
When a tool policy has `autoApprove: false` and you provide a `requestToolApproval` callback, the agent loop blocks until your callback resolves. If your callback never resolves (e.g., waiting for user input that never comes), the session hangs.
For automated pipelines, either:
- Set all tools to `autoApprove: true`
- Implement a timeout in your approval callback
## Plugin Discovery Paths
ClineCore discovers plugins from:
- Global: `~/.cline/plugins/`
- Workspace: `.cline/plugins/`
For SDK consumers, pass plugins via `extensions: [plugin]` or `pluginPaths: ["./path"]` in the session config.
If a plugin isn't loading, verify:
- The file is in one of the discovery directories, or passed via `extensions`/`pluginPaths`
- The file exports a default plugin object with a non-empty `manifest.capabilities` array
- Every `api.register*` call in `setup()` has a matching capability declared
- If `hooks` is present on the plugin, `"hooks"` is in `capabilities`
## extensionContext.workspace Is Required for Plugins
If your plugins use `ctx.workspaceInfo` (e.g., to resolve workspace paths), you must set `extensionContext.workspace` in the session config. Without it, `ctx.workspaceInfo` is undefined:
```typescript
await cline.start({
config: {
extensions: [myPlugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
})
```
The CLI sets this automatically, but SDK consumers must set it explicitly.
## send() Requires an Active Session
`cline.send()` only works on sessions that are still active. If a session has already completed, `send()` may return `undefined` or fail. Check session status with `cline.get(sessionId)` first.
## Result May Be Undefined
`session.result` can be `undefined` if the session was started but hasn't completed yet (e.g., in a non-blocking hub mode). Check for this:
```typescript
const session = await cline.start({ ... })
if (session.result) {
console.log(session.result.text)
} else {
console.log("Session started but not yet complete")
}
```
## Compaction and Long Sessions
For long-running sessions, message history grows and eventually exceeds the model's context window. ClineCore handles this via compaction, which summarizes older messages. Configure it via `compactionConfig`:
```typescript
config: {
compactionConfig: {
strategy: "summarize",
// ...
},
}
```
The default strategy works for most cases, but extremely long sessions may benefit from tuning.
## See Also
- `api.md` - Full API reference
- `patterns.md` - Common patterns
- `../agent/gotchas.md` - Agent-level gotchas
- `../tools/REFERENCE.md` - Tool troubleshooting
- `../providers/REFERENCE.md` - Provider troubleshooting
@@ -1,279 +0,0 @@
# ClineCore Patterns
## Basic Session with Built-in Tools
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Read package.json and summarize the dependencies",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
cwd: process.cwd(),
enableTools: true,
},
})
console.log(session.result?.text)
await cline.dispose()
```
## Streaming Session with UI Updates
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
cline.subscribe((event) => {
switch (event.type) {
case "chunk":
if (event.payload.type === "text") {
ui.appendText(event.payload.text)
}
break
case "ended":
ui.showComplete(event.payload.finishReason)
break
}
})
await cline.start({
prompt: "Refactor the auth module",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: "/path/to/project",
enableTools: true,
},
})
```
## Multi-Turn Session
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
const session = await cline.start({
prompt: "Create a new Express server",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: "/path/to/project",
enableTools: true,
},
})
// Follow-up
const result = await cline.send({
sessionId: session.sessionId,
prompt: "Now add a health check endpoint",
})
console.log(result?.text)
await cline.dispose()
```
## Tiered Permission Model
Auto-approve reads, require approval for writes:
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
toolPolicies: {
read_files: { autoApprove: true },
search: { autoApprove: true },
fetch_web: { autoApprove: true },
bash: { autoApprove: false },
editor: { autoApprove: false },
apply_patch: { autoApprove: false },
},
capabilities: {
requestToolApproval: async (request) => {
const approved = await promptUser(
`Allow ${request.toolName}?\n${JSON.stringify(request.input, null, 2)}`
)
return { approved }
},
},
})
```
## Custom Tools Alongside Built-ins
```typescript
import { ClineCore, createTool } from "@cline/sdk"
import { z } from "zod"
const deployTool = createTool({
name: "deploy",
description: "Deploy the application to the specified environment.",
inputSchema: z.object({
environment: z.enum(["staging", "production"]),
}),
execute: async (input) => {
const result = await runDeployment(input.environment)
return { url: result.url, status: "deployed" }
},
})
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Deploy the app to staging",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
enableTools: true,
tools: [deployTool],
},
})
```
## Session with Plugins
Load plugins inline with `extensions` and provide workspace context so plugins can access `ctx.workspaceInfo`:
```typescript
import { ClineCore } from "@cline/sdk"
import myPlugin from "./my-plugin"
const cline = await ClineCore.create({
clientName: "my-app",
backendMode: "local",
})
await cline.start({
prompt: "Do the thing my plugin enables",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: process.cwd(),
enableTools: true,
extensions: [myPlugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
})
await cline.dispose()
```
For directory-based plugin packages, use `pluginPaths` instead:
```typescript
config: {
pluginPaths: ["./my-cline-plugin"],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
}
```
See `../plugins/REFERENCE.md` for the full plugin authoring guide.
## Session Listing and Replay
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
// List recent sessions
const sessions = await cline.list(10)
for (const session of sessions) {
console.log(`${session.id}: ${session.title}`)
}
// Read messages from a past session
const messages = await cline.readMessages(sessions[0].id)
for (const msg of messages) {
console.log(`[${msg.role}] ${msg.content}`)
}
// Check usage
const usage = await cline.getAccumulatedUsage(sessions[0].id)
console.log(`Total tokens: ${usage.aggregateUsage.totalInputTokens + usage.aggregateUsage.totalOutputTokens}`)
```
## Graceful Shutdown
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
process.on("SIGTERM", async () => {
await cline.dispose("SIGTERM received")
process.exit(0)
})
// Run sessions...
```
## Stateless Worker Pattern
For request/response workloads (API endpoints, queue consumers):
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({
clientName: "worker",
backendMode: "local",
})
async function handleRequest(prompt: string, workspace: string) {
const session = await cline.start({
prompt,
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
cwd: workspace,
enableTools: true,
},
})
return {
text: session.result?.text,
usage: session.result?.usage,
sessionId: session.sessionId,
}
}
```
## Hub-Backed Multi-Client
Multiple clients can attach to the same session:
```typescript
// Process 1: start session
const cline = await ClineCore.create({
clientName: "backend",
backendMode: "hub",
})
const session = await cline.start({
prompt: "Long running refactor task",
config: { ... },
})
// Process 2: attach and stream events
const viewer = await ClineCore.create({
clientName: "dashboard",
backendMode: "hub",
})
viewer.subscribe((event) => {
dashboard.render(event)
}, { sessionId: session.sessionId })
```
## See Also
- `api.md` - Full API reference
- `gotchas.md` - Common pitfalls
- `../tools/REFERENCE.md` - Tool creation
- `../plugins/REFERENCE.md` - Plugin system
- `../scheduling/REFERENCE.md` - Scheduled agents
@@ -1,269 +0,0 @@
# Events
The Cline SDK has three event layers. Which one you use depends on whether you're working with the standalone `Agent` class or `ClineCore`.
## Which Events Do I Get?
| If you use... | You subscribe with... | You receive... | Text streaming event |
|---|---|---|---|
| Standalone `Agent` | `agent.subscribe()` | `AgentRuntimeEvent` | `assistant-text-delta` |
| `ClineCore` | `cline.subscribe()` | `CoreSessionEvent` | `chunk` (with `payload.type === "text"`) |
These are different event types with different shapes. Do not mix them up.
## Layer 1: AgentRuntimeEvent (Standalone Agent)
Emitted by the `Agent` class via `agent.subscribe()`. This is what you get when using `new Agent(...)` directly. Every event includes a `snapshot` field with the current `AgentRuntimeStateSnapshot`.
### Run Lifecycle
```typescript
{ type: "run-started", snapshot }
{ type: "run-finished", snapshot, result: AgentRunResult }
{ type: "run-failed", snapshot, error: Error }
```
### Turns
```typescript
{ type: "turn-started", snapshot, iteration: number }
{ type: "turn-finished", snapshot, iteration: number, toolCallCount: number }
```
### Text Streaming
```typescript
// Streaming text delta (arrives as chunks during generation)
{ type: "assistant-text-delta", snapshot, iteration: number, text: string, accumulatedText: string }
// Streaming reasoning delta (when model uses extended thinking)
{ type: "assistant-reasoning-delta", snapshot, iteration: number, text: string }
// Complete assistant message after model finishes
{ type: "assistant-message", snapshot, iteration: number, message: AgentMessage, finishReason: string }
```
### Messages
```typescript
// Fired when any message (user or assistant) is added to conversation history
{ type: "message-added", snapshot, message: AgentMessage }
```
### Tool Events
```typescript
{ type: "tool-started", snapshot, toolCall: { toolName: string, toolCallId: string, input: unknown } }
{ type: "tool-updated", snapshot, toolCall: { toolName: string, toolCallId: string }, update: string }
{ type: "tool-finished", snapshot, toolCall: { toolName: string, toolCallId: string }, message: AgentMessage }
```
### Usage
```typescript
{
type: "usage-updated",
snapshot,
usage: {
inputTokens: number,
outputTokens: number,
cacheReadTokens?: number,
cacheWriteTokens?: number,
totalCost?: number,
},
}
```
### Notices
```typescript
{ type: "status-notice", snapshot, message: string, metadata?: Record<string, unknown> }
```
### Subscribing
Use `agent.subscribe()`. Register the listener before calling `run()` to avoid missing early events.
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
agent.subscribe((event) => {
switch (event.type) {
case "assistant-text-delta":
process.stdout.write(event.text)
break
case "tool-started":
console.log(`\nUsing tool: ${event.toolCall.toolName}`)
break
case "usage-updated":
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
break
case "run-finished":
console.log(`\nDone: ${event.result.status}`)
break
}
})
const result = await agent.run("Hello!")
```
You can also receive events through hooks (these are awaited, so they can be async):
```typescript
const agent = new Agent({
...config,
hooks: {
onEvent: async (event) => {
// Same AgentRuntimeEvent types as subscribe()
},
},
})
```
## Layer 2: AgentEvent (ClineCore Internal)
When using `ClineCore`, a `RuntimeEventAdapter` translates Layer 1 events into a legacy format called `AgentEvent`. You do not interact with this layer directly -- it is projected into `CoreSessionEvent` for subscribers. The key mappings:
| AgentRuntimeEvent (Layer 1) | AgentEvent (Layer 2) |
|---|---|
| `turn-started` | `iteration_start` |
| `turn-finished` | `iteration_end` |
| `assistant-text-delta` | `content_start` (text) |
| `assistant-message` | `content_end` (text) |
| `tool-started` | `content_start` (tool) |
| `tool-updated` | `content_update` (tool) |
| `tool-finished` | `content_end` (tool) |
| `usage-updated` | `usage` (with computed deltas) |
| `run-finished` | `done` |
| `run-failed` | `error` |
| `run-started`, `message-added` | (suppressed, not emitted) |
This layer exists for backwards compatibility. If you see event types like `content_update` or `iteration_start` in other documentation, they refer to this layer, not to what `agent.subscribe()` emits.
## Layer 3: CoreSessionEvent (ClineCore Subscriber)
Emitted by `ClineCore` via `cline.subscribe()`. These are higher-level session events.
```typescript
type CoreSessionEvent =
| { type: "chunk"; payload: SessionChunkEvent }
| { type: "agent_event"; payload: { sessionId: string, event: AgentEvent } }
| { type: "ended"; payload: SessionEndedEvent }
| { type: "team_progress"; payload: SessionTeamProgressEvent }
| { type: "status"; payload: { sessionId: string, status: string } }
| { type: "hook"; payload: SessionToolEvent }
```
### SessionChunkEvent
```typescript
interface SessionChunkEvent {
type: "text" | "reasoning"
text: string
sessionId: string
}
```
### SessionEndedEvent
```typescript
interface SessionEndedEvent {
sessionId: string
finishReason: "completed" | "max_iterations" | "aborted" | "mistake_limit" | "error"
result?: AgentResult
}
```
### Subscribing
```typescript
cline.subscribe((event) => {
switch (event.type) {
case "chunk":
if (event.payload.type === "text") {
process.stdout.write(event.payload.text)
}
break
case "ended":
console.log(`Finished: ${event.payload.finishReason}`)
break
}
})
```
Filter by session:
```typescript
cline.subscribe(handler, { sessionId: "specific-session-id" })
```
## Hub Events (Layer 3b)
When ClineCore runs in hub mode (via `backendMode: "hub"` or `"auto"` when a hub is available), events are projected over WebSocket using `HubEventName` types like `assistant.delta`, `iteration.started`, `tool.started`, etc. You do not interact with these directly -- `cline.subscribe()` still gives you `CoreSessionEvent` regardless of backend mode.
## Result Type Differences
The standalone Agent and ClineCore return different result types:
| API | Result type | Text property |
|---|---|---|
| `agent.run()` | `AgentRunResult` | `result.outputText` |
| `cline.start()` / `cline.send()` | `AgentResult` | `result.text` |
## Common Patterns
### Streaming Text (Standalone Agent)
```typescript
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
```
### Streaming Text (ClineCore)
```typescript
cline.subscribe((event) => {
if (event.type === "chunk" && event.payload.type === "text") {
process.stdout.write(event.payload.text)
}
})
```
### Usage Tracking (Standalone Agent)
```typescript
agent.subscribe((event) => {
if (event.type === "usage-updated" && event.usage.totalCost) {
console.log(`Running cost: $${event.usage.totalCost.toFixed(4)}`)
}
})
```
### Tool Call Logging (Standalone Agent)
```typescript
agent.subscribe((event) => {
if (event.type === "tool-started") {
console.log(`Tool started: ${event.toolCall.toolName}`)
}
if (event.type === "tool-finished") {
console.log(`Tool finished: ${event.toolCall.toolName}`)
}
})
```
## See Also
- `../agent/REFERENCE.md` - Agent runtime overview
- `../clinecore/REFERENCE.md` - ClineCore session management
- `../plugins/REFERENCE.md` - Plugin hooks for lifecycle events
- `../production/REFERENCE.md` - Observability in production
@@ -1,157 +0,0 @@
# Multi-Agent Coordination
The Cline SDK supports two models for multi-agent work: sub-agents (parent-child) and teams (peer-to-peer).
## Sub-Agents vs Teams
| Feature | Sub-Agents | Teams |
|---------|-----------|-------|
| Enable with | `enableSpawnAgent: true` | `enableAgentTeams: true` |
| Persistence | Session-scoped only | Across sessions |
| Coordination | Parent-child hierarchy | Peer-to-peer |
| Shared state | None | Task board, mailbox, mission log |
| Best for | One-off delegation | Complex multi-session projects |
## Sub-Agents
Sub-agents are spawned by a parent agent during a run. They execute independently and report results back.
### Enabling Sub-Agents
```typescript
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Refactor the auth module and update tests",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
enableSpawnAgent: true,
enableTools: true,
},
})
```
When `enableSpawnAgent` is true, the agent gets access to sub-agent tools:
| Tool | Description |
|------|-------------|
| `start_subagent` | Spawn a background agent with a task |
| `message_subagent` | Send a message to a running sub-agent |
| `handoff_to_agent` | Delegate the current task entirely |
| `submit_and_exit` | Signal completion |
### How Sub-Agents Work
1. The parent agent decides a subtask can be delegated
2. It calls `start_subagent` with a role, task description, and optionally a preset
3. The sub-agent runs independently in the background
4. The parent can check status or send follow-up messages
5. Sub-agent results are available to the parent when complete
## Teams
Teams provide persistent, cross-session coordination between agents.
### Enabling Teams
```typescript
await cline.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
enableAgentTeams: true,
teamName: "auth-sprint",
enableTools: true,
},
})
```
### Team Tools
When `enableAgentTeams` is true, the coordinator agent gets:
| Tool | Description |
|------|-------------|
| `team_spawn_teammate` | Create a new agent with a role and task |
| `team_delegate_task` | Assign a task to an existing teammate |
| `team_check_status` | Check on a delegated task's progress |
| `team_get_result` | Get the completed result from a teammate |
### Team Persistence
Teams store shared state in:
```
~/.cline/data/teams/[team-name]/
task-board.json # task assignments and status
mailbox.json # inter-agent messages
mission-log.json # coordination log
```
This state persists across sessions, so team members can pick up where they left off.
### CLI Team Access
```bash
cline --team-name auth-sprint "Continue the auth refactor"
```
## Choosing Between Sub-Agents and Teams
Use sub-agents when:
- You need one-off parallel execution within a single session
- Tasks are independent and don't need to communicate with each other
- Results only matter to the parent agent
Use teams when:
- Work spans multiple sessions over time
- Agents need to coordinate and share progress
- Tasks have dependencies between them
- You want a persistent record of multi-agent collaboration
## Patterns
### Parallel Research with Sub-Agents
A parent agent spawns multiple sub-agents to research different topics simultaneously:
```typescript
await cline.start({
prompt: `Research these three topics in parallel:
1. Current best practices for JWT auth
2. OAuth 2.0 provider comparison
3. Session management patterns
Spawn a sub-agent for each topic, then synthesize the results.`,
config: {
enableSpawnAgent: true,
enableTools: true,
// ...
},
})
```
### Team Sprint
A coordinator manages a multi-session project:
```typescript
await cline.start({
prompt: `You are the coordinator for the auth-sprint team.
Review the task board and delegate the next highest-priority task
to a teammate. Check status on any in-progress tasks.`,
config: {
enableAgentTeams: true,
teamName: "auth-sprint",
enableTools: true,
// ...
},
})
```
## See Also
- `../clinecore/REFERENCE.md` - ClineCore runtime
- `../clinecore/api.md` - Session config for teams
- `../tools/REFERENCE.md` - Tool system
- `../plugins/REFERENCE.md` - Plugin system
@@ -1,649 +0,0 @@
# Plugins
A Cline plugin is a TypeScript module that extends any agent built on the Cline SDK. The same plugin runs in the Cline CLI, VS Code and JetBrains extensions, and any custom app built on `@cline/core`.
A plugin can:
- Register tools the model can call.
- Hook into the agent loop before/after runs, model calls, and tool calls.
- Rewrite provider messages before they hit the model (custom compaction, redaction, context shaping).
- Register slash commands, prompt rules, providers, and automation event types.
A plugin ships in one of two shapes:
1. Single-file plugin -- one `.ts` file that exports a default plugin object. Drop it in a discovery folder and it loads.
2. Plugin package -- a directory with `package.json`, npm dependencies, and optionally bundled assets. Installable via `cline plugin install`.
Both shapes use the same plugin API.
## The Mental Model
When the host starts a session, it builds a registry of plugins and runs four phases:
1. resolve -- collect the plugin objects.
2. validate -- check each plugin's `manifest`. Capabilities must be non-empty; declared hook stages must have matching handlers; if `hooks` is present, `"hooks"` must be in `capabilities`.
3. setup -- call each plugin's `setup(api, ctx)` once. This is where you `registerTool`, `registerCommand`, etc.
4. activate -- registry is frozen, the agent loop starts, and your hooks/tools are live.
Two invariants the registry enforces:
- Every contribution requires a matching capability. Calling `api.registerRule(...)` without `"rules"` in `manifest.capabilities` throws.
- Capabilities and handlers must agree. Declaring `"hooks"` without a `hooks` object, or vice versa, fails validation.
After validation, registration is one-shot -- no dynamic register/unregister during the session.
## The Smallest Working Plugin
```typescript
import type { AgentPlugin } from "@cline/core"
import { createTool } from "@cline/core"
const plugin: AgentPlugin = {
name: "hello-plugin",
manifest: {
capabilities: ["tools"],
},
setup(api, ctx) {
api.registerTool(
createTool({
name: "say_hello",
description: "Greet a person by name.",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
async execute({ name }: { name: string }) {
return { greeting: `Hello, ${name}!` }
},
}),
)
},
}
export default plugin
```
The agent will see `say_hello` as a callable tool.
## The Manifest
```typescript
manifest: {
capabilities: ["tools", "hooks"], // required, non-empty array
paths?: string[], // optional, multi-entry packages
providerIds?: string[], // optional, provider plugins
modelIds?: string[], // optional, model plugins
}
```
### The Complete Capability List
| Capability | What It Unlocks in `api` |
|-----------|--------------------------|
| `"tools"` | `api.registerTool()` |
| `"commands"` | `api.registerCommand()` (slash commands in chat surfaces) |
| `"rules"` | `api.registerRule()` (string injected into the system prompt) |
| `"messageBuilders"` | `api.registerMessageBuilder()` (rewrites provider-bound messages) |
| `"providers"` | `api.registerProvider()` (custom model provider) |
| `"automationEvents"` | `api.registerAutomationEventType()` and `ctx.automation?.ingestEvent()` |
| `"hooks"` | The runtime `hooks` object on the plugin (lifecycle callbacks) |
You declare any combination -- most real plugins need 1-3 capabilities.
## setup(api, ctx) -- The Registration Phase
`setup()` runs once per session before the agent loop starts. Everything you register here is frozen for the lifetime of the session.
### The api Object
Each `register*` method requires the matching capability in your manifest:
```typescript
api.registerTool(tool) // requires "tools"
api.registerCommand({ name, description, handler }) // requires "commands"
api.registerRule({ id, content, source }) // requires "rules"
api.registerMessageBuilder({ name, build }) // requires "messageBuilders"
api.registerProvider({ name, description }) // requires "providers"
api.registerAutomationEventType({ eventType, source }) // requires "automationEvents"
```
### The ctx Object -- Host-Provided Session Context
The second argument carries everything the host knows about the current session. All fields are optional, so feature-detect before using them -- the same plugin must work in hosts that supply less context (unit tests, sandboxed plugin processes).
```typescript
ctx.session?.sessionId // string, stable core session id
ctx.client?.name // host: "cline-cli", "cline-vscode", etc.
ctx.user // authenticated user/org info, when available
ctx.workspaceInfo // { rootPath, hint, latestGitBranchName,
// latestGitCommitHash, associatedRemoteUrls }
ctx.automation?.ingestEvent // emit normalized automation events
ctx.logger?.log // structured logger scoped to this plugin
ctx.telemetry // ITelemetryService, only present in-process
```
Two rules about `ctx.workspaceInfo`:
1. Always prefer `ctx.workspaceInfo?.rootPath` over `process.cwd()`. The CLI may have been launched with `--cwd` without calling `chdir`, and VS Code workspaces don't share a single CWD. `workspaceInfo` is sourced from the session config and is always correct.
2. Don't use `import.meta.url` tricks to find "the workspace". That gives you the plugin's own location, not the user's project.
### Persisting State Across Hooks
`setup()` runs first; hooks fire later. The simplest way to share state is module-level variables:
```typescript
let sessionWorkspaceRoot: string | undefined
let sessionBranch: string | undefined
const plugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
setup(api, ctx) {
sessionWorkspaceRoot = ctx.workspaceInfo?.rootPath
sessionBranch = ctx.workspaceInfo?.latestGitBranchName
},
hooks: {
beforeTool({ toolCall, input }) {
if (sessionBranch === "main" && toolCall.toolName === "run_commands") {
// inspect input, optionally block
}
return undefined
},
},
}
```
A single Node process may host multiple sessions concurrently. If your plugin will run in a multi-session host, key your state by `ctx.session?.sessionId`:
```typescript
const stateBySession = new Map<string, MyState>()
setup(api, ctx) {
const id = ctx.session?.sessionId
if (id) stateBySession.set(id, /* ... */)
}
```
## Runtime Hooks
Runtime hooks are typed in-process callbacks on the same hook layer the runtime uses internally. They run inside the agent loop with full type information -- no IPC, no JSON marshaling.
Declare `"hooks"` in `manifest.capabilities`, then add a `hooks` property:
```typescript
const plugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
hooks: {
beforeRun(ctx) { /* ... */ },
beforeTool({ toolCall, input }) { /* ... */ },
afterTool({ toolCall, result }) { /* ... */ },
afterRun({ result }) { /* ... */ },
onEvent(event) { /* ... */ },
},
}
```
### The Seven Hooks
| Hook | Fires | Can Stop the Loop? | Common Uses |
|------|-------|--------------------|-------------|
| `beforeRun` | Before the runtime loop starts | Yes | Greet, log, attach session metadata |
| `afterRun` | After the runtime loop finishes (success, abort, or fail) | No | Notifications, metrics, persistent logs |
| `beforeModel` | Before each model request | Yes (mutate req) | Inject context, last-mile prompt edits |
| `afterModel` | After each model response, before tool execution | Yes | Block based on model output |
| `beforeTool` | Before each tool execution | Yes (`{ stop }`) | Audit, redact, block dangerous tools |
| `afterTool` | After each tool execution | Can replace result | Post-process, redact secrets in tool output |
| `onEvent` | On every `AgentRuntimeEvent` emitted by the runtime | No | Streaming UIs, telemetry pipes |
### Stopping the Loop from a Hook
Several hooks return an optional control object. The most common pattern is `beforeTool` blocking a destructive tool call:
```typescript
beforeTool({ toolCall, input }) {
if (toolCall.toolName === "run_commands") {
const { commands } = input as { commands?: string[] }
if (sessionBranch === "main" && commands?.some(c => c.startsWith("git push"))) {
return { stop: true, reason: "Blocked git push on protected branch" }
}
}
return undefined // explicit "continue"
}
```
Returning `undefined` (or omitting `return`) lets execution continue normally.
### afterRun Semantics
`afterRun` fires for every terminal status -- `completed`, `aborted`, `failed`. If you only want to act on success:
```typescript
afterRun({ result }) {
if (result.status !== "completed") return
// notify, log success metrics, etc.
}
```
### Plugin Hooks vs File Hooks
The runtime supports two hook systems:
- File hooks -- external scripts in `.cline/hooks/` invoked with serialized JSON. Right for user/workspace-specific scripts that don't ship with code.
- Plugin runtime hooks -- typed in-process callbacks. Right when the behavior belongs to a reusable extension and needs typed access to the runtime.
Core adapts file hooks onto the runtime hook layer, so you don't need both. If you're shipping a plugin, write it as runtime hooks.
## Message Builders
Message builders rewrite the provider-bound message list before the model call. They run after runtime messages are converted into SDK message blocks but before core's built-in safety builder.
Use them for:
- Custom compaction policies (replace middle history with a summary).
- Redacting PII or secrets before they reach the provider.
- Reshaping context for a specific model's strengths.
```typescript
api.registerMessageBuilder({
name: "summarize-middle-history",
build(messages) {
if (estimateTokens(messages) < THRESHOLD) return messages
return [...prefix, summary, ...recent]
},
})
```
Multiple builders run in registration order; the output of one is the input of the next.
When to use `beforeModel` instead: reach for the `beforeModel` hook only if you need the runtime snapshot or want to mutate the request object itself. Pure message rewrites belong in a builder.
## Automation Events
Plugins can declare normalized event types and emit them into Cline automation. Hosts that don't have automation enabled simply ignore both -- feature-detect `ctx.automation`.
```typescript
manifest: { capabilities: ["automationEvents"] },
setup(api, ctx) {
api.registerAutomationEventType({
eventType: "github.pull_request.opened",
source: "github",
description: "A new GitHub PR was opened",
attributesSchema: { /* JSON Schema for envelope.attributes */ },
})
if (!ctx.automation) return // host has no automation
ctx.automation.ingestEvent({
eventId: "pr-1234",
eventType: "github.pull_request.opened",
source: "github",
subject: "owner/repo#1234",
occurredAt: new Date().toISOString(),
attributes: { /* ... */ },
})
}
```
## Loading a Plugin
There are three ways a plugin gets into a session:
### Auto-Discovery (CLI)
The CLI scans these directories on startup:
- `<workspace>/.cline/plugins/` -- project-scoped plugins.
- `~/.cline/plugins/` -- user-scoped plugins.
Drop a `.ts` or `.js` file in, run `cline`, done:
```bash
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "do the thing my plugin enables"
```
### Explicit extensions in SDK Config
When you build your own host with `ClineCore`, pass the plugin object directly:
```typescript
import plugin from "./my-plugin"
import { ClineCore } from "@cline/core"
const host = await ClineCore.create({ backendMode: "local" })
await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant.",
extensions: [plugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
prompt: "...",
interactive: false,
})
```
### pluginPaths for Directory-Based Plugins
When the plugin is a directory with `package.json`, point `pluginPaths` at the directory:
```typescript
config: {
pluginPaths: ["./path/to/my-plugin-package"],
}
```
Or install with the CLI:
```bash
cline plugin install ./path/to/my-plugin-package
cline plugin install @scope/my-cline-plugin # from npm
cline plugin install --git github.com/owner/repo # from git
```
## Single-File Plugin Template
Save as `my-plugin.ts`, drop in `.cline/plugins/`:
```typescript
import { type AgentPlugin, ClineCore, createTool } from "@cline/core"
let sessionRoot: string | undefined
const plugin: AgentPlugin = {
name: "my-plugin",
manifest: {
capabilities: ["tools", "hooks"],
},
setup(api, ctx) {
sessionRoot = ctx.workspaceInfo?.rootPath
api.registerTool(
createTool({
name: "do_thing",
description: "Do the thing this plugin exists for.",
inputSchema: {
type: "object",
properties: { target: { type: "string" } },
required: ["target"],
},
async execute(input) {
const { target } = input as { target: string }
return { ok: true, target, root: sessionRoot }
},
}),
)
},
hooks: {
beforeRun() {
console.log("[my-plugin] run started")
},
afterRun({ result }) {
if (result.status !== "completed") return
console.log(`[my-plugin] done in ${result.iterations} iteration(s)`)
},
},
}
async function runDemo(): Promise<void> {
const host = await ClineCore.create({ backendMode: "local" })
try {
const result = await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant. Use tools when needed.",
extensions: [plugin],
extensionContext: {
workspace: { rootPath: process.cwd(), cwd: process.cwd() },
},
},
prompt: "Use do_thing on the target 'world'.",
interactive: false,
})
console.log(result.result?.text ?? "")
} finally {
await host.dispose()
}
}
if (import.meta.main) {
await runDemo()
}
export { plugin, runDemo }
export default plugin
```
Copy it, rename the tool, swap in your logic. The `runDemo()` function lets you test with `ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts`.
## Plugin Package
Use a plugin package when you need npm dependencies, multiple entry points, bundled assets, or npm/git distribution.
### Layout
```
my-cline-plugin/
+-- package.json
+-- tsconfig.json (optional, for local typechecking)
+-- index.ts (the plugin entry point)
+-- README.md
+-- assets/ (optional, bundled content)
+-- templates/
+-- schemas/
```
### package.json -- The Discovery Contract
```json
{
"name": "my-cline-plugin",
"version": "0.1.0",
"private": true,
"description": "What this plugin does, in one sentence.",
"type": "module",
"exports": {
".": "./index.ts"
},
"cline": {
"plugins": [
{
"paths": ["./index.ts"],
"capabilities": ["tools", "hooks"]
}
]
},
"peerDependencies": {
"@cline/core": "*"
},
"peerDependenciesMeta": {
"@cline/core": { "optional": true }
},
"dependencies": {
"zod": "^4.1.5"
}
}
```
Key fields:
- `type: "module"` -- required. Cline plugins are ES modules.
- `cline.plugins` -- the discovery contract. Array of entries, each with `paths` (entry files) and `capabilities` (pre-declared, validated before importing).
- `peerDependencies` for `@cline/core` -- the host already provides it. Marking it optional lets you typecheck in isolation.
### Bundling Assets
Resolve asset paths with `import.meta.url`, not `process.cwd()`:
```typescript
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { readFileSync, existsSync } from "node:fs"
const MODULE_DIR = dirname(fileURLToPath(import.meta.url))
const TEMPLATES_DIR = join(MODULE_DIR, "assets", "templates")
function loadTemplate(name: string): string | undefined {
const path = join(TEMPLATES_DIR, `${name}.md`)
return existsSync(path) ? readFileSync(path, "utf8") : undefined
}
```
This is the only place `import.meta.url` is appropriate in a plugin -- locating files inside the plugin package. For workspace paths, always use `ctx.workspaceInfo?.rootPath`.
### The Override Pattern (Bundled / Global / Project)
A package can ship default assets and let users override them. The convention is a three-tier lookup, last write wins by `name`:
1. bundled -- files inside the plugin package (defaults shipped with the plugin).
2. global -- files under `~/.cline/data/settings/<kind>/` (user overrides).
3. project -- files under `<workspace>/.cline/<kind>/` (project overrides).
### Multiple Plugin Entries
If your package exposes more than one plugin, list each in `cline.plugins`:
```json
"cline": {
"plugins": [
{ "paths": ["./tools-plugin.ts"], "capabilities": ["tools"] },
{ "paths": ["./hooks-plugin.ts"], "capabilities": ["hooks"] }
]
}
```
Each entry file should `export default` its own plugin object.
## Testing Your Plugin
### Unit Tests
The plugin object is plain data. Drive `setup()` against a minimal context and exercise tools directly:
```typescript
import plugin from "../my-plugin"
const tools: unknown[] = []
const api = {
registerTool: (t: unknown) => tools.push(t),
registerCommand: () => {},
registerRule: () => {},
registerMessageBuilder: () => {},
registerProvider: () => {},
registerAutomationEventType: () => {},
}
await plugin.setup?.(api as never, {
workspaceInfo: { rootPath: "/tmp/fake-workspace" },
})
// Now `tools` contains the registered tools -- call tool.execute(input, ctx)
```
### End-to-End with runDemo()
Add a `runDemo()` in your plugin file (see the single-file template above) that boots a real `ClineCore` session:
```bash
ANTHROPIC_API_KEY=sk-... bun run my-plugin.ts
```
### CLI Smoke Test
```bash
mkdir -p .cline/plugins
cp my-plugin.ts .cline/plugins/
cline -i "trigger something that exercises the plugin"
```
For packages:
```bash
cline plugin install ./my-cline-plugin
cline -i "..."
```
If the plugin fails validation or setup, the CLI prints a clear error and continues without it.
## Common Gotchas
- "capabilities must be a non-empty array" -- you forgot `manifest.capabilities`, or it's `[]`.
- "registerRule requires the 'rules' capability" -- capability/handler drift. Add `"rules"` to capabilities, or stop calling `registerRule`.
- Tool not visible to the model -- check `enableTools: true` on the session config, and that you're declaring `"tools"` in capabilities.
- `ctx.workspaceInfo` is undefined in SDK tests -- the host didn't pass `extensionContext.workspace`. In SDK code, set it explicitly (see the ClineCore loading example above).
- State leaking across sessions -- module-level variables are shared across sessions in the same process. Key by `ctx.session?.sessionId` if your host runs multiple sessions concurrently.
- `afterRun` firing on aborts -- guard with `if (result.status !== "completed") return`.
- Heavy work in `setup()` -- `setup()` blocks session start. Defer expensive work into the first tool call or `beforeRun`.
- Importing host internals -- only import from `@cline/core`. Reaching into host-specific packages (e.g. CLI internals) will break in non-CLI hosts.
- Sandboxed plugins and `telemetry` -- telemetry is process-local. Feature-detect `ctx.telemetry` and expect it to be undefined in sandboxed plugin processes.
- Resolving bundled assets -- use `import.meta.url` + `fileURLToPath` to find files inside your package; never `process.cwd()`. For workspace paths, do the opposite: use `ctx.workspaceInfo?.rootPath`, never `import.meta.url`.
- Plugin name collisions -- `name` must be unique within a session. If two plugins share a name, validation fails. Namespace by package (`my-org-redactor`, not `redactor`).
## Decision Guide -- Which Extension Point?
| You want to... | Use |
|----------------|-----|
| Give the model a new capability | `registerTool` |
| Add a slash command in chat surfaces | `registerCommand` |
| Inject text into the system prompt | `registerRule` |
| Rewrite messages before they hit the provider | `registerMessageBuilder` |
| Add a custom model provider | `registerProvider` |
| Emit normalized cron/webhook events | `registerAutomationEventType` + `ctx.automation` |
| Observe or steer the agent loop | `hooks.*` |
| Block a dangerous tool call | `hooks.beforeTool` returning `{ stop: true }` |
| Notify on completion | `hooks.afterRun` (gate on `status === "completed"`) |
| Tweak each model request | `hooks.beforeModel` |
| Stream events to a UI | `hooks.onEvent` |
| Ship reusable templates with the plugin | Bundle assets next to `index.ts`, resolve via `import.meta.url` |
| Let users override defaults globally or per-project | Three-tier lookup: bundled / global / project |
## Pre-Ship Checklist
- `manifest.capabilities` is a non-empty array.
- Every `api.register*` call has a matching capability declared.
- If `hooks` is present, `"hooks"` is in `capabilities`.
- `ctx.workspaceInfo?.rootPath` is used for workspace paths (not `process.cwd()`).
- Optional `ctx` fields are feature-detected.
- Tool names are snake_case verbs; descriptions are written for the model.
- Tool inputs have JSON Schema with `required` set.
- `afterRun` handlers gate on `result.status === "completed"` if they only want successes.
- State that must not leak between concurrent sessions is keyed by `ctx.session?.sessionId`.
- (Package) `package.json` has `type: "module"`, `cline.plugins`, and `@cline/core` as an optional peer dep.
- (Package) Bundled assets resolved via `import.meta.url`, not `process.cwd()`.
- Smoke test: drop the plugin into `.cline/plugins/` (or `cline plugin install`), run `cline -i "..."`, watch it work.
## Plugin Examples from SDK
The SDK repo includes these example plugins:
| Plugin | Description |
|--------|-------------|
| `weather-metrics.ts` | Tool registration + lifecycle metrics |
| `mac-notify.ts` | macOS Notification Center alerts |
| `custom-compaction.ts` | Custom message compaction via message builders |
| `background-terminal.ts` | Detached shell job management |
| `automation-events.ts` | Plugin-emitted automation events |
| `gitignore-read-files-guard.ts` | File access policy enforcement via beforeTool |
| `web-search.ts` | Web search via Exa API |
| `typescript-lsp/` | TypeScript Language Service tools (plugin package) |
| `agents-squad/` | Multi-agent team orchestration (plugin package) |
## See Also
- `../tools/REFERENCE.md` - Tool creation
- `../events/REFERENCE.md` - Event system
- `../agent/REFERENCE.md` - Using plugins with Agent
- `../clinecore/REFERENCE.md` - Using plugins with ClineCore
@@ -1,253 +0,0 @@
# Going to Production
Guidelines for deploying Cline SDK agents in production environments.
## Error Handling
Always check the result status:
```typescript
const result = await agent.run(input)
switch (result.status) {
case "completed":
console.log("Success:", result.outputText)
break
case "aborted":
console.log("Cancelled:", result.error?.message)
break
case "failed":
console.error("Failed:", result.error)
break
}
```
For ClineCore, check `finishReason`:
```typescript
const session = await cline.start({ ... })
switch (session.result?.finishReason) {
case "completed":
// normal completion
break
case "max_iterations":
// agent hit iteration limit
break
case "aborted":
// manually cancelled
break
case "mistake_limit":
// too many tool errors
break
case "error":
// unrecoverable error
break
}
```
## Cost Control
### Token Limits
Set maximum tokens per turn and iteration limits:
```typescript
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
maxTokensPerTurn: 4096,
maxIterations: 10,
tools: [...],
})
```
### Model Selection
Use cheaper models for simple tasks:
```typescript
// Simple classification or formatting
{ providerId: "anthropic", modelId: "claude-haiku-4-5" }
// Complex reasoning and code generation
{ providerId: "anthropic", modelId: "claude-sonnet-4-6" }
// Hardest tasks requiring deep reasoning
{ providerId: "anthropic", modelId: "claude-opus-4-7" }
```
### Usage Tracking
Monitor spending in real time:
```typescript
agent.subscribe((event) => {
if (event.type === "usage-updated" && event.usage.totalCost) {
if (event.usage.totalCost > MAX_BUDGET) {
agent.abort("Budget exceeded")
}
}
})
```
## Observability
### OpenTelemetry Integration
The SDK supports OpenTelemetry for traces, metrics, and logs:
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({
clientName: "my-app",
// OpenTelemetry config is picked up from environment
// OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, etc.
})
```
### Structured Logging
Use the `BasicLogger` interface for injectable logging:
```typescript
import type { BasicLogger } from "@cline/sdk"
const logger: BasicLogger = {
debug: (msg, meta) => console.debug(msg, meta),
log: (msg, meta) => console.log(msg, meta),
error: (msg, meta) => console.error(msg, meta),
}
await cline.start({
config: {
logger,
// ...
},
})
```
### Custom Metrics via Plugins
```typescript
const metricsPlugin: AgentPlugin = {
name: "metrics",
manifest: { capabilities: ["hooks"] },
setup() {},
hooks: {
beforeRun() {
metrics.increment("agent.runs.started")
},
afterRun({ result }) {
metrics.increment("agent.runs.completed")
metrics.histogram("agent.iterations", result.iterations)
metrics.histogram("agent.tokens.output", result.usage.outputTokens)
},
beforeTool({ toolCall }) {
metrics.increment(`agent.tools.${toolCall.toolName}`)
},
},
}
```
## Security
### Sandbox Tool Execution
Validate tool inputs to prevent path traversal and injection:
```typescript
execute: async (input) => {
const safePath = path.resolve(WORKSPACE_ROOT, input.path)
if (!safePath.startsWith(WORKSPACE_ROOT)) {
return { error: "Path traversal attempt blocked" }
}
return await readFile(safePath, "utf-8")
}
```
### API Key Management
- Use environment variables, never hardcode keys
- Rotate keys regularly
- Use different keys for development and production
```typescript
{
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY, // never a literal string
}
```
### Tool Policy Hardening
Disable tools you don't need and require approval for dangerous ones:
```typescript
toolPolicies: {
read_files: { autoApprove: true },
search: { autoApprove: true },
bash: { autoApprove: false }, // require approval
editor: { autoApprove: false },
apply_patch: { autoApprove: false },
fetch_web: { enabled: false }, // disable entirely
}
```
## Deployment Patterns
### Stateless Worker
For request/response workloads (API endpoints, queue consumers):
```typescript
const cline = await ClineCore.create({
clientName: "worker",
backendMode: "local",
})
app.post("/agent", async (req, res) => {
const session = await cline.start({
prompt: req.body.prompt,
config: { ... },
})
res.json({ text: session.result?.text, usage: session.result?.usage })
})
```
### Persistent Service
For long-running services with session management:
```typescript
const cline = await ClineCore.create({
clientName: "service",
backendMode: "hub",
})
process.on("SIGTERM", async () => {
await cline.dispose("SIGTERM")
process.exit(0)
})
```
### Scheduled Automation
See `../scheduling/REFERENCE.md` for recurring agent tasks.
## Retry and Resilience
- Tool `execute` functions support `retryable: true` (default) and `maxRetries: 3` (default)
- Provider API calls are retried automatically on transient failures
- Use `timeoutMs` on tools to prevent hanging
- Monitor `mistake_limit` finish reason to detect systematic tool failures
## See Also
- `../agent/REFERENCE.md` - Agent overview
- `../clinecore/REFERENCE.md` - ClineCore overview
- `../tools/REFERENCE.md` - Tool configuration
- `../plugins/REFERENCE.md` - Metrics plugins
- `../scheduling/REFERENCE.md` - Scheduled agents
@@ -1,257 +0,0 @@
# Model Providers
The Cline SDK supports every major LLM provider out of the box via `@cline/llms`.
## Supported Providers
| Provider ID | Models |
|-------------|--------|
| `"anthropic"` | Claude Opus 4.7, Sonnet 4.6, Haiku 4.5 |
| `"openai"` | GPT-5.5, GPT-5.3 Codex |
| `"gemini"` | Gemini 3.1 Pro Preview, Gemini 3 Flash Preview |
| `"vertex"` | Google models via Vertex AI |
| `"bedrock"` | Claude, Llama via AWS Bedrock |
| `"mistral"` | Mistral Large, Codestral |
| `"openai-compatible"` | vLLM, Together, Fireworks, Groq, etc. |
## Basic Configuration
### With Agent
```typescript
import { Agent } from "@cline/sdk"
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: "You are a helpful assistant.",
tools: [],
})
```
### With ClineCore
```typescript
import { ClineCore } from "@cline/sdk"
const cline = await ClineCore.create({ clientName: "my-app" })
await cline.start({
prompt: "Hello",
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY,
},
})
```
## Provider-Specific Configuration
### Anthropic
```typescript
{
providerId: "anthropic",
modelId: "claude-opus-4-7", // or "claude-sonnet-4-6", "claude-haiku-4-5"
apiKey: process.env.ANTHROPIC_API_KEY,
}
```
### OpenAI
```typescript
{
providerId: "openai",
modelId: "gpt-5.5",
apiKey: process.env.OPENAI_API_KEY,
}
```
### Google (Gemini)
```typescript
{
providerId: "gemini",
modelId: "gemini-3.1-pro-preview",
apiKey: process.env.GOOGLE_API_KEY,
}
```
### Google (Vertex AI)
```typescript
{
providerId: "vertex",
modelId: "gemini-3.1-pro-preview",
// Uses application default credentials or service account
}
```
### AWS Bedrock
```typescript
{
providerId: "bedrock",
modelId: "anthropic.claude-sonnet-4-6",
// Uses AWS credential chain (env vars, config file, IAM role)
// Set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
}
```
### Mistral
```typescript
{
providerId: "mistral",
modelId: "mistral-large-latest",
apiKey: process.env.MISTRAL_API_KEY,
}
```
### OpenAI-Compatible
For any provider with an OpenAI-compatible API:
```typescript
{
providerId: "openai-compatible",
modelId: "my-model",
apiKey: process.env.API_KEY,
baseUrl: "https://api.together.xyz/v1",
}
```
Works with: vLLM, Together AI, Fireworks, Groq, Ollama, LiteLLM, etc.
## Custom Base URL
Override the API endpoint for any provider:
```typescript
{
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.API_KEY,
baseUrl: "https://my-proxy.example.com/v1",
}
```
## Custom Headers
Pass additional headers to API requests:
```typescript
{
providerId: "openai",
modelId: "gpt-5.5",
apiKey: process.env.API_KEY,
headers: {
"X-Custom-Header": "value",
},
}
```
## Gateway API
For advanced multi-provider setups, use the Gateway directly:
```typescript
import { createGateway, DefaultGateway } from "@cline/llms"
const gateway = createGateway({
providerConfigs: [
{ providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
{ providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
],
})
// Create a model for a specific provider
const model = gateway.createAgentModel({
providerId: "anthropic",
modelId: "claude-opus-4-7",
})
// Use with Agent
const agent = new Agent({ model, systemPrompt: "...", tools: [] })
```
### Gateway Methods
```typescript
gateway.registerProvider(registration) // add a custom provider
gateway.configureProvider(config) // update provider settings
gateway.listProviders() // list available providers
gateway.listModels(providerId?) // list available models
gateway.createAgentModel(selection) // create model for agent
gateway.stream(request) // raw streaming (AsyncIterable)
```
## Provider Registry
Query and register providers programmatically:
```typescript
import {
getAllProviders,
getProviderIds,
getProvider,
getModelsForProvider,
registerProvider,
registerModel,
createHandler,
} from "@cline/llms"
// List all registered providers
const providers = getAllProviders()
// Get models for a provider
const models = getModelsForProvider("anthropic")
// Register a custom provider
registerProvider({
id: "my-provider",
name: "My Custom Provider",
handler: createHandler({ ... }),
})
```
## Model Metadata
Access model info (context window, pricing, capabilities):
```typescript
import { getModelsForProvider } from "@cline/llms"
const models = getModelsForProvider("anthropic")
for (const model of models) {
console.log(`${model.id}: context=${model.contextWindow}, input=$${model.inputPrice}/MTok`)
}
```
## Cost Tracking
Track per-request and cumulative costs:
```typescript
// Via events
agent.subscribe((event) => {
if (event.type === "usage-updated") {
console.log(`Cost: $${event.usage.totalCost?.toFixed(4)}`)
}
})
// Via result
const result = await agent.run("...")
console.log(`Total cost: $${result.usage.totalCost?.toFixed(4)}`)
// Via ClineCore accumulated usage
const usage = await cline.getAccumulatedUsage(sessionId)
```
## See Also
- `../agent/REFERENCE.md` - Using providers with Agent
- `../clinecore/REFERENCE.md` - Using providers with ClineCore
- `../production/REFERENCE.md` - Cost control in production
@@ -1,227 +0,0 @@
# Scheduling and Automation
The Cline SDK supports scheduled, one-off, and event-driven agent execution through the automation subsystem in `@cline/core`.
## Overview
Three trigger types:
| Trigger | Description |
|---------|-------------|
| `schedule` | Recurring jobs via cron expressions |
| `one_off` | Single execution tasks |
| `event` | Triggered by external events (GitHub, Linear, custom) |
## CLI Schedule Management
```bash
# Create a recurring schedule
cline schedule create "Daily standup" \
--cron "0 9 * * MON-FRI" \
--prompt "Summarize open PRs and blockers" \
--workspace /path/to/project \
--model anthropic/claude-sonnet-4-6
# List schedules
cline schedule list
# Trigger a schedule immediately
cline schedule trigger <schedule-id>
# Pause/resume
cline schedule pause <schedule-id>
cline schedule resume <schedule-id>
# Delete
cline schedule delete <schedule-id>
# View past executions
cline schedule executions <schedule-id>
```
## Cron Expressions
| Expression | Meaning |
|-----------|---------|
| `0 9 * * MON-FRI` | 9 AM weekdays |
| `0 */6 * * *` | Every 6 hours |
| `0 8 * * MON` | Mondays at 8 AM |
| `*/30 * * * *` | Every 30 minutes |
| `0 0 1 * *` | First of every month |
## File-Based Specs
Create Markdown files in `~/.cline/cron/` (global) or `.cline/cron/` (workspace):
### Recurring Schedule
```markdown
---
trigger: schedule
schedule: "0 9 * * MON-FRI"
timezone: America/New_York
mode: exclusive
prompt: "Check for dependency updates and create PRs for any outdated packages."
modelSelection:
providerId: anthropic
modelId: claude-sonnet-4-6
tools:
enabled: true
---
Additional context or instructions for the agent go in the body.
```
### One-Off Task
```markdown
---
trigger: one_off
prompt: "Generate a comprehensive test coverage report."
modelSelection:
providerId: anthropic
modelId: claude-sonnet-4-6
---
```
### Event-Driven
```markdown
---
trigger: event
eventType: github.pull_request.opened
filters:
repository: myorg/myrepo
debounceMs: 5000
cooldownMs: 60000
prompt: "Review the PR for security issues and code quality."
modelSelection:
providerId: anthropic
modelId: claude-sonnet-4-6
---
```
## CronSpec Types
```typescript
interface CronScheduleSpec {
trigger: "schedule"
schedule: string // cron expression
timezone?: string
mode?: "exclusive" | "concurrent"
prompt: string
modelSelection?: { providerId: string; modelId?: string }
extensionLoading?: "isolated" | "direct"
configExtensions?: RuntimeConfigExtensionKind[]
tools?: { enabled?: boolean; names?: string[] }
}
interface CronOneOffSpec {
trigger: "one_off"
prompt: string
modelSelection?: { providerId: string; modelId?: string }
}
interface CronEventSpec {
trigger: "event"
eventType: string // e.g., "github.pull_request.opened"
filters?: Record<string, unknown>
debounceMs?: number
cooldownMs?: number
prompt: string
modelSelection?: { providerId: string; modelId?: string }
}
```
## Programmatic Automation API
```typescript
const cline = await ClineCore.create({
clientName: "my-app",
automation: true,
})
// Start automation service
cline.automation.start()
// Ingest an external event
cline.automation.ingestEvent({
eventId: "evt-123",
eventType: "github.pull_request.opened",
source: "github",
timestamp: Date.now(),
payload: { pr: { number: 42, title: "..." } },
})
// List specs, runs, events
const specs = await cline.automation.listSpecs()
const runs = await cline.automation.listRuns()
const events = await cline.automation.listEvents()
// Reconcile specs from directory
await cline.automation.reconcile(specDirectory)
// Stop automation
cline.automation.stop()
```
## Event Ingestion from Plugins
Plugins can declare and emit automation events:
```typescript
const webhookPlugin: AgentPlugin = {
name: "webhook-events",
manifest: { capabilities: ["automationEvents"] },
setup(api) {
api.registerAutomationEventType({
type: "webhook.received",
description: "External webhook received",
})
},
}
```
Submit events via the plugin context:
```typescript
ctx.automation.ingestEvent({
eventId: "evt-456",
eventType: "webhook.received",
source: "custom",
timestamp: Date.now(),
payload: { ... },
})
```
## Concurrency Control
| Mode | Behavior |
|------|----------|
| `"exclusive"` | Skip if previous run still active |
| `"concurrent"` | Allow overlapping runs |
## Run Reports
Each completed run writes a Markdown report to `.cline/cron/reports/<run-id>.md` with:
- Run metadata (spec, trigger, timing)
- Summary of agent output
- Usage (tokens, cost)
- Tool calls made
- Trigger event context (for event-driven runs)
## Use Cases
- Daily standup summaries
- Automated dependency update checks
- PR review on open
- Codebase health reports
- Scheduled security scans
- Event-driven CI/CD workflows
## See Also
- `../clinecore/REFERENCE.md` - ClineCore runtime
- `../clinecore/api.md` - Automation API details
- `../plugins/REFERENCE.md` - Plugin events
- `../production/REFERENCE.md` - Production deployment
@@ -1,259 +0,0 @@
# Tools
Tools are how agents interact with the world. The Cline SDK supports both built-in tools (via ClineCore) and custom tools you define yourself.
## Creating Custom Tools
Use `createTool()` from `@cline/sdk` (or `@cline/shared`):
```typescript
import { createTool } from "@cline/sdk"
const myTool = createTool({
name: "search_issues",
description: "Search GitHub issues by query. Returns up to 10 results.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
state: { type: "string", enum: ["open", "closed", "all"] },
},
required: ["query"],
},
execute: async (input) => {
const issues = await github.searchIssues(input.query, input.state)
return { issues, count: issues.length }
},
})
```
### With Zod Schema
```typescript
import { createTool } from "@cline/sdk"
import { z } from "zod"
const deployTool = createTool({
name: "deploy",
description: "Deploy the app to the specified environment.",
inputSchema: z.object({
environment: z.enum(["staging", "production"]).describe("Target environment"),
version: z.string().optional().describe("Version tag, defaults to latest"),
}),
execute: async (input) => {
const result = await deploy(input.environment, input.version)
return { url: result.url, status: "deployed" }
},
})
```
### Tool Config Options
```typescript
createTool({
name: string, // snake_case, unique per agent
description: string, // what the tool does (model reads this)
inputSchema: JSONSchema | ZodSchema, // input validation
execute: async (input, context, onChange?) => output,
timeoutMs?: number, // default: 30000
retryable?: boolean, // default: true
maxRetries?: number, // default: 3
lifecycle?: {
completesRun?: boolean // true = ends agent loop on success
},
})
```
### AgentToolContext
The second argument to `execute` provides runtime context:
```typescript
interface AgentToolContext {
agentId: string
conversationId: string
iteration: number
abortSignal?: AbortSignal
metadata?: Record<string, unknown>
}
```
## Tool Naming Rules
- Names must be `snake_case` (e.g., `search_issues`, `deploy_app`)
- Names must be unique within a single agent's tool set
- Choose descriptive names since the model uses them to decide which tool to call
## Tool Descriptions Matter
The model reads the tool description to decide when and how to use it. Write clear, specific descriptions:
```typescript
// Bad: vague
description: "Does deployment stuff"
// Good: specific with constraints
description: "Deploy the application to staging or production. " +
"Staging deployments are immediate. Production requires a passing CI build. " +
"Returns the deployment URL and status."
```
Include constraints, rate limits, and expected behavior in the description.
## Error Handling in Tools
Return errors as structured data instead of throwing:
```typescript
// Good: return error data
execute: async (input) => {
const file = await readFile(input.path).catch(() => null)
if (!file) {
return { error: "File not found", path: input.path }
}
return { content: file }
}
```
Thrown exceptions count as "mistakes" against the agent's mistake limit. Returned error data lets the agent adjust its approach.
## Completion Tools
Tools with `lifecycle: { completesRun: true }` end the agent loop when they execute successfully:
```typescript
const submitAnswer = createTool({
name: "submit_answer",
description: "Submit the final answer and end the task.",
inputSchema: z.object({
answer: z.string(),
confidence: z.number().min(0).max(1),
}),
lifecycle: { completesRun: true },
execute: async (input) => input,
})
```
The model sees the tool result and the run ends. Access the output via `result.toolCalls`.
## Built-in Tools (ClineCore Only)
When using `ClineCore` with `enableTools: true`, these tools are available automatically:
| Tool | Name | What It Does |
|------|------|-------------|
| Shell | `bash` | Execute shell commands in the session workspace |
| Editor | `editor` | Create and edit files |
| Read | `read_files` | Read file contents |
| Patch | `apply_patch` | Apply unified diffs to files |
| Search | `search` | Search file contents and directory structure |
| Web | `fetch_web` | Fetch web content via HTTP |
Built-in tools respect the `cwd` setting in `CoreSessionConfig`.
## Tool Policies
Control which tools are available and whether they require approval:
```typescript
// In Agent config
const agent = new Agent({
tools: [toolA, toolB, toolC],
toolPolicies: {
tool_a: { autoApprove: true }, // runs without asking
tool_b: { autoApprove: false }, // requires approval
tool_c: { enabled: false }, // hidden from model
},
})
// In ClineCore session
await cline.start({
prompt: "...",
config: { ... },
toolPolicies: {
bash: { autoApprove: true },
editor: { autoApprove: false },
},
})
```
### Policy Options
| Policy | Effect |
|--------|--------|
| `{ autoApprove: true }` | Tool runs without approval |
| `{ autoApprove: false }` | Triggers approval callback before running |
| `{ enabled: false }` | Tool is hidden from the model entirely |
| No policy set | Defaults to enabled and auto-approved |
## Abort Signal in Long-Running Tools
Respect the abort signal for tools that take a long time:
```typescript
execute: async (input, context) => {
const results = []
for (const item of input.items) {
if (context.abortSignal?.aborted) {
return { results, aborted: true, processed: results.length }
}
results.push(await processItem(item))
}
return { results, processed: results.length }
}
```
## Streaming Tool Output
Use the `onChange` callback (third argument) to stream partial results:
```typescript
execute: async (input, context, onChange) => {
let progress = 0
for (const step of steps) {
progress++
onChange?.(`Processing step ${progress}/${steps.length}...`)
await processStep(step)
}
return { completed: true }
}
```
## Testing Tools
Tools are plain async functions, so they're straightforward to test:
```typescript
import { describe, it, expect } from "vitest"
describe("deploy tool", () => {
it("deploys to staging", async () => {
const context = { agentId: "test", conversationId: "test", iteration: 1 }
const result = await deployTool.execute({ environment: "staging" }, context)
expect(result.status).toBe("deployed")
})
})
```
## MCP Tool Integration
ClineCore can connect to MCP (Model Context Protocol) servers for additional tools. Configure in `.cline/mcp-servers.json`:
```json
{
"servers": {
"my-server": {
"command": "node",
"args": ["./mcp-server.js"]
}
}
}
```
MCP tools appear alongside built-in and custom tools automatically.
## See Also
- `../agent/REFERENCE.md` - Using tools with Agent
- `../clinecore/REFERENCE.md` - Using tools with ClineCore
- `../plugins/REFERENCE.md` - Packaging tools as plugins
-200
View File
@@ -1,200 +0,0 @@
---
name: opentui
description: Comprehensive OpenTUI skill for building terminal user interfaces. Covers the core imperative API, React reconciler, and Solid reconciler. Use for any TUI development task including components, layout, keyboard handling, animations, and testing.
metadata:
references: core, react, solid
---
# OpenTUI Platform Skill
Consolidated skill for building terminal user interfaces with OpenTUI. Use decision trees below to find the right framework and components, then load detailed references.
## Critical Rules
**Follow these rules in all OpenTUI code:**
1. **Use `create-tui` for new projects.** See framework `REFERENCE.md` quick starts.
2. **`create-tui` options must come before arguments.** `bunx create-tui -t react my-app` works, `bunx create-tui my-app -t react` does NOT.
3. **Never call `process.exit()` directly.** Use `renderer.destroy()` (see `core/gotchas.md`).
4. **Text styling requires nested tags in React/Solid.** Use modifier elements, not props (see `components/text-display.md`).
## How to Use This Skill
### Reference File Structure
Framework references follow a 5-file pattern. Cross-cutting concepts are single-file guides.
Each framework in `./references/<framework>/` contains:
| File | Purpose | When to Read |
|------|---------|--------------|
| `REFERENCE.md` | Overview, when to use, quick start | **Always read first** |
| `api.md` | Runtime API, components, hooks | Writing code |
| `configuration.md` | Setup, tsconfig, bundling | Configuring a project |
| `patterns.md` | Common patterns, best practices | Implementation guidance |
| `gotchas.md` | Pitfalls, limitations, debugging | Troubleshooting |
Cross-cutting concepts in `./references/<concept>/` have `REFERENCE.md` as the entry point.
### Reading Order
1. Start with `REFERENCE.md` for your chosen framework
2. Then read additional files relevant to your task:
- Building components -> `api.md` + `components/<category>.md`
- Setting up project -> `configuration.md`
- Layout/positioning -> `layout/REFERENCE.md`
- Keyboard/input handling -> `keyboard/REFERENCE.md`
- Animations -> `animation/REFERENCE.md`
- Troubleshooting -> `gotchas.md` + `testing/REFERENCE.md`
### Example Paths
```
./references/react/REFERENCE.md # Start here for React
./references/react/api.md # React components and hooks
./references/solid/configuration.md # Solid project setup
./references/components/inputs.md # Input, Textarea, Select docs
./references/core/gotchas.md # Core debugging tips
```
### Runtime Notes
OpenTUI runs on Bun and uses Zig for native builds. Read `./references/core/gotchas.md` for runtime requirements and build guidance.
## Quick Decision Trees
### "Which framework should I use?"
```
Which framework?
├─ I want full control, maximum performance, no framework overhead
│ └─ core/ (imperative API)
├─ I know React, want familiar component patterns
│ └─ react/ (React reconciler)
├─ I want fine-grained reactivity, optimal re-renders
│ └─ solid/ (Solid reconciler)
└─ I'm building a library/framework on top of OpenTUI
└─ core/ (imperative API)
```
### "I need to display content"
```
Display content?
├─ Plain or styled text -> components/text-display.md
├─ Container with borders/background -> components/containers.md
├─ Scrollable content area -> components/containers.md (scrollbox)
├─ ASCII art banner/title -> components/text-display.md (ascii-font)
├─ Data table with borders/wrapping -> components/code-diff.md (TextTable)
├─ Code with syntax highlighting -> components/code-diff.md
├─ Diff viewer (unified/split) -> components/code-diff.md
├─ Line numbers with diagnostics -> components/code-diff.md
└─ Markdown content (streaming) -> components/code-diff.md (markdown)
```
### "I need user input"
```
User input?
├─ Single-line text field -> components/inputs.md (input)
├─ Multi-line text editor -> components/inputs.md (textarea)
├─ Select from a list (vertical) -> components/inputs.md (select)
├─ Tab-based selection (horizontal) -> components/inputs.md (tab-select)
└─ Custom keyboard shortcuts -> keyboard/REFERENCE.md
```
### "I need layout/positioning"
```
Layout?
├─ Flexbox-style layouts (row, column, wrap) -> layout/REFERENCE.md
├─ Absolute positioning -> layout/patterns.md
├─ Responsive to terminal size -> layout/patterns.md
├─ Centering content -> layout/patterns.md
└─ Complex nested layouts -> layout/patterns.md
```
### "I need animations"
```
Animations?
├─ Timeline-based animations -> animation/REFERENCE.md
├─ Easing functions -> animation/REFERENCE.md
├─ Property transitions -> animation/REFERENCE.md
└─ Looping animations -> animation/REFERENCE.md
```
### "I need to handle input"
```
Input handling?
├─ Keyboard events (keypress, release) -> keyboard/REFERENCE.md
├─ Focus management -> keyboard/REFERENCE.md
├─ Paste events -> keyboard/REFERENCE.md
├─ Mouse events -> components/containers.md
├─ Text selection & copy-on-select -> keyboard/REFERENCE.md (selection)
└─ Clipboard (OSC 52) -> keyboard/REFERENCE.md (clipboard)
```
### "I need to test my TUI"
```
Testing?
├─ Snapshot testing -> testing/REFERENCE.md
├─ Interaction testing -> testing/REFERENCE.md
├─ Test renderer setup -> testing/REFERENCE.md
└─ Debugging tests -> testing/REFERENCE.md
```
### "I need to debug/troubleshoot"
```
Troubleshooting?
├─ Runtime errors, crashes -> <framework>/gotchas.md
├─ Layout issues -> layout/REFERENCE.md + layout/patterns.md
├─ Input/focus issues -> keyboard/REFERENCE.md
└─ Repro + regression tests -> testing/REFERENCE.md
```
### Troubleshooting Index
- Terminal cleanup, crashes -> `core/gotchas.md`
- Text styling not applying -> `components/text-display.md`
- Input focus/shortcuts -> `keyboard/REFERENCE.md`
- Layout misalignment -> `layout/REFERENCE.md`
- Flaky snapshots -> `testing/REFERENCE.md`
For component naming differences and text modifiers, see `components/REFERENCE.md`.
## Product Index
### Frameworks
| Framework | Entry File | Description |
|-----------|------------|-------------|
| Core | `./references/core/REFERENCE.md` | Imperative API, all primitives |
| React | `./references/react/REFERENCE.md` | React reconciler for declarative TUI |
| Solid | `./references/solid/REFERENCE.md` | SolidJS reconciler for declarative TUI |
### Cross-Cutting Concepts
| Concept | Entry File | Description |
|---------|------------|-------------|
| Layout | `./references/layout/REFERENCE.md` | Yoga/Flexbox layout system |
| Components | `./references/components/REFERENCE.md` | Component reference by category |
| Keyboard | `./references/keyboard/REFERENCE.md` | Keyboard input handling |
| Animation | `./references/animation/REFERENCE.md` | Timeline-based animations |
| Testing | `./references/testing/REFERENCE.md` | Test renderer and snapshots |
### Component Categories
| Category | Entry File | Components |
|----------|------------|------------|
| Text & Display | `./references/components/text-display.md` | text, ascii-font, styled text |
| Containers | `./references/components/containers.md` | box, scrollbox, borders |
| Inputs | `./references/components/inputs.md` | input, textarea, select, tab-select |
| Code & Diff | `./references/components/code-diff.md` | code, line-number, diff, markdown, text-table |
## Resources
**Repository**: https://github.com/anomalyco/opentui
**Core Docs**: https://github.com/anomalyco/opentui/tree/main/packages/core/docs
**Examples**: https://github.com/anomalyco/opentui/tree/main/packages/core/src/examples
**Awesome List**: https://github.com/msmps/awesome-opentui
@@ -1,431 +0,0 @@
# Animation System
OpenTUI provides a timeline-based animation system for smooth property transitions.
## Overview
Animations in OpenTUI use:
- **Timeline**: Orchestrates multiple animations
- **Animation Engine**: Manages timelines and rendering
- **Easing Functions**: Control animation curves
## When to Use
Use this reference when you need timeline-driven animations, easing curves, or progressive transitions.
## Basic Usage
### React
```tsx
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
function AnimatedBox() {
const [width, setWidth] = useState(0)
const timeline = useTimeline({
duration: 2000,
})
useEffect(() => {
timeline.add(
{ width: 0 },
{
width: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].width))
},
}
)
}, [])
return (
<box
width={width}
height={3}
backgroundColor="#6a5acd"
/>
)
}
```
### Solid
```tsx
import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"
function AnimatedBox() {
const [width, setWidth] = createSignal(0)
const timeline = useTimeline({
duration: 2000,
})
onMount(() => {
timeline.add(
{ width: 0 },
{
width: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].width))
},
}
)
})
return (
<box
width={width()}
height={3}
backgroundColor="#6a5acd"
/>
)
}
```
### Core
```typescript
import { createCliRenderer, Timeline, engine } from "@opentui/core"
const renderer = await createCliRenderer()
engine.attach(renderer)
const timeline = new Timeline({
duration: 2000,
autoplay: true,
})
timeline.add(
{ x: 0 },
{
x: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
box.setLeft(Math.round(anim.targets[0].x))
},
}
)
engine.addTimeline(timeline)
```
## Timeline Options
```typescript
const timeline = useTimeline({
duration: 2000, // Total duration in ms
loop: false, // Loop the timeline
autoplay: true, // Start automatically
onComplete: () => {}, // Called when timeline completes
onPause: () => {}, // Called when timeline pauses
})
```
## Timeline Methods
```typescript
// Add animation
timeline.add(target, properties, startTime?)
// Control playback
timeline.play() // Start/resume
timeline.pause() // Pause
timeline.restart() // Restart from beginning
// State
timeline.progress // Current progress (0-1)
timeline.duration // Total duration
```
## Animation Properties
```typescript
timeline.add(
{ value: 0 }, // Target object with initial values
{
value: 100, // Final value
duration: 1000, // Animation duration in ms
ease: "linear", // Easing function
delay: 0, // Delay before starting
onUpdate: (anim) => {
// Called each frame
const current = anim.targets[0].value
},
onComplete: () => {
// Called when this animation completes
},
},
0 // Start time in timeline (optional)
)
```
## Easing Functions
Available easing functions:
### Linear
| Name | Description |
|------|-------------|
| `linear` | Constant speed |
### Quad (Power of 2)
| Name | Description |
|------|-------------|
| `easeInQuad` | Slow start |
| `easeOutQuad` | Slow end |
| `easeInOutQuad` | Slow start and end |
### Cubic (Power of 3)
| Name | Description |
|------|-------------|
| `easeInCubic` | Slower start |
| `easeOutCubic` | Slower end |
| `easeInOutCubic` | Slower start and end |
### Quart (Power of 4)
| Name | Description |
|------|-------------|
| `easeInQuart` | Even slower start |
| `easeOutQuart` | Even slower end |
| `easeInOutQuart` | Even slower start and end |
### Expo (Exponential)
| Name | Description |
|------|-------------|
| `easeInExpo` | Exponential start |
| `easeOutExpo` | Exponential end |
| `easeInOutExpo` | Exponential start and end |
### Back (Overshoot)
| Name | Description |
|------|-------------|
| `easeInBack` | Pull back, then forward |
| `easeOutBack` | Overshoot, then settle |
| `easeInOutBack` | Both |
### Elastic
| Name | Description |
|------|-------------|
| `easeInElastic` | Elastic start |
| `easeOutElastic` | Elastic end (bouncy) |
| `easeInOutElastic` | Both |
### Bounce
| Name | Description |
|------|-------------|
| `easeInBounce` | Bounce at start |
| `easeOutBounce` | Bounce at end |
| `easeInOutBounce` | Both |
## Patterns
### Progress Bar
```tsx
function ProgressBar({ progress }: { progress: number }) {
const [width, setWidth] = useState(0)
const maxWidth = 50
const timeline = useTimeline()
useEffect(() => {
timeline.add(
{ value: width },
{
value: (progress / 100) * maxWidth,
duration: 300,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].value))
},
}
)
}, [progress])
return (
<box flexDirection="column" gap={1}>
<text>Progress: {progress}%</text>
<box width={maxWidth} height={1} backgroundColor="#333">
<box width={width} height={1} backgroundColor="#00FF00" />
</box>
</box>
)
}
```
### Fade In
```tsx
function FadeIn({ children }) {
const [opacity, setOpacity] = useState(0)
const timeline = useTimeline()
useEffect(() => {
timeline.add(
{ opacity: 0 },
{
opacity: 1,
duration: 500,
ease: "easeOutQuad",
onUpdate: (anim) => {
setOpacity(anim.targets[0].opacity)
},
}
)
}, [])
return (
<box style={{ opacity }}>
{children}
</box>
)
}
```
### Looping Animation
```tsx
function Spinner() {
const [frame, setFrame] = useState(0)
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
useEffect(() => {
const interval = setInterval(() => {
setFrame(f => (f + 1) % frames.length)
}, 80)
return () => clearInterval(interval)
}, [])
return <text>{frames[frame]} Loading...</text>
}
```
### Staggered Animation
```tsx
function StaggeredList({ items }) {
const [visibleCount, setVisibleCount] = useState(0)
useEffect(() => {
let count = 0
const interval = setInterval(() => {
count++
setVisibleCount(count)
if (count >= items.length) {
clearInterval(interval)
}
}, 100)
return () => clearInterval(interval)
}, [items.length])
return (
<box flexDirection="column">
{items.slice(0, visibleCount).map((item, i) => (
<text key={i}>{item}</text>
))}
</box>
)
}
```
### Slide In
```tsx
function SlideIn({ children, from = "left" }) {
const [offset, setOffset] = useState(from === "left" ? -20 : 20)
const timeline = useTimeline()
useEffect(() => {
timeline.add(
{ offset: from === "left" ? -20 : 20 },
{
offset: 0,
duration: 300,
ease: "easeOutCubic",
onUpdate: (anim) => {
setOffset(Math.round(anim.targets[0].offset))
},
}
)
}, [])
return (
<box position="relative" left={offset}>
{children}
</box>
)
}
```
## Performance Tips
### Batch Updates
Timeline automatically batches updates within the render loop.
### Use Integer Values
Round animated values for character-based positioning:
```typescript
onUpdate: (anim) => {
setX(Math.round(anim.targets[0].x))
}
```
### Clean Up Timelines
Hooks automatically clean up, but for core:
```typescript
// When done with timeline
engine.removeTimeline(timeline)
```
## Gotchas
### Terminal Refresh Rate
Terminal UIs typically refresh at 60 FPS max. Very fast animations may appear choppy.
### Character Grid
Animations are constrained to character cells. Sub-pixel positioning isn't possible.
### Cleanup in Effects
Always clean up intervals and timelines:
```tsx
useEffect(() => {
const interval = setInterval(...)
return () => clearInterval(interval)
}, [])
```
## See Also
- [React API](../react/api.md) - `useTimeline` hook reference
- [Solid API](../solid/api.md) - `useTimeline` hook reference
- [Core API](../core/api.md) - `AnimationEngine` and `Timeline` classes
- [Layout Patterns](../layout/patterns.md) - Animated positioning and transitions
@@ -1,144 +0,0 @@
# OpenTUI Components
Reference for all OpenTUI components, organized by category. Components are available in all three frameworks (Core, React, Solid) with slight API differences.
## When to Use
Use this reference when you need to find the right component category or compare naming across Core, React, and Solid.
## Component Categories
| Category | Components | File |
|----------|------------|------|
| Text & Display | text, ascii-font, styled text | [text-display.md](./text-display.md) |
| Containers | box, scrollbox, borders | [containers.md](./containers.md) |
| Inputs | input, textarea, select, tab-select | [inputs.md](./inputs.md) |
| Code & Diff | code, line-number, diff, markdown, text-table | [code-diff.md](./code-diff.md) |
## Component Chooser
```
Need a component?
├─ Styled text or ASCII art -> text-display.md
├─ Containers, borders, scrolling -> containers.md
├─ Forms or input controls -> inputs.md
└─ Code blocks, diffs, line numbers, markdown -> code-diff.md
```
## Component Naming
Components have different names across frameworks:
| Concept | Core (Class) | React (JSX) | Solid (JSX) |
|---------|--------------|-------------|-------------|
| Text | `TextRenderable` | `<text>` | `<text>` |
| Box | `BoxRenderable` | `<box>` | `<box>` |
| ScrollBox | `ScrollBoxRenderable` | `<scrollbox>` | `<scrollbox>` |
| Input | `InputRenderable` | `<input>` | `<input>` |
| Textarea | `TextareaRenderable` | `<textarea>` | `<textarea>` |
| Select | `SelectRenderable` | `<select>` | `<select>` |
| Tab Select | `TabSelectRenderable` | `<tab-select>` | `<tab_select>` |
| ASCII Font | `ASCIIFontRenderable` | `<ascii-font>` | `<ascii_font>` |
| Code | `CodeRenderable` | `<code>` | `<code>` |
| Line Number | `LineNumberRenderable` | `<line-number>` | `<line_number>` |
| Diff | `DiffRenderable` | `<diff>` | `<diff>` |
| Markdown | `MarkdownRenderable` | `<markdown>` | `<markdown>` |
| TextTable | `TextTableRenderable` | N/A (Core only) | N/A (Core only) |
**Note**: Solid uses underscores (`tab_select`) while React uses hyphens (`tab-select`). `TextTableRenderable` is used internally by `MarkdownRenderable` for table rendering and is also available as a standalone Core component.
## Common Properties
All components share these layout properties (see [Layout](../layout/REFERENCE.md)):
```tsx
// Positioning
position="relative" | "absolute"
left, top, right, bottom
// Dimensions
width, height
minWidth, maxWidth, minHeight, maxHeight
// Flexbox
flexDirection, flexGrow, flexShrink, flexBasis
justifyContent, alignItems, alignSelf
flexWrap, gap
// Spacing
padding, paddingTop, paddingRight, paddingBottom, paddingLeft
paddingX, paddingY // Axis shorthand (horizontal/vertical)
margin, marginTop, marginRight, marginBottom, marginLeft
marginX, marginY // Axis shorthand (horizontal/vertical)
// Display
display="flex" | "none"
overflow="visible" | "hidden" | "scroll"
zIndex
```
## Quick Examples
### Core (Imperative)
```typescript
import { createCliRenderer, TextRenderable, BoxRenderable } from "@opentui/core"
const renderer = await createCliRenderer()
const box = new BoxRenderable(renderer, {
id: "container",
border: true,
padding: 2,
})
const text = new TextRenderable(renderer, {
id: "greeting",
content: "Hello!",
fg: "#00FF00",
})
box.add(text)
renderer.root.add(box)
```
### React
```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
return (
<box border padding={2}>
<text fg="#00FF00">Hello!</text>
</box>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```
### Solid
```tsx
import { render } from "@opentui/solid"
function App() {
return (
<box border padding={2}>
<text fg="#00FF00">Hello!</text>
</box>
)
}
render(() => <App />)
```
## See Also
- [Core API](../core/api.md) - Imperative component classes
- [React API](../react/api.md) - React component props
- [Solid API](../solid/api.md) - Solid component props
- [Layout](../layout/REFERENCE.md) - Layout system details
@@ -1,672 +0,0 @@
# Code & Diff Components
Components for displaying code with syntax highlighting and diffs in OpenTUI.
## Code Component
Display syntax-highlighted code blocks.
### Basic Usage
```tsx
// React
<code
code={`function hello() {
console.log("Hello, World!");
}`}
language="typescript"
/>
// Solid
<code
code={sourceCode}
language="javascript"
/>
// Core
const codeBlock = new CodeRenderable(renderer, {
id: "code",
code: sourceCode,
language: "typescript",
})
```
### Supported Languages
OpenTUI uses Tree-sitter for syntax highlighting. Common languages:
- `typescript`, `javascript`
- `python`
- `rust`
- `go`
- `json`
- `html`, `css`
- `markdown`
- `bash`, `shell`
### Styling
```tsx
<code
code={sourceCode}
language="typescript"
backgroundColor="#1a1a2e"
showLineNumbers
/>
```
### onHighlight Callback
Intercept and modify syntax highlights before rendering:
```tsx
// Core
const codeBlock = new CodeRenderable(renderer, {
id: "code",
code: sourceCode,
language: "typescript",
onHighlight: (highlights, context) => {
// Add custom highlights
highlights.push([10, 20, "custom.error", {}])
return highlights
},
})
// React/Solid
<code
code={sourceCode}
language="typescript"
onHighlight={(highlights, context) => {
// context: { content, filetype, syntaxStyle }
// Modify and return highlights array
return highlights.filter(h => h[2] !== "comment")
}}
/>
```
**Callback signature:**
- `highlights: SimpleHighlight[]` - Array of `[start, end, scope, metadata]`
- `context: { content, filetype, syntaxStyle }` - Highlighting context
- Return modified highlights array or `undefined` to use original
Supports async callbacks for fetching additional highlight data.
### onChunks Callback
Post-process rendered text chunks after syntax highlighting. Runs after `onHighlight` and receives fully resolved chunks:
```tsx
// Core
const codeBlock = new CodeRenderable(renderer, {
id: "code",
code: sourceCode,
language: "typescript",
onChunks: (chunks, context) => {
// Transform chunks (e.g., add link detection)
return chunks
},
})
// React/Solid
<code
code={sourceCode}
language="typescript"
onChunks={(chunks, context) => {
// context: { content, filetype, syntaxStyle, highlights }
return chunks
}}
/>
```
### Link Detection Utility
Auto-detect URLs in code and add clickable hyperlinks:
```typescript
import { detectLinks } from "@opentui/core"
<code
code={sourceCode}
language="typescript"
onChunks={(chunks, context) => detectLinks(chunks, context)}
/>
```
`detectLinks` examines Tree-sitter highlights to find URL tokens and sets `chunk.link` on matching chunks. Supports async usage.
## TextTable Component
Render data tables with borders, word wrapping, and selection support.
### Basic Usage
```typescript
// Core
import { TextTableRenderable, type TextTableContent } from "@opentui/core"
const content: TextTableContent = [
[[ { text: "Name" } ], [ { text: "Age" } ], [ { text: "Role" } ]],
[[ { text: "Alice" } ], [ { text: "30" } ], [ { text: "Engineer" } ]],
[[ { text: "Bob" } ], [ { text: "25" } ], [ { text: "Designer" } ]],
]
const table = new TextTableRenderable(renderer, {
id: "table",
content,
wrapMode: "word", // "none" | "char" | "word"
columnWidthMode: "content", // "content" | "fill"
cellPadding: 0,
border: true,
outerBorder: true,
borderStyle: "single", // single | double | rounded | bold
selectable: true, // Allow text selection
columnFitter: "balanced", // "proportional" | "balanced"
})
```
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `content` | `TextTableContent` | - | 2D array of cell content |
| `wrapMode` | `"none" \| "char" \| "word"` | `"none"` | Text wrapping in cells |
| `columnWidthMode` | `"content" \| "fill"` | `"content"` | Column sizing strategy |
| `cellPadding` | `number` | `0` | Padding inside cells |
| `border` | `boolean` | `true` | Show inner borders |
| `outerBorder` | `boolean` | `true` | Show outer borders |
| `borderStyle` | `string` | `"single"` | Border style |
| `borderColor` | `string \| RGBA` | - | Border color |
| `selectable` | `boolean` | `false` | Allow text selection |
| `columnFitter` | `"proportional" \| "balanced"` | `"proportional"` | Column width distribution |
### Cell Content Format
Each cell is an array of styled text chunks:
```typescript
type TextTableCellContent = { text: string; fg?: RGBA; bg?: RGBA }[]
type TextTableContent = TextTableCellContent[][] // rows -> cells -> chunks
```
### Selection
```typescript
table.getSelectedText() // Get selected text
table.hasSelection() // Check if text is selected
```
Columnar selection is supported: dragging vertically within a single column selects only that column's content.
## Line Number Component
Code display with line numbers, highlighting, and diagnostics.
### Basic Usage
```tsx
// React
<line-number
code={sourceCode}
language="typescript"
/>
// Solid (note underscore)
<line_number
code={sourceCode}
language="typescript"
/>
// Core
const codeView = new LineNumberRenderable(renderer, {
id: "code-view",
code: sourceCode,
language: "typescript",
})
```
### Line Number Options
```tsx
// React
<line-number
code={sourceCode}
language="typescript"
startLine={1} // Starting line number
showLineNumbers={true} // Display line numbers
/>
// Solid
<line_number
code={sourceCode}
language="typescript"
startLine={1}
showLineNumbers={true}
/>
```
### Line Highlighting
Highlight specific lines:
```tsx
// React
<line-number
code={sourceCode}
language="typescript"
highlightedLines={[5, 10, 15]} // Highlight these lines
/>
// Solid
<line_number
code={sourceCode}
language="typescript"
highlightedLines={[5, 10, 15]}
/>
```
### Diagnostics
Show errors, warnings, and info on specific lines:
```tsx
// React
<line-number
code={sourceCode}
language="typescript"
diagnostics={[
{ line: 3, severity: "error", message: "Unexpected token" },
{ line: 7, severity: "warning", message: "Unused variable" },
{ line: 12, severity: "info", message: "Consider using const" },
]}
/>
// Solid
<line_number
code={sourceCode}
language="typescript"
diagnostics={[
{ line: 3, severity: "error", message: "Unexpected token" },
]}
/>
```
**Diagnostic severity levels:**
- `error` - Red indicator
- `warning` - Yellow indicator
- `info` - Blue indicator
- `hint` - Gray indicator
### Diff Highlighting
Show added/removed lines:
```tsx
<line-number
code={sourceCode}
language="typescript"
addedLines={[5, 6, 7]} // Green background
removedLines={[10, 11]} // Red background
/>
```
## Diff Component
Unified or split diff viewer with syntax highlighting.
### Basic Usage
```tsx
// React
<diff
oldCode={originalCode}
newCode={modifiedCode}
language="typescript"
/>
// Solid
<diff
oldCode={originalCode}
newCode={modifiedCode}
language="typescript"
/>
// Core
const diffView = new DiffRenderable(renderer, {
id: "diff",
oldCode: originalCode,
newCode: modifiedCode,
language: "typescript",
})
```
### Display Modes
```tsx
// Unified diff (default)
<diff
oldCode={old}
newCode={new}
mode="unified"
/>
// Split/side-by-side diff
<diff
oldCode={old}
newCode={new}
mode="split"
/>
```
### Synchronized Scrolling (Split View)
In split view, enable synchronized scrolling between left and right panes:
```tsx
// React/Solid
<diff
oldCode={old}
newCode={new}
mode="split"
syncScroll // Scrolling one pane syncs the other
/>
// Core
const diffView = new DiffRenderable(renderer, {
id: "diff",
diff: unifiedDiff,
view: "split",
syncScroll: true,
})
// Toggle at runtime
diffView.syncScroll = true
diffView.syncScroll = false
```
### Options
```tsx
<diff
oldCode={originalCode}
newCode={modifiedCode}
language="typescript"
mode="unified"
showLineNumbers
context={3} // Lines of context around changes
/>
```
### Styling
```tsx
<diff
oldCode={old}
newCode={new}
addedLineColor="#2d4f2d" // Background for added lines
removedLineColor="#4f2d2d" // Background for removed lines
unchangedLineColor="transparent"
/>
```
### Line Highlighting API (Core)
Programmatically highlight specific lines in a diff:
```typescript
// Set a single line's color
diffView.setLineColor(5, "#2d4f2d")
diffView.setLineColor(5, { gutter: "#333", content: "#2d4f2d" })
// Clear a single line's color
diffView.clearLineColor(5)
// Set multiple lines at once
diffView.setLineColors(new Map([
[1, "#2d4f2d"],
[2, "#4f2d2d"],
]))
// Highlight a range
diffView.highlightLines(10, 20, "#2d4f2d")
diffView.clearHighlightLines(10, 20)
// Clear all line colors
diffView.clearAllLineColors()
```
The `LineNumberRenderable` also supports programmatic highlighting:
```typescript
lineNumberView.highlightLines(5, 10, "#2d4f2d")
lineNumberView.clearHighlightLines(5, 10)
```
```
## Markdown Component
Render markdown content with syntax highlighting for code blocks.
### Basic Usage
```tsx
// React
<markdown
content={markdownText}
syntaxStyle={mySyntaxStyle}
/>
// Solid
<markdown
content={markdownText}
syntaxStyle={mySyntaxStyle}
/>
// Core
import { MarkdownRenderable } from "@opentui/core"
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "# Hello\n\nThis is **markdown**.",
syntaxStyle: mySyntaxStyle,
})
```
### Options
```tsx
<markdown
content={markdownText}
syntaxStyle={syntaxStyle}
treeSitterClient={client} // Optional: custom tree-sitter client
conceal={true} // Hide markdown syntax characters
streaming={true} // Enable streaming mode for incremental updates
tableOptions={{ // Customize markdown table rendering
widthMode: "full", // "content" | "full"
wrapMode: "word", // "none" | "char" | "word"
cellPadding: 0,
borders: true,
outerBorder: true,
borderStyle: "single",
borderColor: "#555",
selectable: true, // Tables are selectable by default
}}
/>
```
### Custom Node Rendering
```tsx
// Core
const md = new MarkdownRenderable(renderer, {
id: "markdown",
content: "# Custom Heading",
syntaxStyle,
renderNode: (node, ctx, defaultRender) => {
if (node.type === "heading") {
// Return custom renderable for headings
return new TextRenderable(ctx, {
content: `>> ${node.content} <<`,
})
}
return null // Use default rendering
},
})
```
### Streaming Mode
For real-time content like LLM output:
```tsx
const [content, setContent] = useState("")
// Append text as it arrives
useEffect(() => {
llmStream.on("token", (token) => {
setContent(c => c + token)
})
}, [])
<markdown
content={content}
syntaxStyle={syntaxStyle}
streaming={true} // Optimizes for incremental updates
/>
```
## Use Cases
### Code Editor
```tsx
function CodeEditor() {
const [code, setCode] = useState(`function hello() {
console.log("Hello!");
}`)
return (
<box flexDirection="column" height="100%">
<box height={1}>
<text>editor.ts</text>
</box>
<textarea
value={code}
onChange={setCode}
language="typescript"
showLineNumbers
flexGrow={1}
focused
/>
</box>
)
}
```
### Code Review
```tsx
function CodeReview({ oldCode, newCode }) {
return (
<box flexDirection="column" height="100%">
<box height={1} backgroundColor="#333">
<text>Changes in src/utils.ts</text>
</box>
<diff
oldCode={oldCode}
newCode={newCode}
language="typescript"
mode="split"
showLineNumbers
/>
</box>
)
}
```
### Syntax-Highlighted Preview
```tsx
function MarkdownPreview({ content }) {
// Extract code blocks from markdown
const codeBlocks = extractCodeBlocks(content)
return (
<scrollbox height={20}>
{codeBlocks.map((block, i) => (
<box key={i} marginBottom={1}>
<code
code={block.code}
language={block.language}
/>
</box>
))}
</scrollbox>
)
}
```
### Error Display
```tsx
function ErrorView({ errors, code }) {
const diagnostics = errors.map(err => ({
line: err.line,
severity: "error",
message: err.message,
}))
return (
<line-number
code={code}
language="typescript"
diagnostics={diagnostics}
highlightedLines={errors.map(e => e.line)}
/>
)
}
```
## Gotchas
### Solid Uses Underscores
```tsx
// React
<line-number />
// Solid
<line_number />
```
### Language Required for Highlighting
```tsx
// No highlighting (plain text)
<code code={text} />
// With highlighting
<code code={text} language="typescript" />
```
### Large Files
For very large files, consider:
- Pagination or virtual scrolling
- Loading only visible portion
- Using `scrollbox` wrapper
```tsx
<scrollbox height={30}>
<line-number
code={largeFile}
language="typescript"
/>
</scrollbox>
```
### Tree-sitter Loading
Syntax highlighting requires Tree-sitter grammars. If highlighting isn't working:
1. Check the language is supported
2. Verify grammars are installed
3. Check `OTUI_TREE_SITTER_WORKER_PATH` if using custom path
@@ -1,417 +0,0 @@
# Container Components
Components for grouping and organizing content in OpenTUI.
## Box Component
The primary container component with borders, backgrounds, and layout capabilities.
### Basic Usage
```tsx
// React/Solid
<box>
<text>Content inside box</text>
</box>
// Core
const box = new BoxRenderable(renderer, {
id: "container",
})
box.add(child)
```
### Borders
```tsx
<box border>
Simple border
</box>
<box
border
borderStyle="single" // single | double | rounded | bold | none
borderColor="#FFFFFF"
>
Styled border
</box>
// Individual borders
<box
borderTop
borderBottom
borderLeft={false}
borderRight={false}
>
Top and bottom only
</box>
```
**Border Styles:**
| Style | Appearance |
|-------|------------|
| `single` | `┌─┐│ │└─┘` |
| `double` | `╔═╗║ ║╚═╝` |
| `rounded` | `╭─╮│ │╰─╯` |
| `bold` | `┏━┓┃ ┃┗━┛` |
### Title
```tsx
<box
border
title="Settings"
titleAlignment="center" // left | center | right
>
Panel content
</box>
```
### Background
```tsx
<box backgroundColor="#1a1a2e">
Dark background
</box>
<box backgroundColor="transparent">
No background
</box>
```
### Layout
Boxes are flex containers by default:
```tsx
<box
flexDirection="row" // row | column | row-reverse | column-reverse
justifyContent="center" // flex-start | flex-end | center | space-between | space-around
alignItems="center" // flex-start | flex-end | center | stretch | baseline
gap={2} // Space between children
>
<text>Item 1</text>
<text>Item 2</text>
</box>
```
### Spacing
```tsx
<box
padding={2} // All sides
paddingTop={1}
paddingRight={2}
paddingBottom={1}
paddingLeft={2}
paddingX={2} // Horizontal (left + right)
paddingY={1} // Vertical (top + bottom)
margin={1}
marginTop={1}
marginX={2} // Horizontal (left + right)
marginY={1} // Vertical (top + bottom)
>
Spaced content
</box>
```
### Dimensions
```tsx
<box
width={40} // Fixed width
height={10} // Fixed height
width="50%" // Percentage of parent
minWidth={20} // Minimum width
maxWidth={80} // Maximum width
flexGrow={1} // Grow to fill space
>
Sized box
</box>
```
### Mouse Events
```tsx
<box
onMouseDown={(event) => {
console.log("Clicked at:", event.x, event.y)
}}
onMouseUp={(event) => {}}
onMouseMove={(event) => {}}
>
Clickable box
</box>
```
### Focusable Boxes
By default, Box elements are not focusable. Set the `focusable` prop to enable focus behavior:
```tsx
// Make a box focusable - it can receive focus via mouse click
<box focusable border>
<text>Click to focus</text>
</box>
// Controlled focus state
const [focused, setFocused] = useState(false)
<box
focusable
focused={focused}
border
borderColor={focused ? "#00ff00" : "#888"}
>
<text>{focused ? "Focused!" : "Not focused"}</text>
</box>
```
When a focusable Box is clicked, focus bubbles up from the click target to the nearest focusable parent. Use `event.preventDefault()` in `onMouseDown` to prevent auto-focus.
## ScrollBox Component
A scrollable container for content that exceeds the viewport.
### Basic Usage
```tsx
// React
<scrollbox height={10}>
{items.map((item, i) => (
<text key={i}>{item}</text>
))}
</scrollbox>
// Solid
<scrollbox height={10}>
<For each={items()}>
{(item) => <text>{item}</text>}
</For>
</scrollbox>
// Core
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "list",
height: 10,
})
items.forEach(item => {
scrollbox.add(new TextRenderable(renderer, { content: item }))
})
```
### Focus for Keyboard Scrolling
```tsx
<scrollbox focused height={20}>
{/* Use arrow keys to scroll */}
</scrollbox>
```
### Scrollbar Styling
```tsx
// React
<scrollbox
style={{
rootOptions: {
backgroundColor: "#24283b",
},
wrapperOptions: {
backgroundColor: "#1f2335",
},
viewportOptions: {
backgroundColor: "#1a1b26",
},
contentOptions: {
backgroundColor: "#16161e",
},
scrollbarOptions: {
showArrows: true,
trackOptions: {
foregroundColor: "#7aa2f7",
backgroundColor: "#414868",
},
},
}}
>
{content}
</scrollbox>
```
### Scroll Position (Core)
```typescript
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "list",
height: 20,
})
// Scroll programmatically
scrollbox.scrollTo(0) // Scroll to top
scrollbox.scrollTo(100) // Scroll to position
scrollbox.scrollBy(10) // Scroll relative
scrollbox.scrollToBottom() // Scroll to end
// Scroll a child into view (nearest alignment)
scrollbox.scrollChildIntoView("child-id") // Searches descendants by ID
```
`scrollChildIntoView(childId)` scrolls the minimum amount needed to make the identified descendant visible. It mirrors `Element.scrollIntoView({ block: "nearest" })` from the CSSOM View spec. Works with nested descendants and handles both horizontal and vertical scrolling.
## Composition Patterns
### Card Component
```tsx
function Card({ title, children }) {
return (
<box
border
borderStyle="rounded"
padding={2}
marginBottom={1}
>
{title && (
<text fg="#00FFFF" bold>
{title}
</text>
)}
<box marginTop={title ? 1 : 0}>
{children}
</box>
</box>
)
}
```
### Panel Component
```tsx
function Panel({ title, children, width = 40 }) {
return (
<box
border
borderStyle="double"
width={width}
backgroundColor="#1a1a2e"
>
{title && (
<box
borderBottom
padding={1}
backgroundColor="#2a2a4e"
>
<text bold>{title}</text>
</box>
)}
<box padding={2}>
{children}
</box>
</box>
)
}
```
### List Container
```tsx
function List({ items, renderItem }) {
return (
<scrollbox height={15} focused>
{items.map((item, i) => (
<box
key={i}
padding={1}
backgroundColor={i % 2 === 0 ? "#222" : "#333"}
>
{renderItem(item, i)}
</box>
))}
</scrollbox>
)
}
```
## Nesting Containers
```tsx
<box flexDirection="column" height="100%">
{/* Header */}
<box height={3} border>
<text>Header</text>
</box>
{/* Main area with sidebar */}
<box flexDirection="row" flexGrow={1}>
<box width={20} border>
<text>Sidebar</text>
</box>
<box flexGrow={1}>
<scrollbox height="100%">
{/* Scrollable content */}
</scrollbox>
</box>
</box>
{/* Footer */}
<box height={1}>
<text>Footer</text>
</box>
</box>
```
## Gotchas
### Percentage Dimensions Need Parent Size
```tsx
// WRONG - parent has no explicit size
<box>
<box width="50%">Won't work</box>
</box>
// CORRECT
<box width="100%">
<box width="50%">Works</box>
</box>
```
### FlexGrow Needs Sized Parent
```tsx
// WRONG
<box>
<box flexGrow={1}>Won't grow</box>
</box>
// CORRECT
<box height="100%">
<box flexGrow={1}>Will grow</box>
</box>
```
### ScrollBox Needs Height
```tsx
// WRONG - no height constraint
<scrollbox>
{items}
</scrollbox>
// CORRECT
<scrollbox height={20}>
{items}
</scrollbox>
```
### Borders Add to Size
Borders take up space inside the box:
```tsx
<box width={10} border>
{/* Inner content area is 8 chars (10 - 2 for borders) */}
</box>
```
@@ -1,531 +0,0 @@
# Input Components
Components for user input in OpenTUI.
## Input Component
Single-line text input field.
### Basic Usage
```tsx
// React
<input
value={value}
onChange={(newValue) => setValue(newValue)}
placeholder="Enter text..."
focused
/>
// Solid
<input
value={value()}
onInput={(newValue) => setValue(newValue)}
placeholder="Enter text..."
focused
/>
// Core
const input = new InputRenderable(renderer, {
id: "name",
placeholder: "Enter text...",
})
input.on(InputRenderableEvents.CHANGE, (value) => {
console.log("Value:", value)
})
input.focus()
```
### Styling
```tsx
<input
width={30}
backgroundColor="#1a1a1a"
textColor="#FFFFFF"
cursorColor="#00FF00"
focusedBackgroundColor="#2a2a2a"
placeholderColor="#666666"
/>
```
### Events
```tsx
// React
<input
onChange={(value) => console.log("Changed:", value)}
onFocus={() => console.log("Focused")}
onBlur={() => console.log("Blurred")}
/>
// Core
input.on(InputRenderableEvents.CHANGE, (value) => {})
input.on(InputRenderableEvents.FOCUS, () => {})
input.on(InputRenderableEvents.BLUR, () => {})
```
### Controlled Input
```tsx
// React
function ControlledInput() {
const [value, setValue] = useState("")
return (
<input
value={value}
onChange={setValue}
focused
/>
)
}
// Solid
function ControlledInput() {
const [value, setValue] = createSignal("")
return (
<input
value={value()}
onInput={setValue}
focused
/>
)
}
```
## Textarea Component
Multi-line text input field.
### Basic Usage
```tsx
// React
<textarea
value={text}
onChange={(newText) => setText(newText)}
placeholder="Enter multiple lines..."
width={40}
height={10}
focused
/>
// Solid
<textarea
value={text()}
onInput={(newText) => setText(newText)}
placeholder="Enter multiple lines..."
width={40}
height={10}
focused
/>
// Core
const textarea = new TextareaRenderable(renderer, {
id: "editor",
width: 40,
height: 10,
placeholder: "Enter text...",
})
```
### Features
```tsx
<textarea
showLineNumbers // Display line numbers
wrapText // Wrap long lines
readOnly // Disable editing
tabSize={2} // Tab character width
/>
```
### Syntax Highlighting
```tsx
<textarea
language="typescript"
value={code}
onChange={setCode}
/>
```
## Select Component
List selection for choosing from options.
### Basic Usage
```tsx
// React
<select
options={[
{ name: "Option 1", description: "First option", value: "1" },
{ name: "Option 2", description: "Second option", value: "2" },
{ name: "Option 3", description: "Third option", value: "3" },
]}
onSelect={(index, option) => {
console.log("Selected:", option.name) // Called when Enter is pressed
}}
focused
/>
// Solid
<select
options={[
{ name: "Option 1", description: "First option", value: "1" },
{ name: "Option 2", description: "Second option", value: "2" },
]}
onSelect={(index, option) => {
console.log("Selected:", option.name) // Called when Enter is pressed
}}
focused
/>
// Core
const select = new SelectRenderable(renderer, {
id: "menu",
options: [
{ name: "Option 1", description: "First option", value: "1" },
{ name: "Option 2", description: "Second option", value: "2" },
],
})
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Selected:", option.name) // Called when Enter is pressed
})
select.focus()
```
### Option Format
```typescript
interface SelectOption {
name: string // Display text
description?: string // Optional description shown below
value?: any // Associated value
}
```
### Styling
```tsx
<select
height={8} // Visible height
selectedIndex={0} // Initially selected
showScrollIndicator // Show scroll arrows
selectedBackgroundColor="#333"
selectedTextColor="#fff"
highlightBackgroundColor="#444"
/>
```
### Navigation
Default keybindings:
- `Up` / `k` - Move up
- `Down` / `j` - Move down
- `Enter` - Select item
### Events
**Important**: `onSelect` and `onChange` serve different purposes:
| Event | Trigger | Use Case |
|-------|---------|----------|
| `onSelect` | **Enter key pressed** - user confirms selection | Perform action with selected item |
| `onChange` | **Arrow keys** - user navigates list | Preview, update UI as user browses |
```tsx
// React/Solid
<select
onSelect={(index, option) => {
// Called when Enter is pressed - selection confirmed
console.log("User selected:", option.name)
performAction(option)
}}
onChange={(index, option) => {
// Called when navigating with arrow keys
console.log("Browsing:", option.name)
showPreview(option)
}}
/>
// Core
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
// Called when Enter is pressed
})
select.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {
// Called when navigating with arrow keys
})
```
## Tab Select Component
Horizontal tab-based selection.
### Basic Usage
```tsx
// React
<tab-select
options={[
{ name: "Home", description: "Dashboard view" },
{ name: "Settings", description: "Configuration" },
{ name: "Help", description: "Documentation" },
]}
onSelect={(index, option) => {
console.log("Tab selected:", option.name) // Called when Enter is pressed
}}
focused
/>
// Solid (note underscore)
<tab_select
options={[
{ name: "Home", description: "Dashboard view" },
{ name: "Settings", description: "Configuration" },
]}
onSelect={(index, option) => {
console.log("Tab selected:", option.name) // Called when Enter is pressed
}}
focused
/>
// Core
const tabs = new TabSelectRenderable(renderer, {
id: "tabs",
options: [...],
tabWidth: 20,
})
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Tab selected:", option.name) // Called when Enter is pressed
})
tabs.focus()
```
### Events
Same pattern as Select - `onSelect` for Enter key, `onChange` for navigation:
```tsx
<tab-select
onSelect={(index, option) => {
// Called when Enter is pressed - switch to tab
setActiveTab(index)
}}
onChange={(index, option) => {
// Called when navigating with arrow keys
showTabPreview(option)
}}
/>
```
### Styling
```tsx
// React
<tab-select
tabWidth={20} // Width of each tab
selectedIndex={0} // Initially selected tab
/>
// Solid
<tab_select
tabWidth={20}
selectedIndex={0}
/>
```
### Navigation
Default keybindings:
- `Left` / `[` - Previous tab
- `Right` / `]` - Next tab
- `Enter` - Select tab
## Focus Management
### Single Focused Input
```tsx
function SingleInput() {
return <input placeholder="I'm focused" focused />
}
```
### Multiple Inputs with Focus State
```tsx
// React
function Form() {
const [focusIndex, setFocusIndex] = useState(0)
const fields = ["name", "email", "message"]
useKeyboard((key) => {
if (key.name === "tab") {
setFocusIndex(i => (i + 1) % fields.length)
}
})
return (
<box flexDirection="column" gap={1}>
{fields.map((field, i) => (
<input
key={field}
placeholder={`Enter ${field}`}
focused={i === focusIndex}
/>
))}
</box>
)
}
```
### Focus Methods (Core)
```typescript
input.focus() // Give focus
input.blur() // Remove focus
input.isFocused() // Check focus state
```
## Form Patterns
### Login Form
```tsx
function LoginForm() {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [focusField, setFocusField] = useState<"username" | "password">("username")
useKeyboard((key) => {
if (key.name === "tab") {
setFocusField(f => f === "username" ? "password" : "username")
}
if (key.name === "enter") {
handleLogin()
}
})
return (
<box flexDirection="column" gap={1} border padding={2}>
<box flexDirection="row" gap={1}>
<text>Username:</text>
<input
value={username}
onChange={setUsername}
focused={focusField === "username"}
width={20}
/>
</box>
<box flexDirection="row" gap={1}>
<text>Password:</text>
<input
value={password}
onChange={setPassword}
focused={focusField === "password"}
width={20}
/>
</box>
</box>
)
}
```
### Search with Results
```tsx
function SearchableList({ items, onItemSelected }) {
const [query, setQuery] = useState("")
const [focusSearch, setFocusSearch] = useState(true)
const [preview, setPreview] = useState(null)
const filtered = items.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
)
useKeyboard((key) => {
if (key.name === "tab") {
setFocusSearch(f => !f)
}
})
return (
<box flexDirection="column">
<input
value={query}
onChange={setQuery}
placeholder="Search..."
focused={focusSearch}
/>
<select
options={filtered.map(item => ({ name: item }))}
focused={!focusSearch}
height={10}
onSelect={(index, option) => {
// Enter pressed - confirm selection
onItemSelected(option)
}}
onChange={(index, option) => {
// Navigating - show preview
setPreview(option)
}}
/>
</box>
)
}
```
## Gotchas
### Focus Required
Inputs must be focused to receive keyboard input:
```tsx
// WRONG - won't receive input
<input placeholder="Type here" />
// CORRECT
<input placeholder="Type here" focused />
```
### Select Options Format
Options must be objects with `name` property:
```tsx
// WRONG
<select options={["a", "b", "c"]} />
// CORRECT
<select options={[
{ name: "A", description: "Option A" },
{ name: "B", description: "Option B" },
]} />
```
### Solid Uses Underscores
```tsx
// React
<tab-select />
// Solid
<tab_select />
```
### Value vs onInput (Solid)
Solid uses `onInput` instead of `onChange`:
```tsx
// React
<input value={value} onChange={setValue} />
// Solid
<input value={value()} onInput={setValue} />
```
@@ -1,386 +0,0 @@
# Text & Display Components
Components for displaying text content in OpenTUI.
## Text Component
The primary component for displaying styled text.
### Basic Usage
```tsx
// React/Solid
<text>Hello, World!</text>
// With content prop
<text content="Hello, World!" />
// Core
const text = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, World!",
})
```
### Styling (React/Solid)
For React and Solid, use **nested modifier tags** for text styling:
```tsx
<text fg="#FFFFFF" bg="#000000">
<strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
</text>
```
> **Important**: Do NOT use `bold`, `italic`, `underline`, `dim`, `strikethrough` as props on `<text>` — they don't work. Always use nested tags like `<strong>`, `<em>`, `<u>`, or `<span>` with styling.
### Styling (Core) - Text Attributes
```typescript
import { TextRenderable, TextAttributes } from "@opentui/core"
const text = new TextRenderable(renderer, {
content: "Styled",
attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
})
```
**Available attributes:**
- `TextAttributes.BOLD`
- `TextAttributes.DIM`
- `TextAttributes.ITALIC`
- `TextAttributes.UNDERLINE`
- `TextAttributes.BLINK`
- `TextAttributes.INVERSE`
- `TextAttributes.HIDDEN`
- `TextAttributes.STRIKETHROUGH`
### Text Selection
```tsx
<text selectable>
This text can be selected by the user
</text>
<text selectable={false}>
This text cannot be selected
</text>
```
For copy-on-selection and the full selection API, see `keyboard/REFERENCE.md` (selection).
## Text Modifiers
Inline styling elements that must be used inside `<text>`:
### Span
Inline styled text:
```tsx
<text>
Normal text with <span fg="red">red text</span> inline
</text>
```
### Bold/Strong
```tsx
<text>
<strong>Bold text</strong>
<b>Also bold</b>
</text>
```
### Italic/Emphasis
```tsx
<text>
<em>Italic text</em>
<i>Also italic</i>
</text>
```
### Underline
```tsx
<text>
<u>Underlined text</u>
</text>
```
### Line Break
```tsx
<text>
Line one
<br />
Line two
</text>
```
### Link
```tsx
<text>
Visit <a href="https://example.com">our website</a>
</text>
```
### Combined Modifiers
```tsx
<text>
<span fg="#00FF00">
<strong>Bold green</strong>
</span>
and
<span fg="#FF0000">
<em><u>italic underlined red</u></em>
</span>
</text>
```
## Styled Text Template (Core)
The `t` template literal for complex styling:
```typescript
import { t, bold, italic, underline, fg, bg, dim } from "@opentui/core"
const styled = t`
${bold("Bold")} and ${italic("italic")} text.
${fg("#FF0000")("Red text")} with ${bg("#0000FF")("blue background")}.
${dim("Dimmed")} and ${underline("underlined")}.
`
const text = new TextRenderable(renderer, {
content: styled,
})
```
### Style Functions
| Function | Description |
|----------|-------------|
| `bold(text)` | Bold text |
| `italic(text)` | Italic text |
| `underline(text)` | Underlined text |
| `dim(text)` | Dimmed text |
| `strikethrough(text)` | Strikethrough text |
| `fg(color)(text)` | Set foreground color |
| `bg(color)(text)` | Set background color |
## ASCII Font Component
Display large ASCII art text banners.
### Basic Usage
```tsx
// React
<ascii-font text="TITLE" font="tiny" />
// Solid
<ascii_font text="TITLE" font="tiny" />
// Core
const title = new ASCIIFontRenderable(renderer, {
id: "title",
text: "TITLE",
font: "tiny",
})
```
### Available Fonts
| Font | Description |
|------|-------------|
| `tiny` | Compact ASCII font |
| `block` | Block-style letters |
| `slick` | Sleek modern style |
| `shade` | Shaded 3D effect |
### Styling
```tsx
// React
<ascii-font
text="HELLO"
font="block"
color="#00FF00"
/>
// Core
import { RGBA } from "@opentui/core"
const title = new ASCIIFontRenderable(renderer, {
text: "HELLO",
font: "block",
color: RGBA.fromHex("#00FF00"),
})
```
### Example Output
```
Font: tiny
╭─╮╭─╮╭─╮╭╮╭╮╭─╮╶╮╶ ╶╮
│ ││─┘├┤ │╰╯││ │ │
╰─╯╵ ╰─╯╵ ╵╰─╯╶╯╶╰─╯
Font: block
█▀▀█ █▀▀█ █▀▀ █▀▀▄
█ █ █▀▀▀ █▀▀ █ █
▀▀▀▀ ▀ ▀▀▀ ▀ ▀
```
## Colors
### Color Formats
```tsx
// Hex colors
<text fg="#FF0000">Red</text>
<text fg="#F00">Short hex</text>
// Named colors
<text fg="red">Red</text>
<text fg="blue">Blue</text>
// Transparent
<text bg="transparent">No background</text>
```
### RGBA Class
The `RGBA` class from `@opentui/core` can be used in **all frameworks** (Core, React, Solid) for programmatic color manipulation:
```typescript
import { RGBA } from "@opentui/core"
// From hex string (most common)
const red = RGBA.fromHex("#FF0000")
const shortHex = RGBA.fromHex("#F00") // Short form supported
// From integers (0-255 range for each channel)
const green = RGBA.fromInts(0, 255, 0, 255) // r, g, b, a
const semiGreen = RGBA.fromInts(0, 255, 0, 128) // 50% transparent
// From normalized floats (0.0-1.0 range)
const blue = RGBA.fromValues(0.0, 0.0, 1.0, 1.0) // r, g, b, a
const overlay = RGBA.fromValues(0.1, 0.1, 0.1, 0.7) // Dark semi-transparent
// Common use cases
const backgroundColor = RGBA.fromHex("#1a1a2e")
const textColor = RGBA.fromHex("#FFFFFF")
const borderColor = RGBA.fromInts(122, 162, 247, 255) // Tokyo Night blue
const shadowColor = RGBA.fromValues(0.0, 0.0, 0.0, 0.5) // 50% black
```
**When to use each method:**
- `fromHex()` - When working with design specs or CSS colors
- `fromInts()` - When you have 8-bit color values (0-255)
- `fromValues()` - When doing color math or interpolation (normalized 0.0-1.0)
### Using RGBA in React/Solid
```tsx
// React or Solid - RGBA works with color props
import { RGBA } from "@opentui/core"
const primaryColor = RGBA.fromHex("#7aa2f7")
function MyComponent() {
return (
<box backgroundColor={primaryColor} borderColor={primaryColor}>
<text fg={RGBA.fromHex("#c0caf5")}>Styled with RGBA</text>
</box>
)
}
```
Most props that accept color strings (`"#FF0000"`, `"red"`) also accept `RGBA` objects directly.
## Text Wrapping
Text wraps based on parent container:
```tsx
<box width={40}>
<text>
This long text will wrap when it reaches the edge of the
40-character wide parent container.
</text>
</box>
```
## Dynamic Content
### React
```tsx
function Counter() {
const [count, setCount] = useState(0)
return <text>Count: {count}</text>
}
```
### Solid
```tsx
function Counter() {
const [count, setCount] = createSignal(0)
return <text>Count: {count()}</text>
}
```
### Core
```typescript
const text = new TextRenderable(renderer, {
id: "counter",
content: "Count: 0",
})
// Update later
text.setContent("Count: 1")
```
## Gotchas
### Text Modifiers Outside Text
```tsx
// WRONG - modifiers only work inside <text>
<box>
<strong>Won't work</strong>
</box>
// CORRECT
<box>
<text>
<strong>This works</strong>
</text>
</box>
```
### Empty Text
```tsx
// May cause layout issues
<text></text>
// Better - use space or conditional
<text>{content || " "}</text>
```
### Color Format
```tsx
// WRONG
<text fg="FF0000">Missing #</text>
// CORRECT
<text fg="#FF0000">With #</text>
```
@@ -1,145 +0,0 @@
# OpenTUI Core (@opentui/core)
The foundational library for building terminal user interfaces. Provides an imperative API with all primitives, giving you maximum control over rendering, state, and behavior.
## Overview
OpenTUI Core runs on Bun with native Zig bindings for performance-critical operations:
- **Renderer**: Manages terminal output, input events, and the rendering loop
- **Renderables**: Hierarchical UI building blocks with Yoga layout
- **Constructs**: Declarative wrappers for composing Renderables
- **FrameBuffer**: Low-level 2D rendering surface for custom graphics
## When to Use Core
Use the core imperative API when:
- Building a library or framework on top of OpenTUI
- Need maximum control over rendering and state
- Want smallest possible bundle size (no React/Solid runtime)
- Building performance-critical applications
- Integrating with existing imperative codebases
## When NOT to Use Core
| Scenario | Use Instead |
|----------|-------------|
| Familiar with React patterns | `@opentui/react` |
| Want fine-grained reactivity | `@opentui/solid` |
| Building typical applications | React or Solid reconciler |
| Rapid prototyping | React or Solid reconciler |
## Quick Start
### Using create-tui (Recommended)
```bash
bunx create-tui@latest -t core my-app
cd my-app
bun run src/index.ts
```
The CLI creates the `my-app` directory for you - it must **not already exist**.
**Agent guidance**: Always use autonomous mode with `-t <template>` flag. Never use interactive mode (`bunx create-tui@latest my-app` without `-t`) as it requires user prompts that agents cannot respond to.
### Manual Setup
```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/core
```
```typescript
import { createCliRenderer, TextRenderable, BoxRenderable } from "@opentui/core"
const renderer = await createCliRenderer()
// Create a box container
const container = new BoxRenderable(renderer, {
id: "container",
width: 40,
height: 10,
border: true,
borderStyle: "rounded",
padding: 1,
})
// Create text inside the box
const greeting = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, OpenTUI!",
fg: "#00FF00",
})
// Compose the tree
container.add(greeting)
renderer.root.add(container)
```
## Core Concepts
### Renderer
The `CliRenderer` orchestrates everything:
- Manages the terminal viewport and alternate screen
- Handles input events (keyboard, mouse, paste)
- Runs the rendering loop (configurable FPS)
- Provides the root node for the renderable tree
### Renderables vs Constructs
| Renderables (Imperative) | Constructs (Declarative) |
|--------------------------|--------------------------|
| `new TextRenderable(renderer, {...})` | `Text({...})` |
| Requires renderer at creation | Creates VNode, instantiated later |
| Direct mutation via methods | Chained calls recorded, replayed on instantiation |
| Full control | Cleaner composition |
### Storage Options
Renderables can be composed in two ways:
1. **Imperative**: Create instances, call `.add()` to compose
2. **Declarative (Constructs)**: Create VNodes, pass children as arguments
## Essential Commands
```bash
bun install @opentui/core # Install
bun run src/index.ts # Run directly (no build needed)
bun test # Run tests
```
## Runtime Requirements
OpenTUI runs on Bun and uses Zig for native builds.
```bash
# Package management
bun install @opentui/core
# Running
bun run src/index.ts
bun test
# Building (only needed for native code changes)
bun run build
```
**Zig** is required for building native components.
## In This Reference
- [Configuration](./configuration.md) - Renderer options, environment variables
- [API](./api.md) - Renderer, Renderables, types, utilities
- [Patterns](./patterns.md) - Composition, events, state management
- [Gotchas](./gotchas.md) - Common issues, debugging, limitations
## See Also
- [React](../react/REFERENCE.md) - React reconciler for declarative TUI
- [Solid](../solid/REFERENCE.md) - Solid reconciler for declarative TUI
- [Layout](../layout/REFERENCE.md) - Yoga/Flexbox layout system
- [Components](../components/REFERENCE.md) - Component reference by category
- [Keyboard](../keyboard/REFERENCE.md) - Input handling and shortcuts
- [Testing](../testing/REFERENCE.md) - Test renderer and snapshots
@@ -1,543 +0,0 @@
# Core API Reference
## Renderer
### createCliRenderer(config?)
Creates and initializes the CLI renderer.
```typescript
import { createCliRenderer, type CliRendererConfig } from "@opentui/core"
const renderer = await createCliRenderer({
targetFPS: 60, // Target frames per second
exitOnCtrlC: true, // Exit process on Ctrl+C
consoleOptions: { // Debug console overlay
position: ConsolePosition.BOTTOM,
sizePercent: 30,
startInDebugMode: false,
},
onDestroy: () => {}, // Cleanup callback
})
```
### CliRenderer Instance
```typescript
renderer.root // Root renderable node
renderer.width // Terminal width in columns
renderer.height // Terminal height in rows
renderer.keyInput // Keyboard event emitter
renderer.console // Console overlay controller
renderer.start() // Start render loop
renderer.stop() // Stop render loop
renderer.destroy() // Cleanup and exit alternate screen
renderer.requestRender() // Request a re-render
renderer.setCursorStyle(options) // Set cursor style
renderer.setCursorColor(color) // Set cursor color
renderer.setMousePointer(style) // Set mouse pointer shape
```
### Cursor & Mouse Pointer
```typescript
import { type CursorStyleOptions, type MousePointerStyle } from "@opentui/core"
// Set cursor style (options object)
renderer.setCursorStyle({
style: "block", // "block" | "line" | "underline" | "default"
blinking: true, // Cursor blink
color: RGBA.fromHex("#FF0000"), // Cursor color
cursor: "pointer", // Mouse pointer shape
})
// Set mouse pointer shape (OSC 22)
renderer.setMousePointer("pointer")
// Available: "default" | "pointer" | "text" | "crosshair" | "move" | "not-allowed"
```
### Renderer Events
```typescript
renderer.on("resize", (width, height) => {}) // Terminal resized
renderer.on("focus", () => {}) // Terminal window gained focus
renderer.on("blur", () => {}) // Terminal window lost focus
renderer.on("theme_mode", (mode) => {}) // "dark" | "light"
renderer.on("capabilities", (caps) => {}) // Terminal capabilities detected
renderer.on("selection", (selection) => {}) // Text selection finished (mouse-up)
renderer.on("destroy", () => {}) // Renderer destroyed
renderer.on("memory:snapshot", (snapshot) => {}) // Memory snapshot
renderer.on("debugOverlay:toggle", () => {}) // Debug overlay toggled
```
### Console Overlay
```typescript
renderer.console.show() // Show console overlay
renderer.console.hide() // Hide console overlay
renderer.console.toggle() // Toggle visibility/focus
renderer.console.clear() // Clear console contents
```
## Renderables
All renderables extend the base `Renderable` class and share common properties.
### Common Properties
```typescript
interface CommonProps {
id?: string // Unique identifier
// Positioning
position?: "relative" | "absolute"
left?: number | string
top?: number | string
right?: number | string
bottom?: number | string
// Dimensions
width?: number | string | "auto"
height?: number | string | "auto"
minWidth?: number
minHeight?: number
maxWidth?: number
maxHeight?: number
// Flexbox
flexDirection?: "row" | "column" | "row-reverse" | "column-reverse"
flexGrow?: number
flexShrink?: number
flexBasis?: number | string
flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly"
alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
alignContent?: "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-around"
// Spacing
padding?: number
paddingTop?: number
paddingRight?: number
paddingBottom?: number
paddingLeft?: number
margin?: number
marginTop?: number
marginRight?: number
marginBottom?: number
marginLeft?: number
gap?: number
// Display
display?: "flex" | "none"
overflow?: "visible" | "hidden" | "scroll"
zIndex?: number
}
```
### Renderable Methods
```typescript
renderable.add(child) // Add child renderable
renderable.remove(child) // Remove child renderable
renderable.getRenderable(id) // Find child by ID
renderable.focus() // Focus this renderable
renderable.blur() // Remove focus
renderable.destroy() // Destroy and cleanup
renderable.on(event, handler) // Add event listener
renderable.off(event, handler) // Remove event listener
renderable.emit(event, ...args) // Emit event
```
### TextRenderable
Display styled text content.
```typescript
import { TextRenderable, TextAttributes, t, bold, fg, underline } from "@opentui/core"
const text = new TextRenderable(renderer, {
id: "text",
content: "Hello World",
fg: "#FFFFFF", // Foreground color
bg: "#000000", // Background color
attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
selectable: true, // Allow text selection
})
// Styled text with template literals
const styled = new TextRenderable(renderer, {
content: t`${bold("Bold")} and ${fg("#FF0000")(underline("red underlined"))}`,
})
```
**TextAttributes flags:**
- `TextAttributes.BOLD`
- `TextAttributes.DIM`
- `TextAttributes.ITALIC`
- `TextAttributes.UNDERLINE`
- `TextAttributes.BLINK`
- `TextAttributes.INVERSE`
- `TextAttributes.HIDDEN`
- `TextAttributes.STRIKETHROUGH`
### BoxRenderable
Container with borders and layout.
```typescript
import { BoxRenderable } from "@opentui/core"
const box = new BoxRenderable(renderer, {
id: "box",
width: 40,
height: 10,
backgroundColor: "#1a1a2e",
border: true,
borderStyle: "single" | "double" | "rounded" | "bold" | "none",
borderColor: "#FFFFFF",
title: "Panel Title",
titleAlignment: "left" | "center" | "right",
onMouseDown: (event) => {},
onMouseUp: (event) => {},
onMouseMove: (event) => {},
})
```
### InputRenderable
Single-line text input.
```typescript
import { InputRenderable, InputRenderableEvents } from "@opentui/core"
const input = new InputRenderable(renderer, {
id: "input",
width: 30,
placeholder: "Enter text...",
value: "", // Initial value
backgroundColor: "#1a1a1a",
textColor: "#FFFFFF",
cursorColor: "#00FF00",
focusedBackgroundColor: "#2a2a2a",
})
input.on(InputRenderableEvents.CHANGE, (value: string) => {
console.log("Value:", value)
})
input.focus() // Must be focused to receive input
```
### SelectRenderable
List selection component.
```typescript
import { SelectRenderable, SelectRenderableEvents } from "@opentui/core"
const select = new SelectRenderable(renderer, {
id: "select",
width: 30,
height: 10,
options: [
{ name: "Option 1", description: "First option", value: "1" },
{ name: "Option 2", description: "Second option", value: "2" },
],
selectedIndex: 0,
})
// Called when Enter is pressed - selection confirmed
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Selected:", option.name)
performAction(option)
})
// Called when navigating with arrow keys
select.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {
console.log("Browsing:", option.name)
showPreview(option)
})
select.focus() // Navigate with up/down/j/k, select with enter
```
**Event distinction:**
- `ITEM_SELECTED` - Enter key pressed, user confirms selection
- `SELECTION_CHANGED` - Arrow keys, user navigating/browsing options
### TabSelectRenderable
Horizontal tab selection.
```typescript
import { TabSelectRenderable, TabSelectRenderableEvents } from "@opentui/core"
const tabs = new TabSelectRenderable(renderer, {
id: "tabs",
width: 60,
options: [
{ name: "Home", description: "Dashboard" },
{ name: "Settings", description: "Configuration" },
],
tabWidth: 20,
})
// Called when Enter is pressed - tab selected
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Tab selected:", option.name)
switchToTab(index)
})
// Called when navigating with arrow keys
tabs.on(TabSelectRenderableEvents.SELECTION_CHANGED, (index, option) => {
console.log("Browsing tab:", option.name)
})
tabs.focus() // Navigate with left/right/[/], select with enter
```
**Event distinction** (same as SelectRenderable):
- `ITEM_SELECTED` - Enter key pressed, user confirms tab
- `SELECTION_CHANGED` - Arrow keys, user navigating tabs
### ScrollBoxRenderable
Scrollable container.
```typescript
import { ScrollBoxRenderable } from "@opentui/core"
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "scrollbox",
width: 40,
height: 20,
showScrollbar: true,
scrollbarOptions: {
showArrows: true,
trackOptions: {
foregroundColor: "#7aa2f7",
backgroundColor: "#414868",
},
},
})
// Add content that exceeds viewport
for (let i = 0; i < 100; i++) {
scrollbox.add(new TextRenderable(renderer, {
id: `line-${i}`,
content: `Line ${i}`,
}))
}
scrollbox.focus() // Scroll with arrow keys
```
### ASCIIFontRenderable
ASCII art text.
```typescript
import { ASCIIFontRenderable, RGBA } from "@opentui/core"
const title = new ASCIIFontRenderable(renderer, {
id: "title",
text: "OPENTUI",
font: "tiny" | "block" | "slick" | "shade",
color: RGBA.fromHex("#FFFFFF"),
})
```
### FrameBufferRenderable
Low-level 2D rendering surface.
```typescript
import { FrameBufferRenderable, RGBA } from "@opentui/core"
const canvas = new FrameBufferRenderable(renderer, {
id: "canvas",
width: 50,
height: 20,
})
// Direct pixel manipulation
canvas.frameBuffer.fillRect(10, 5, 20, 8, RGBA.fromHex("#FF0000"))
canvas.frameBuffer.drawText("Custom", 12, 7, RGBA.fromHex("#FFFFFF"))
canvas.frameBuffer.setCell(x, y, char, fg, bg)
```
## Constructs (VNode API)
Declarative wrappers that create VNodes instead of direct instances.
```typescript
import { Text, Box, Input, Select, instantiate, delegate } from "@opentui/core"
// Create VNode tree
const ui = Box(
{ border: true, padding: 1 },
Text({ content: "Hello" }),
Input({ placeholder: "Type here..." }),
)
// Instantiate onto renderer
renderer.root.add(ui)
// Delegate focus to nested element
const form = delegate(
{ focus: "email-input" },
Box(
{},
Text({ content: "Email:" }),
Input({ id: "email-input", placeholder: "you@example.com" }),
),
)
form.focus() // Focuses the input, not the box
```
## Colors (RGBA)
The `RGBA` class is exported from `@opentui/core` but works across **all frameworks** (Core, React, Solid). Use it for programmatic color manipulation.
### Creating Colors
```typescript
import { RGBA, parseColor } from "@opentui/core"
// From hex string (most common)
RGBA.fromHex("#FF0000") // Full hex
RGBA.fromHex("#F00") // Short hex
// From integers (0-255 range)
RGBA.fromInts(255, 0, 0, 255) // r, g, b, a - fully opaque red
RGBA.fromInts(255, 0, 0, 128) // 50% transparent red
RGBA.fromInts(0, 0, 0, 0) // Fully transparent
// From normalized floats (0.0-1.0 range)
RGBA.fromValues(1.0, 0.0, 0.0, 1.0) // Fully opaque red
RGBA.fromValues(0.1, 0.1, 0.1, 0.7) // Dark gray, 70% opaque
RGBA.fromValues(0.0, 0.5, 1.0, 1.0) // Light blue
```
### Common Color Patterns
```typescript
// Theme colors
const primary = RGBA.fromHex("#7aa2f7") // Tokyo Night blue
const background = RGBA.fromHex("#1a1a2e")
const foreground = RGBA.fromHex("#c0caf5")
const error = RGBA.fromHex("#f7768e")
// Overlays and shadows
const modalOverlay = RGBA.fromValues(0.0, 0.0, 0.0, 0.5) // 50% black
const shadow = RGBA.fromInts(0, 0, 0, 77) // 30% black
// Borders
const activeBorder = RGBA.fromHex("#7aa2f7")
const inactiveBorder = RGBA.fromInts(65, 72, 104, 255)
```
### parseColor Utility
```typescript
// Accepts multiple formats
parseColor("#FF0000") // Hex string
parseColor("red") // CSS color name
parseColor("transparent") // Special values
parseColor(RGBA.fromHex("#F00")) // Pass-through RGBA objects
```
### When to Use Each Method
| Method | Use When |
|--------|----------|
| `fromHex()` | Working with design specs, CSS colors, config files |
| `fromInts()` | You have 8-bit values (0-255), common in graphics |
| `fromValues()` | Doing color interpolation, animations, math |
| `parseColor()` | Accepting user input or config that could be any format |
### Using RGBA in React/Solid
```tsx
// Import from @opentui/core, use in any framework
import { RGBA } from "@opentui/core"
// React or Solid component
function ThemedBox() {
const bg = RGBA.fromHex("#1a1a2e")
const border = RGBA.fromInts(122, 162, 247, 255)
return (
<box backgroundColor={bg} borderColor={border} border>
<text fg={RGBA.fromHex("#c0caf5")}>Works everywhere!</text>
</box>
)
}
```
Color props in React/Solid accept both string formats (`"#FF0000"`, `"red"`) and `RGBA` objects.
## Keyboard Input
```typescript
import { type KeyEvent } from "@opentui/core"
renderer.keyInput.on("keypress", (key: KeyEvent) => {
console.log(key.name) // "a", "escape", "f1", etc.
console.log(key.sequence) // Raw escape sequence
console.log(key.ctrl) // Ctrl held
console.log(key.shift) // Shift held
console.log(key.meta) // Alt held
console.log(key.option) // Option held (macOS)
console.log(key.eventType) // "press" | "release" | "repeat"
})
renderer.keyInput.on("paste", (event: PasteEvent) => {
const text = decodePasteBytes(event.bytes)
console.log("Pasted:", text)
})
```
## Animation Timeline
```typescript
import { Timeline, engine } from "@opentui/core"
const timeline = new Timeline({
duration: 2000,
loop: false,
autoplay: true,
})
timeline.add(
{ width: 0 },
{
width: 50,
duration: 1000,
ease: "easeOutQuad",
onUpdate: (anim) => {
box.setWidth(anim.targets[0].width)
},
},
)
engine.attach(renderer)
engine.addTimeline(timeline)
```
## Type Exports
```typescript
import type {
CliRenderer,
CliRendererConfig,
RenderContext,
KeyEvent,
Renderable,
// ... and more
} from "@opentui/core"
```
@@ -1,168 +0,0 @@
# Core Configuration
## Renderer Configuration
### createCliRenderer Options
```typescript
import { createCliRenderer, ConsolePosition } from "@opentui/core"
const renderer = await createCliRenderer({
// Rendering
targetFPS: 60, // Target frames per second (default: 60)
// Behavior
exitOnCtrlC: true, // Exit on Ctrl+C (default: true)
// Console overlay
consoleOptions: {
position: ConsolePosition.BOTTOM, // BOTTOM | TOP | LEFT | RIGHT
sizePercent: 30, // Percentage of screen
colorInfo: "#00FFFF",
colorWarn: "#FFFF00",
colorError: "#FF0000",
colorDebug: "#888888",
startInDebugMode: false,
},
// Lifecycle
onDestroy: () => {
// Cleanup callback
},
})
```
## Environment Variables
OpenTUI respects several environment variables for configuration and debugging.
### Debug & Development
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_DEBUG` | boolean | false | Enable debug mode, capture raw input |
| `OTUI_DEBUG_FFI` | boolean | false | Debug logging for FFI bindings |
| `OTUI_TRACE_FFI` | boolean | false | Tracing for FFI bindings |
| `OTUI_SHOW_STATS` | boolean | false | Show debug overlay at startup |
| `OTUI_DUMP_CAPTURES` | boolean | false | Dump captured output on exit |
### Console
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_USE_CONSOLE` | boolean | true | Enable console capture |
| `SHOW_CONSOLE` | boolean | false | Show console at startup |
### Rendering
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_NO_NATIVE_RENDER` | boolean | false | Disable ANSI output (for debugging) |
| `OTUI_USE_ALTERNATE_SCREEN` | boolean | true | Use alternate screen buffer |
| `OTUI_OVERRIDE_STDOUT` | boolean | true | Override stdout stream |
### Terminal Capabilities
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OPENTUI_NO_GRAPHICS` | boolean | false | Disable Kitty graphics protocol |
| `OPENTUI_FORCE_UNICODE` | boolean | false | Force Mode 2026 Unicode support |
| `OPENTUI_FORCE_WCWIDTH` | boolean | false | Use wcwidth for character width |
| `OPENTUI_FORCE_NOZWJ` | boolean | false | Disable ZWJ emoji joining |
| `OPENTUI_FORCE_EXPLICIT_WIDTH` | string | - | Force explicit width ("true"/"false") |
### Tree-sitter (Syntax Highlighting)
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `OTUI_TS_STYLE_WARN` | boolean | false | Warn on missing syntax styles |
| `OTUI_TREE_SITTER_WORKER_PATH` | string | "" | Custom tree-sitter worker path |
### XDG Paths
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `XDG_CONFIG_HOME` | string | "" | User config directory |
| `XDG_DATA_HOME` | string | "" | User data directory |
## Usage Examples
### Development Mode
```bash
# Show debug overlay and console
OTUI_SHOW_STATS=true SHOW_CONSOLE=true bun run src/index.ts
# Debug FFI issues
OTUI_DEBUG_FFI=true OTUI_TRACE_FFI=true bun run src/index.ts
# Disable native rendering for testing
OTUI_NO_NATIVE_RENDER=true bun run src/index.ts
```
### Terminal Compatibility
```bash
# Force wcwidth for problematic terminals
OPENTUI_FORCE_WCWIDTH=true bun run src/index.ts
# Disable graphics for SSH sessions
OPENTUI_NO_GRAPHICS=true bun run src/index.ts
```
## Project Setup
### package.json
```json
{
"name": "my-tui-app",
"type": "module",
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun --watch run src/index.ts",
"test": "bun test"
},
"dependencies": {
"@opentui/core": "latest"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "latest"
}
}
```
### tsconfig.json
```json
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["bun-types"]
},
"include": ["src/**/*"]
}
```
> **Note**: OpenTUI uses `NodeNext` module resolution. All internal imports use `.js` extensions. If you use `bundler` resolution, imports still work but `NodeNext` is recommended for compatibility.
## Building Native Code
Native code changes require rebuilding:
```bash
# From repo root (if developing OpenTUI itself)
bun run build
# Zig is required for native compilation
# Install: https://ziglang.org/learn/getting-started/
```
**Note**: TypeScript changes do NOT require building. Bun runs TypeScript directly.
@@ -1,393 +0,0 @@
# Core Gotchas
## Runtime Environment
### Use Bun, Not Node.js
OpenTUI is built for Bun. Always use Bun commands:
```bash
# CORRECT
bun install @opentui/core
bun run src/index.ts
bun test
# WRONG
npm install @opentui/core
node src/index.ts
npx jest
```
### Bun APIs to Use
Prefer Bun's built-in APIs for your application code:
```typescript
// CORRECT - Bun APIs
Bun.serve({ ... }) // Instead of express
Bun.$`ls -la` // Instead of execa
import { Database } from "bun:sqlite" // Instead of better-sqlite3
// WRONG - Node.js patterns
import express from "express"
```
> **Note**: OpenTUI itself uses `node:fs` internally for file I/O (for broader compatibility), but your application code should still prefer Bun APIs where available.
### Avoid process.exit()
**Never use `process.exit()` directly** - it prevents proper terminal cleanup and can leave the terminal in a broken state (alternate screen mode, raw input mode, etc.).
```typescript
// WRONG - Terminal may be left in broken state
if (error) {
console.error("Fatal error")
process.exit(1)
}
// CORRECT - Use renderer.destroy() for cleanup
if (error) {
console.error("Fatal error")
await renderer.destroy()
process.exit(1) // Only after destroy
}
// BETTER - Let destroy handle exit
const renderer = await createCliRenderer({
exitOnCtrlC: true, // Handles Ctrl+C properly
})
// For programmatic exit
renderer.destroy() // Cleans up and exits
```
`renderer.destroy()` restores the terminal to its original state before exiting.
### Environment Variables
Bun auto-loads `.env` files. Don't use dotenv:
```typescript
// CORRECT
const apiKey = process.env.API_KEY
// WRONG
import dotenv from "dotenv"
dotenv.config()
```
## Debugging TUIs
### Cannot See console.log Output
OpenTUI captures console output for the debug overlay. You can't see logs in the terminal while the TUI is running.
**Solutions:**
1. **Use the console overlay:**
```typescript
const renderer = await createCliRenderer()
renderer.console.show()
console.log("This appears in the overlay")
```
2. **Toggle with keyboard:**
```typescript
renderer.keyInput.on("keypress", (key) => {
if (key.name === "f12") {
renderer.console.toggle()
}
})
```
3. **Write to a file:**
```typescript
import { appendFileSync } from "node:fs"
function debugLog(msg: string) {
appendFileSync("debug.log", `${new Date().toISOString()} ${msg}\n`)
}
```
4. **Disable console capture:**
```bash
OTUI_USE_CONSOLE=false bun run src/index.ts
```
### Reproduce Issues in Tests
Don't guess at bugs. Create a reproducible test:
```typescript
import { test, expect } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
test("reproduces the issue", async () => {
const { renderer, snapshot } = await createTestRenderer({
width: 40,
height: 10,
})
// Setup that reproduces the bug
const box = new BoxRenderable(renderer, { ... })
renderer.root.add(box)
// Verify with snapshot
expect(snapshot()).toMatchSnapshot()
})
```
## Focus Management
### Components Must Be Focused
Input components only receive keyboard input when focused:
```typescript
const input = new InputRenderable(renderer, {
id: "input",
placeholder: "Type here...",
})
renderer.root.add(input)
// WRONG - input won't receive keystrokes
// (no focus call)
// CORRECT
input.focus()
```
### Focus in Nested Components
When a component is inside a container, focus the component directly:
```typescript
const container = new BoxRenderable(renderer, { id: "container" })
const input = new InputRenderable(renderer, { id: "input" })
container.add(input)
renderer.root.add(container)
// WRONG
container.focus()
// CORRECT
input.focus()
// Or use getRenderable
container.getRenderable("input")?.focus()
// Or use delegate (constructs)
const form = delegate(
{ focus: "input" },
Box({}, Input({ id: "input" })),
)
form.focus() // Routes to the input
```
## Build Requirements
### Zig is Required
Native code compilation requires Zig:
```bash
# Install Zig first
# macOS
brew install zig
# Linux
# Download from https://ziglang.org/download/
# Then build
bun run build
```
### When to Build
- **TypeScript changes**: NO build needed (Bun runs TS directly)
- **Native code changes**: Build required
```bash
# Only needed when changing native (Zig) code
cd packages/core
bun run build
```
## Common Errors
### "Cannot read properties of undefined"
Usually means a renderable wasn't added to the tree:
```typescript
// WRONG - not added to tree
const text = new TextRenderable(renderer, { content: "Hello" })
// text.someMethod() // May fail
// CORRECT
const text = new TextRenderable(renderer, { content: "Hello" })
renderer.root.add(text)
text.someMethod()
```
### Layout Not Updating
Yoga layout is calculated lazily. Force a recalculation:
```typescript
// After changing layout properties
box.setWidth(newWidth)
renderer.requestRender()
```
### Text Overflow/Clipping
Text doesn't wrap by default. Set explicit width:
```typescript
// May overflow
const text = new TextRenderable(renderer, {
content: "Very long text that might overflow the terminal...",
})
// Contained within width
const text = new TextRenderable(renderer, {
content: "Very long text that might overflow the terminal...",
width: 40, // Will clip or wrap based on parent
})
```
### Colors Not Showing
Check terminal capability and color format:
```typescript
// CORRECT formats
fg: "#FF0000" // Hex
fg: "red" // CSS color name
fg: RGBA.fromHex("#FF0000")
// WRONG
fg: "FF0000" // Missing #
fg: 0xFF0000 // Number (not supported)
```
## Performance
### Avoid Frequent Re-renders
Batch updates when possible:
```typescript
// WRONG - multiple render calls
item1.setContent("...")
item2.setContent("...")
item3.setContent("...")
// BETTER - single render after all updates
// (OpenTUI batches automatically, but be mindful)
items.forEach((item, i) => {
item.setContent(data[i])
})
```
### Minimize Tree Depth
Deep nesting impacts layout calculation:
```typescript
// Avoid unnecessary wrappers
// WRONG
Box({}, Box({}, Box({}, Text({ content: "Hello" }))))
// CORRECT
Box({}, Text({ content: "Hello" }))
```
### Use display: none
Hide elements instead of removing/re-adding:
```typescript
// For toggling visibility
element.setDisplay("none") // Hidden
element.setDisplay("flex") // Visible
// Instead of
parent.remove(element)
parent.add(element)
```
## Testing
### Test Runner
Use Bun's test runner:
```typescript
import { test, expect, beforeEach, afterEach } from "bun:test"
test("my test", () => {
expect(1 + 1).toBe(2)
})
```
### Test from Package Directories
Run tests from the specific package directory:
```bash
# CORRECT
cd packages/core
bun test
# For native tests
cd packages/core
bun run test:native
```
### Filter Tests
```bash
# Bun test filter
bun test --filter "component name"
# Native test filter
bun run test:native -Dtest-filter="test name"
```
## Keyboard Handling
### Key Names
Common key names for `KeyEvent.name`:
```typescript
// Letters/numbers
"a", "b", ..., "z"
"1", "2", ..., "0"
// Special keys
"escape", "enter", "return", "tab", "backspace", "delete"
"up", "down", "left", "right"
"home", "end", "pageup", "pagedown"
"f1", "f2", ..., "f12"
"space"
// Modifiers (check boolean properties)
key.ctrl // Ctrl held
key.shift // Shift held
key.meta // Alt held
key.option // Option held (macOS)
```
### Key Event Types
```typescript
renderer.keyInput.on("keypress", (key) => {
// eventType: "press" | "release" | "repeat"
if (key.eventType === "repeat") {
// Key being held down
}
})
```
@@ -1,449 +0,0 @@
# Core Patterns
## Composition Patterns
### Imperative Composition
Create renderables and compose with `.add()`:
```typescript
import { createCliRenderer, BoxRenderable, TextRenderable } from "@opentui/core"
const renderer = await createCliRenderer()
// Create parent
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
padding: 1,
})
// Create children
const header = new TextRenderable(renderer, {
id: "header",
content: "Header",
fg: "#00FF00",
})
const body = new TextRenderable(renderer, {
id: "body",
content: "Body content",
})
// Compose tree
container.add(header)
container.add(body)
renderer.root.add(container)
```
### Declarative Composition (Constructs)
Use VNode functions for cleaner composition:
```typescript
import { createCliRenderer, Box, Text, Input, delegate } from "@opentui/core"
const renderer = await createCliRenderer()
// Compose as function calls
const ui = Box(
{ flexDirection: "column", padding: 1 },
Text({ content: "Header", fg: "#00FF00" }),
Box(
{ flexDirection: "row", gap: 2 },
Text({ content: "Name:" }),
Input({ id: "name", placeholder: "Enter name..." }),
),
)
renderer.root.add(ui)
```
### Reusable Components
Create factory functions for reusable UI pieces:
```typescript
// Imperative factory
function createLabeledInput(
renderer: RenderContext,
props: { id: string; label: string; placeholder: string }
) {
const container = new BoxRenderable(renderer, {
id: `${props.id}-container`,
flexDirection: "row",
gap: 1,
})
container.add(new TextRenderable(renderer, {
id: `${props.id}-label`,
content: props.label,
}))
container.add(new InputRenderable(renderer, {
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}))
return container
}
// Declarative factory
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
return delegate(
{ focus: `${props.id}-input` },
Box(
{ flexDirection: "row", gap: 1 },
Text({ content: props.label }),
Input({
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}),
),
)
}
```
### Focus Delegation
Route focus calls to nested elements:
```typescript
import { delegate, Box, Input, Text } from "@opentui/core"
const form = delegate(
{
focus: "email-input", // Route .focus() to this child
blur: "email-input", // Route .blur() to this child
},
Box(
{ border: true, padding: 1 },
Text({ content: "Email:" }),
Input({ id: "email-input", placeholder: "you@example.com" }),
),
)
// This focuses the input inside, not the box
form.focus()
```
## Event Handling
### Keyboard Events
```typescript
const renderer = await createCliRenderer()
// Global keyboard handler
renderer.keyInput.on("keypress", (key) => {
if (key.name === "escape") {
renderer.destroy()
process.exit(0)
}
if (key.ctrl && key.name === "c") {
// Ctrl+C handling (if exitOnCtrlC is false)
}
if (key.name === "tab") {
// Tab navigation
focusNext()
}
})
// Paste events
renderer.keyInput.on("paste", (event) => {
const text = decodePasteBytes(event.bytes)
currentInput?.setValue(currentInput.value + text)
})
```
### Component Events
```typescript
import { InputRenderable, InputRenderableEvents } from "@opentui/core"
const input = new InputRenderable(renderer, {
id: "search",
placeholder: "Search...",
})
input.on(InputRenderableEvents.CHANGE, (value) => {
performSearch(value)
})
// Select events
const select = new SelectRenderable(renderer, {
id: "menu",
options: [...],
})
select.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
handleSelection(option)
})
select.on(SelectRenderableEvents.SELECTION_CHANGED, (index, option) => {
showPreview(option)
})
```
### Mouse Events
```typescript
const button = new BoxRenderable(renderer, {
id: "button",
border: true,
onMouseDown: (event) => {
button.setBackgroundColor("#444444")
},
onMouseUp: (event) => {
button.setBackgroundColor("#222222")
handleClick()
},
onMouseMove: (event) => {
// Hover effect
},
})
```
## State Management
### Local State
Manage state in closures or objects:
```typescript
// Closure-based state
function createCounter(renderer: RenderContext) {
let count = 0
const display = new TextRenderable(renderer, {
id: "count",
content: `Count: ${count}`,
})
const increment = () => {
count++
display.setContent(`Count: ${count}`)
}
return { display, increment }
}
// Class-based state
class CounterWidget {
private count = 0
private display: TextRenderable
constructor(renderer: RenderContext) {
this.display = new TextRenderable(renderer, {
id: "count",
content: this.formatCount(),
})
}
private formatCount() {
return `Count: ${this.count}`
}
increment() {
this.count++
this.display.setContent(this.formatCount())
}
getRenderable() {
return this.display
}
}
```
### Focus Management
Track and manage focus across components:
```typescript
class FocusManager {
private focusables: Renderable[] = []
private currentIndex = 0
register(renderable: Renderable) {
this.focusables.push(renderable)
}
focusNext() {
this.focusables[this.currentIndex]?.blur()
this.currentIndex = (this.currentIndex + 1) % this.focusables.length
this.focusables[this.currentIndex]?.focus()
}
focusPrevious() {
this.focusables[this.currentIndex]?.blur()
this.currentIndex = (this.currentIndex - 1 + this.focusables.length) % this.focusables.length
this.focusables[this.currentIndex]?.focus()
}
}
// Usage
const focusManager = new FocusManager()
focusManager.register(input1)
focusManager.register(input2)
focusManager.register(select1)
renderer.keyInput.on("keypress", (key) => {
if (key.name === "tab") {
key.shift ? focusManager.focusPrevious() : focusManager.focusNext()
}
})
```
## Lifecycle Patterns
### Cleanup
Always clean up resources:
```typescript
const renderer = await createCliRenderer()
// Track intervals/timeouts
const intervals: Timer[] = []
intervals.push(setInterval(() => {
updateClock()
}, 1000))
// Cleanup on exit
process.on("SIGINT", () => {
intervals.forEach(clearInterval)
renderer.destroy()
process.exit(0)
})
// Or use onDestroy callback
const renderer = await createCliRenderer({
onDestroy: () => {
intervals.forEach(clearInterval)
},
})
```
### Dynamic Updates
Update UI based on external data:
```typescript
async function createDashboard(renderer: RenderContext) {
const statsText = new TextRenderable(renderer, {
id: "stats",
content: "Loading...",
})
// Poll for updates
const updateStats = async () => {
const data = await fetchStats()
statsText.setContent(`CPU: ${data.cpu}% | Memory: ${data.memory}%`)
}
// Initial load
await updateStats()
// Periodic updates
setInterval(updateStats, 5000)
return statsText
}
```
## Layout Patterns
### Responsive Layout
Adapt to terminal size:
```typescript
const renderer = await createCliRenderer()
const mainPanel = new BoxRenderable(renderer, {
id: "main",
width: "100%",
height: "100%",
flexDirection: renderer.width > 80 ? "row" : "column",
})
// Listen for resize
process.stdout.on("resize", () => {
mainPanel.setFlexDirection(renderer.width > 80 ? "row" : "column")
})
```
### Split Panels
```typescript
function createSplitView(renderer: RenderContext, ratio = 0.3) {
const container = new BoxRenderable(renderer, {
id: "split",
flexDirection: "row",
width: "100%",
height: "100%",
})
const left = new BoxRenderable(renderer, {
id: "left",
width: `${ratio * 100}%`,
border: true,
})
const right = new BoxRenderable(renderer, {
id: "right",
flexGrow: 1,
border: true,
})
container.add(left)
container.add(right)
return { container, left, right }
}
```
## Debugging Patterns
### Console Overlay
Use the built-in console for debugging:
```typescript
const renderer = await createCliRenderer({
consoleOptions: {
startInDebugMode: true,
},
})
// Show console
renderer.console.show()
// All console methods work
console.log("Debug info")
console.warn("Warning")
console.error("Error")
// Toggle with keyboard
renderer.keyInput.on("keypress", (key) => {
if (key.name === "f12") {
renderer.console.toggle()
}
})
```
### State Inspection
```typescript
function debugState(label: string, state: unknown) {
console.log(`[${label}]`, JSON.stringify(state, null, 2))
}
// In your update logic
debugState("form", { name: nameInput.value, email: emailInput.value })
```
@@ -1,617 +0,0 @@
# Keyboard Input Handling
How to handle keyboard input in OpenTUI applications.
## Overview
OpenTUI provides keyboard input handling through:
- **Core**: `renderer.keyInput` EventEmitter
- **React**: `useKeyboard()` hook
- **Solid**: `useKeyboard()` hook
## When to Use
Use this reference when you need keyboard shortcuts, focus-aware input handling, or custom keybindings.
## KeyEvent Object
All keyboard handlers receive a `KeyEvent` object:
```typescript
interface KeyEvent {
name: string // Key name: "a", "escape", "f1", etc.
sequence: string // Raw escape sequence
ctrl: boolean // Ctrl modifier held
shift: boolean // Shift modifier held
meta: boolean // Alt modifier held
option: boolean // Option modifier held (macOS)
eventType: "press" | "release" | "repeat"
repeated: boolean // Key is being held (repeat event)
}
```
## Basic Usage
### Core
```typescript
import { createCliRenderer, type KeyEvent } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.keyInput.on("keypress", (key: KeyEvent) => {
if (key.name === "escape") {
renderer.destroy()
return
}
if (key.ctrl && key.name === "s") {
saveDocument()
}
})
```
### React
```tsx
import { useKeyboard, useRenderer } from "@opentui/react"
function App() {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "escape") {
renderer.destroy()
}
})
return <text>Press ESC to exit</text>
}
```
### Solid
```tsx
import { useKeyboard, useRenderer } from "@opentui/solid"
function App() {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "escape") {
renderer.destroy()
}
})
return <text>Press ESC to exit</text>
}
```
## Key Names
### Alphabetic Keys
Lowercase: `a`, `b`, `c`, ... `z`
With Shift: Check `key.shift && key.name === "a"` for uppercase
### Numeric Keys
`0`, `1`, `2`, ... `9`
### Function Keys
`f1`, `f2`, `f3`, ... `f12`
### Special Keys
| Key Name | Description |
|----------|-------------|
| `escape` | Escape key |
| `enter` | Enter/Return |
| `return` | Enter/Return (alias) |
| `tab` | Tab key |
| `backspace` | Backspace |
| `delete` | Delete key |
| `space` | Spacebar |
### Arrow Keys
| Key Name | Description |
|----------|-------------|
| `up` | Up arrow |
| `down` | Down arrow |
| `left` | Left arrow |
| `right` | Right arrow |
### Navigation Keys
| Key Name | Description |
|----------|-------------|
| `home` | Home key |
| `end` | End key |
| `pageup` | Page Up |
| `pagedown` | Page Down |
| `insert` | Insert key |
## Modifier Keys
Check modifier properties on `KeyEvent`:
```typescript
renderer.keyInput.on("keypress", (key) => {
if (key.ctrl && key.name === "c") {
// Ctrl+C
}
if (key.shift && key.name === "tab") {
// Shift+Tab
}
if (key.meta && key.name === "s") {
// Alt+S (meta = Alt on most systems)
}
if (key.option && key.name === "a") {
// Option+A (macOS)
}
})
```
### Modifier Combinations
```typescript
// Ctrl+Shift+S
if (key.ctrl && key.shift && key.name === "s") {
saveAs()
}
// Ctrl+Alt+Delete (careful with system shortcuts!)
if (key.ctrl && key.meta && key.name === "delete") {
// ...
}
```
## Event Types
### Press Events (Default)
Normal key press:
```typescript
renderer.keyInput.on("keypress", (key) => {
if (key.eventType === "press") {
// Initial key press
}
})
```
### Repeat Events
Key held down:
```typescript
renderer.keyInput.on("keypress", (key) => {
if (key.eventType === "repeat" || key.repeated) {
// Key is being held
}
})
```
### Release Events
Key released (opt-in):
```tsx
// React
useKeyboard(
(key) => {
if (key.eventType === "release") {
// Key released
}
},
{ release: true } // Enable release events
)
// Solid
useKeyboard(
(key) => {
if (key.eventType === "release") {
// Key released
}
},
{ release: true }
)
```
## Patterns
### Navigation Menu
```tsx
function Menu() {
const [selectedIndex, setSelectedIndex] = useState(0)
const items = ["Home", "Settings", "Help", "Quit"]
useKeyboard((key) => {
switch (key.name) {
case "up":
case "k":
setSelectedIndex(i => Math.max(0, i - 1))
break
case "down":
case "j":
setSelectedIndex(i => Math.min(items.length - 1, i + 1))
break
case "enter":
handleSelect(items[selectedIndex])
break
}
})
return (
<box flexDirection="column">
{items.map((item, i) => (
<text
key={item}
fg={i === selectedIndex ? "#00FF00" : "#FFFFFF"}
>
{i === selectedIndex ? "> " : " "}{item}
</text>
))}
</box>
)
}
```
### Modal Escape
```tsx
function Modal({ onClose, children }) {
useKeyboard((key) => {
if (key.name === "escape") {
onClose()
}
})
return (
<box border padding={2}>
{children}
</box>
)
}
```
### Vim-style Modes
```tsx
function Editor() {
const [mode, setMode] = useState<"normal" | "insert">("normal")
const [content, setContent] = useState("")
useKeyboard((key) => {
if (mode === "normal") {
switch (key.name) {
case "i":
setMode("insert")
break
case "escape":
// Already in normal mode
break
case "j":
moveCursorDown()
break
case "k":
moveCursorUp()
break
}
} else if (mode === "insert") {
if (key.name === "escape") {
setMode("normal")
}
// Input component handles text in insert mode
}
})
return (
<box flexDirection="column">
<text>Mode: {mode}</text>
<textarea
value={content}
onChange={setContent}
focused={mode === "insert"}
/>
</box>
)
}
```
### Game Controls
```tsx
function Game() {
const [pressed, setPressed] = useState(new Set<string>())
useKeyboard(
(key) => {
setPressed(keys => {
const newKeys = new Set(keys)
if (key.eventType === "release") {
newKeys.delete(key.name)
} else {
newKeys.add(key.name)
}
return newKeys
})
},
{ release: true }
)
// Game logic uses pressed set
useEffect(() => {
if (pressed.has("up") || pressed.has("w")) {
moveUp()
}
if (pressed.has("down") || pressed.has("s")) {
moveDown()
}
}, [pressed])
return <text>WASD or arrows to move</text>
}
```
### Keyboard Shortcuts Help
```tsx
function ShortcutsHelp() {
const shortcuts = [
{ keys: "Ctrl+S", action: "Save" },
{ keys: "Ctrl+Q", action: "Quit" },
{ keys: "Ctrl+F", action: "Find" },
{ keys: "Tab", action: "Next field" },
{ keys: "Shift+Tab", action: "Previous field" },
]
return (
<box border title="Keyboard Shortcuts" padding={1}>
{shortcuts.map(({ keys, action }) => (
<box key={keys} flexDirection="row">
<text width={15} fg="#00FFFF">{keys}</text>
<text>{action}</text>
</box>
))}
</box>
)
}
```
## Paste Events
Handle pasted content. Paste events deliver raw bytes, not decoded text.
### PasteEvent Object
```typescript
import { type PasteEvent } from "@opentui/core"
interface PasteEvent {
type: "paste" // Always "paste"
bytes: Uint8Array // Raw pasted bytes
metadata?: PasteMetadata // Optional metadata
preventDefault(): void // Prevent default paste handling
defaultPrevented: boolean // Whether preventDefault was called
}
interface PasteMetadata {
mimeType?: string // MIME type if available
kind?: PasteKind // Paste kind
}
```
### Decoding Paste Bytes
Use `decodePasteBytes` to convert raw bytes to a string, and `stripAnsiSequences` to remove ANSI escape codes:
```typescript
import { decodePasteBytes, stripAnsiSequences } from "@opentui/core"
const text = decodePasteBytes(event.bytes) // Decode UTF-8
const clean = stripAnsiSequences(decodePasteBytes(event.bytes)) // Decode + strip ANSI
```
### Core
```typescript
import { type PasteEvent, decodePasteBytes } from "@opentui/core"
renderer.keyInput.on("paste", (event: PasteEvent) => {
const text = decodePasteBytes(event.bytes)
console.log("Pasted:", text)
})
```
### Solid
Solid provides a dedicated `usePaste` hook:
```tsx
import { usePaste } from "@opentui/solid"
import { decodePasteBytes } from "@opentui/core"
function App() {
usePaste((event) => {
const text = decodePasteBytes(event.bytes)
console.log("Pasted:", text)
})
return <text>Paste something</text>
}
```
> **Note**: `usePaste` is **Solid-only**. React does not have this hook - handle paste via the Core event emitter or input component's `onChange`.
## Text Selection
Text selection is renderer-managed. The renderer owns a single `Selection` object, walks the renderable tree to find selectable children, and emits a `"selection"` event when the user finishes selecting (mouse-up). The `Selection` object aggregates text from all selected renderables automatically.
### Making Renderables Selectable
A renderable must have `selectable` set to `true` to participate in selection. Text-based renderables (`TextRenderable`, `TextareaRenderable`, `ASCIIFontRenderable`, `TextTableRenderable`) support this:
```tsx
// React / Solid
<text selectable>This text can be selected</text>
// Core
const text = new TextRenderable(renderer, {
id: "label",
content: "This text can be selected",
selectable: true,
})
```
### Copy-on-Selection (Core)
Listen to the renderer's `"selection"` event. The `Selection` object's `getSelectedText()` returns text aggregated from all selected renderables in reading order:
```typescript
import type { Selection } from "@opentui/core"
renderer.on("selection", (selection: Selection) => {
const text = selection.getSelectedText()
if (text) {
renderer.copyToClipboardOSC52(text)
}
})
```
> **Important**: Call `selection.getSelectedText()` on the `Selection` object from the event -- not `renderer.root.getSelectedText()`. Individual renderables only return their own selected text. The `Selection` object aggregates across the tree.
### Copy-on-Selection (Solid)
```tsx
import { useSelectionHandler } from "@opentui/solid"
function App() {
useSelectionHandler((selection) => {
const text = selection.getSelectedText()
if (text) {
renderer.copyToClipboardOSC52(text)
}
})
return <text selectable>Select this text</text>
}
```
> **Note**: `useSelectionHandler` is **Solid-only**. React does not have this hook -- use the Core `renderer.on("selection", ...)` event.
### Selection Object
The `Selection` object passed to the event callback:
```typescript
selection.getSelectedText() // Aggregated text from all selected renderables
selection.bounds // { startX, startY, endX, endY } bounding rect
selection.selectedRenderables // Renderable[] with active selections
selection.isActive // Whether selection is still active
```
Individual renderables also expose:
```typescript
renderable.hasSelection() // Does this renderable have selected text?
renderable.getSelectedText() // Selected text in this renderable only
```
### How Selection Traversal Works
When the user drags to select, the renderer:
1. Identifies the selection container (common ancestor of start and end points)
2. Walks all `selectable` descendants within the selection bounds
3. Calls `onSelectionChanged(selection)` on each, which computes local selection
4. Tracks which renderables have active selections in `selection.selectedRenderables`
This means selection works across multiple renderables. Dragging across two `<text selectable>` elements selects text in both, and `selection.getSelectedText()` joins them with newlines.
## Clipboard API (OSC 52)
Copy text to the system clipboard using OSC 52 escape sequences. Works over SSH and in most modern terminal emulators.
```typescript
// Copy to clipboard
const success = renderer.copyToClipboardOSC52("text to copy")
// Check if OSC 52 is supported
if (renderer.isOsc52Supported()) {
renderer.copyToClipboardOSC52("Hello!")
}
// Clear clipboard
renderer.clearClipboardOSC52()
// Target specific clipboard (X11)
import { ClipboardTarget } from "@opentui/core"
renderer.copyToClipboardOSC52("text", ClipboardTarget.Primary) // X11 primary
renderer.copyToClipboardOSC52("text", ClipboardTarget.Clipboard) // System clipboard (default)
```
## Focus and Input Components
Input components (`<input>`, `<textarea>`, `<select>`) capture keyboard events when focused:
```tsx
<input focused /> // Receives keyboard input
// Global useKeyboard still fires, but input consumes characters
```
To prevent conflicts, check if an input is focused before handling global shortcuts:
```tsx
function App() {
const renderer = useRenderer()
const [inputFocused, setInputFocused] = useState(false)
useKeyboard((key) => {
if (inputFocused) return // Let input handle it
// Global shortcuts
if (key.name === "escape") {
renderer.destroy()
}
})
return (
<input
focused={inputFocused}
onFocus={() => setInputFocused(true)}
onBlur={() => setInputFocused(false)}
/>
)
}
```
## Gotchas
### Terminal Limitations
Some key combinations are captured by the terminal or OS:
- `Ctrl+C` often sends SIGINT (use `exitOnCtrlC: false` to handle)
- `Ctrl+Z` suspends the process
- Some function keys may be intercepted
### SSH and Remote Sessions
Key detection may vary over SSH. Test on target environments.
### Multiple Handlers
Multiple `useKeyboard` calls all receive events. Coordinate handlers to prevent conflicts.
## See Also
- [React API](../react/api.md) - `useKeyboard` hook reference
- [Solid API](../solid/api.md) - `useKeyboard` hook reference
- [Input Components](../components/inputs.md) - Focus management with input, textarea, select
- [Testing](../testing/REFERENCE.md) - Simulating key presses in tests
@@ -1,337 +0,0 @@
# OpenTUI Layout System
OpenTUI uses the Yoga layout engine, providing CSS Flexbox-like capabilities for positioning and sizing components in the terminal.
## Overview
Key concepts:
- **Flexbox model**: Familiar CSS Flexbox properties
- **Yoga engine**: Facebook's cross-platform layout engine
- **Terminal units**: Dimensions are in character cells (columns x rows)
- **Percentage support**: Relative sizing based on parent
## Flex Container Properties
### flexDirection
Controls the main axis direction:
```tsx
// Row (default) - children flow horizontally
<box flexDirection="row">
<text>1</text>
<text>2</text>
<text>3</text>
</box>
// Output: 1 2 3
// Column - children flow vertically
<box flexDirection="column">
<text>1</text>
<text>2</text>
<text>3</text>
</box>
// Output:
// 1
// 2
// 3
// Reverse variants
<box flexDirection="row-reverse">...</box> // 3 2 1
<box flexDirection="column-reverse">...</box> // Bottom to top
```
### justifyContent
Aligns children along the main axis:
```tsx
<box flexDirection="row" width={40} justifyContent="flex-start">
{/* Children at start (left for row) */}
</box>
<box flexDirection="row" width={40} justifyContent="flex-end">
{/* Children at end (right for row) */}
</box>
<box flexDirection="row" width={40} justifyContent="center">
{/* Children centered */}
</box>
<box flexDirection="row" width={40} justifyContent="space-between">
{/* First at start, last at end, rest evenly distributed */}
</box>
<box flexDirection="row" width={40} justifyContent="space-around">
{/* Equal space around each child */}
</box>
<box flexDirection="row" width={40} justifyContent="space-evenly">
{/* Equal space between all children and edges */}
</box>
```
### alignItems
Aligns children along the cross axis:
```tsx
<box flexDirection="row" height={10} alignItems="flex-start">
{/* Children at top */}
</box>
<box flexDirection="row" height={10} alignItems="flex-end">
{/* Children at bottom */}
</box>
<box flexDirection="row" height={10} alignItems="center">
{/* Children vertically centered */}
</box>
<box flexDirection="row" height={10} alignItems="stretch">
{/* Children stretch to fill height */}
</box>
<box flexDirection="row" height={10} alignItems="baseline">
{/* Children aligned by text baseline */}
</box>
```
### flexWrap
Controls whether children wrap to new lines:
```tsx
<box flexDirection="row" flexWrap="nowrap" width={20}>
{/* Children overflow (default) */}
</box>
<box flexDirection="row" flexWrap="wrap" width={20}>
{/* Children wrap to next row */}
</box>
<box flexDirection="row" flexWrap="wrap-reverse" width={20}>
{/* Children wrap upward */}
</box>
```
### gap
Space between children:
```tsx
<box flexDirection="row" gap={2}>
<text>A</text>
<text>B</text>
<text>C</text>
</box>
// Output: A B C (2 spaces between)
```
## Flex Item Properties
### flexGrow
How much a child should grow relative to siblings:
```tsx
<box flexDirection="row" width={30}>
<box flexGrow={1}><text>1</text></box>
<box flexGrow={2}><text>2</text></box>
<box flexGrow={1}><text>1</text></box>
</box>
// Widths: 7.5 | 15 | 7.5 (1:2:1 ratio)
```
### flexShrink
How much a child should shrink when space is limited:
```tsx
<box flexDirection="row" width={20}>
<box width={15} flexShrink={1}><text>Shrinks</text></box>
<box width={15} flexShrink={0}><text>Fixed</text></box>
</box>
```
### flexBasis
Initial size before growing/shrinking:
```tsx
<box flexDirection="row">
<box flexBasis={20} flexGrow={1}>Starts at 20, can grow</box>
<box flexBasis="50%">Half of parent</box>
</box>
```
### alignSelf
Override parent's alignItems for this child:
```tsx
<box flexDirection="row" height={10} alignItems="center">
<text>Centered</text>
<text alignSelf="flex-start">Top</text>
<text alignSelf="flex-end">Bottom</text>
</box>
```
## Dimensions
### Fixed Dimensions
```tsx
<box width={40} height={10}>
{/* Exactly 40 columns by 10 rows */}
</box>
```
### Percentage Dimensions
Parent must have explicit size:
```tsx
<box width="100%" height="100%">
<box width="50%" height="50%">
{/* Half of parent */}
</box>
</box>
```
### Min/Max Constraints
```tsx
<box
minWidth={20}
maxWidth={60}
minHeight={5}
maxHeight={20}
>
{/* Constrained sizing */}
</box>
```
## Spacing
### Padding (inside)
```tsx
// All sides
<box padding={2}>Content</box>
// Individual sides
<box
paddingTop={1}
paddingRight={2}
paddingBottom={1}
paddingLeft={2}
>
Content
</box>
```
### Margin (outside)
```tsx
// All sides
<box margin={1}>Content</box>
// Individual sides
<box
marginTop={1}
marginRight={2}
marginBottom={1}
marginLeft={2}
>
Content
</box>
```
## Positioning
### Relative (default)
Element flows in normal document order:
```tsx
<box position="relative">
{/* Normal flow */}
</box>
```
### Absolute
Element positioned relative to nearest positioned ancestor:
```tsx
<box position="relative" width="100%" height="100%">
<box
position="absolute"
left={10}
top={5}
width={20}
height={5}
>
Positioned at (10, 5)
</box>
</box>
```
### Position Properties
```tsx
<box
position="absolute"
left={10} // From left edge
top={5} // From top edge
right={10} // From right edge
bottom={5} // From bottom edge
>
Content
</box>
```
## Display
### Visibility Control
```tsx
// Visible (default)
<box display="flex">Visible</box>
// Hidden (removed from layout)
<box display="none">Hidden</box>
```
## Overflow
```tsx
<box overflow="visible">
{/* Content can extend beyond bounds (default) */}
</box>
<box overflow="hidden">
{/* Content clipped at bounds */}
</box>
<box overflow="scroll">
{/* Scrollable when content exceeds bounds */}
</box>
```
## Z-Index
Control stacking order for overlapping elements:
```tsx
<box position="relative">
<box position="absolute" zIndex={1}>Behind</box>
<box position="absolute" zIndex={2}>In front</box>
</box>
```
## See Also
- [Layout Patterns](./patterns.md) - Common layout recipes
- [Components/Containers](../components/containers.md) - Box and ScrollBox details
@@ -1,444 +0,0 @@
# Layout Patterns
Common layout recipes for terminal user interfaces.
## Full-Screen App
Fill the entire terminal:
```tsx
function App() {
return (
<box width="100%" height="100%">
{/* Content fills terminal */}
</box>
)
}
```
## Header/Content/Footer
Classic app layout:
```tsx
function AppLayout() {
return (
<box flexDirection="column" width="100%" height="100%">
{/* Header - fixed height */}
<box height={3} borderStyle="single" borderBottom>
<text>Header</text>
</box>
{/* Content - fills remaining space */}
<box flexGrow={1}>
<text>Main Content</text>
</box>
{/* Footer - fixed height */}
<box height={1}>
<text>Status: Ready</text>
</box>
</box>
)
}
```
## Sidebar Layout
```tsx
function SidebarLayout() {
return (
<box flexDirection="row" width="100%" height="100%">
{/* Sidebar - fixed width */}
<box width={25} borderStyle="single" borderRight>
<text>Sidebar</text>
</box>
{/* Main - fills remaining space */}
<box flexGrow={1}>
<text>Main Content</text>
</box>
</box>
)
}
```
## Resizable Sidebar
Responsive based on terminal width:
```tsx
function ResponsiveSidebar() {
const dims = useTerminalDimensions() // React: useTerminalDimensions()
const showSidebar = dims.width > 60
const sidebarWidth = Math.min(30, Math.floor(dims.width * 0.3))
return (
<box flexDirection="row" width="100%" height="100%">
{showSidebar && (
<box width={sidebarWidth} border>
<text>Sidebar</text>
</box>
)}
<box flexGrow={1}>
<text>Main</text>
</box>
</box>
)
}
```
## Centered Content
### Horizontally Centered
```tsx
<box width="100%" justifyContent="center">
<box width={40}>
<text>Centered horizontally</text>
</box>
</box>
```
### Vertically Centered
```tsx
<box height="100%" alignItems="center">
<text>Centered vertically</text>
</box>
```
### Both Axes
```tsx
<box
width="100%"
height="100%"
justifyContent="center"
alignItems="center"
>
<box width={40} height={10} border>
<text>Centered both ways</text>
</box>
</box>
```
## Modal/Dialog
Centered overlay:
```tsx
function Modal({ children, visible }) {
if (!visible) return null
return (
<box
position="absolute"
left={0}
top={0}
width="100%"
height="100%"
justifyContent="center"
alignItems="center"
backgroundColor="rgba(0,0,0,0.5)"
>
<box
width={50}
height={15}
border
borderStyle="double"
backgroundColor="#1a1a2e"
padding={2}
>
{children}
</box>
</box>
)
}
```
## Grid Layout
Using flexWrap:
```tsx
function Grid({ items, columns = 3 }) {
const itemWidth = `${Math.floor(100 / columns)}%`
return (
<box flexDirection="row" flexWrap="wrap" width="100%">
{items.map((item, i) => (
<box key={i} width={itemWidth} padding={1}>
<text>{item}</text>
</box>
))}
</box>
)
}
```
## Split Panels
### Horizontal Split
```tsx
function HorizontalSplit({ ratio = 0.5 }) {
return (
<box flexDirection="row" width="100%" height="100%">
<box width={`${ratio * 100}%`} border>
<text>Left Panel</text>
</box>
<box flexGrow={1} border>
<text>Right Panel</text>
</box>
</box>
)
}
```
### Vertical Split
```tsx
function VerticalSplit({ ratio = 0.5 }) {
return (
<box flexDirection="column" width="100%" height="100%">
<box height={`${ratio * 100}%`} border>
<text>Top Panel</text>
</box>
<box flexGrow={1} border>
<text>Bottom Panel</text>
</box>
</box>
)
}
```
## Form Layout
Label + Input pairs:
```tsx
function FormField({ label, children }) {
return (
<box flexDirection="row" marginBottom={1}>
<box width={15}>
<text>{label}:</text>
</box>
<box flexGrow={1}>
{children}
</box>
</box>
)
}
function LoginForm() {
return (
<box flexDirection="column" padding={2} border width={50}>
<FormField label="Username">
<input placeholder="Enter username" />
</FormField>
<FormField label="Password">
<input placeholder="Enter password" />
</FormField>
<box marginTop={2} justifyContent="flex-end">
<box border padding={1}>
<text>Login</text>
</box>
</box>
</box>
)
}
```
## Navigation Tabs
```tsx
function TabBar({ tabs, activeIndex, onSelect }) {
return (
<box flexDirection="row" borderBottom>
{tabs.map((tab, i) => (
<box
key={i}
padding={1}
backgroundColor={i === activeIndex ? "#333" : "transparent"}
onMouseDown={() => onSelect(i)}
>
<text fg={i === activeIndex ? "#fff" : "#888"}>
{tab}
</text>
</box>
))}
</box>
)
}
```
## Sticky Footer
Footer always at bottom:
```tsx
function StickyFooterLayout() {
return (
<box flexDirection="column" width="100%" height="100%">
{/* Content area */}
<box flexGrow={1} flexDirection="column">
{/* Your content here */}
<text>Content that might be short</text>
</box>
{/* Footer pushed to bottom */}
<box height={1}>
<text fg="#888">Press ? for help | q to quit</text>
</box>
</box>
)
}
```
## Absolute Positioning Overlay
Tooltip or popup:
```tsx
function Tooltip({ x, y, children }) {
return (
<box
position="absolute"
left={x}
top={y}
border
backgroundColor="#333"
padding={1}
zIndex={100}
>
{children}
</box>
)
}
```
## Responsive Breakpoints
Different layouts based on terminal size:
```tsx
function ResponsiveApp() {
const { width, height } = useTerminalDimensions()
// Define breakpoints
const isSmall = width < 60
const isMedium = width >= 60 && width < 100
const isLarge = width >= 100
if (isSmall) {
// Mobile-like: stacked layout
return (
<box flexDirection="column">
<Navigation />
<Content />
</box>
)
}
if (isMedium) {
// Tablet-like: sidebar + content
return (
<box flexDirection="row">
<box width={20}><Navigation /></box>
<box flexGrow={1}><Content /></box>
</box>
)
}
// Large: full layout
return (
<box flexDirection="row">
<box width={25}><Navigation /></box>
<box flexGrow={1}><Content /></box>
<box width={30}><Sidebar /></box>
</box>
)
}
```
## Equal Height Columns
```tsx
function EqualColumns() {
return (
<box flexDirection="row" alignItems="stretch" height={20}>
<box flexGrow={1} border>
<text>Short content</text>
</box>
<box flexGrow={1} border>
<text>
Longer content that
spans multiple lines
and takes up space
</text>
</box>
<box flexGrow={1} border>
<text>Medium content</text>
</box>
</box>
)
}
```
## Spacing Utilities
Consistent spacing patterns:
```tsx
// Spacer component
function Spacer({ size = 1 }) {
return <box height={size} width={size} />
}
// Divider component
function Divider() {
return <box height={1} width="100%" backgroundColor="#333" />
}
// Usage
<box flexDirection="column">
<text>Section 1</text>
<Spacer size={2} />
<Divider />
<Spacer size={2} />
<text>Section 2</text>
</box>
```
### Axis Shorthand Props
Use `paddingX`/`paddingY` and `marginX`/`marginY` for horizontal/vertical spacing:
```tsx
// Horizontal padding (left + right)
<box paddingX={4}>
<text>4 chars padding left and right</text>
</box>
// Vertical padding (top + bottom)
<box paddingY={2}>
<text>2 lines padding top and bottom</text>
</box>
// Horizontal margin for centering-like effect
<box marginX={10}>
<text>Indented content</text>
</box>
// Combined for card-like spacing
<box paddingX={3} paddingY={1} marginY={1} border>
<text>Nicely spaced card</text>
</box>
```
These are shorthand for:
- `paddingX={n}` = `paddingLeft={n}` + `paddingRight={n}`
- `paddingY={n}` = `paddingTop={n}` + `paddingBottom={n}`
- `marginX={n}` = `marginLeft={n}` + `marginRight={n}`
- `marginY={n}` = `marginTop={n}` + `marginBottom={n}`
@@ -1,174 +0,0 @@
# OpenTUI React (@opentui/react)
A React reconciler for building terminal user interfaces with familiar React patterns. Write TUIs using JSX, hooks, and component composition.
## Overview
OpenTUI React provides:
- **Custom reconciler**: React components render to OpenTUI renderables
- **JSX intrinsics**: `<text>`, `<box>`, `<input>`, etc.
- **Hooks**: `useKeyboard`, `useRenderer`, `useTimeline`, etc.
- **Full React compatibility**: useState, useEffect, context, and more
## When to Use React
Use the React reconciler when:
- You're familiar with React patterns
- You want declarative UI composition
- You need React's ecosystem (context, state management libraries)
- Building applications with complex state
- Team knows React already
## When NOT to Use React
| Scenario | Use Instead |
|----------|-------------|
| Maximum performance critical | `@opentui/core` (imperative) |
| Fine-grained reactivity | `@opentui/solid` |
| Smallest bundle size | `@opentui/core` |
| Building a framework/library | `@opentui/core` |
## Quick Start
```bash
bunx create-tui@latest -t react my-app
cd my-app
bun run src/index.tsx
```
The CLI creates the `my-app` directory for you - it must **not already exist**.
**Agent guidance**: Always use autonomous mode with `-t <template>` flag. Never use interactive mode (`bunx create-tui@latest my-app` without `-t`) as it requires user prompts that agents cannot respond to.
Or manual setup:
```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/react @opentui/core react
```
```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
import { useState } from "react"
function App() {
const [count, setCount] = useState(0)
return (
<box border padding={2}>
<text>Count: {count}</text>
<box
border
onMouseDown={() => setCount(c => c + 1)}
>
<text>Click me!</text>
</box>
</box>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```
## Core Concepts
### JSX Elements
React maps JSX intrinsic elements to OpenTUI renderables:
```tsx
// These are not HTML elements!
<text>Hello</text> // TextRenderable
<box border>Content</box> // BoxRenderable
<input placeholder="..." /> // InputRenderable
<select options={[...]} /> // SelectRenderable
```
### Text Modifiers
Inside `<text>`, use modifier elements:
```tsx
<text>
<strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
<span fg="red">Colored text</span>
<br />
New line with <a href="https://example.com">link</a>
</text>
```
### Styling
Two approaches to styling:
```tsx
// Direct props
<box backgroundColor="blue" padding={2} border>
<text fg="#00FF00">Green text</text>
</box>
// Style prop
<box style={{ backgroundColor: "blue", padding: 2, border: true }}>
<text style={{ fg: "#00FF00" }}>Green text</text>
</box>
```
## Available Components
### Layout & Display
- `<text>` - Styled text content
- `<box>` - Container with borders and layout
- `<scrollbox>` - Scrollable container
- `<ascii-font>` - ASCII art text
### Input
- `<input>` - Single-line text input
- `<textarea>` - Multi-line text input
- `<select>` - List selection
- `<tab-select>` - Tab-based selection
### Code & Diff
- `<code>` - Syntax-highlighted code
- `<line-number>` - Code with line numbers
- `<diff>` - Unified or split diff viewer
### Text Modifiers (inside `<text>`)
- `<span>` - Inline styled text
- `<strong>`, `<b>` - Bold
- `<em>`, `<i>` - Italic
- `<u>` - Underline
- `<br>` - Line break
- `<a>` - Link
## Essential Hooks
```tsx
import {
useRenderer,
useKeyboard,
useOnResize,
useTerminalDimensions,
useTimeline,
} from "@opentui/react"
```
See [API Reference](./api.md) for detailed hook documentation.
## In This Reference
- [Configuration](./configuration.md) - Project setup, tsconfig, bundling
- [API](./api.md) - Components, hooks, createRoot
- [Patterns](./patterns.md) - State management, keyboard handling, forms
- [Gotchas](./gotchas.md) - Common issues, debugging, limitations
## See Also
- [Core](../core/REFERENCE.md) - Underlying imperative API
- [Solid](../solid/REFERENCE.md) - Alternative declarative approach
- [Components](../components/REFERENCE.md) - Component reference by category
- [Layout](../layout/REFERENCE.md) - Flexbox layout system
- [Keyboard](../keyboard/REFERENCE.md) - Input handling and shortcuts
- [Testing](../testing/REFERENCE.md) - Test renderer and snapshots
@@ -1,436 +0,0 @@
# React API Reference
## Rendering
### createRoot(renderer)
Creates a React root for rendering.
```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
const renderer = await createCliRenderer({
exitOnCtrlC: false, // Handle Ctrl+C yourself
})
const root = createRoot(renderer)
root.render(<App />)
```
## Hooks
### useRenderer()
Access the OpenTUI renderer instance.
```tsx
import { useRenderer } from "@opentui/react"
import { useEffect } from "react"
function App() {
const renderer = useRenderer()
useEffect(() => {
// Access renderer properties
console.log(`Terminal: ${renderer.width}x${renderer.height}`)
// Show debug console
renderer.console.show()
// Access theme mode (dark/light based on terminal settings)
console.log(`Theme: ${renderer.themeMode}`) // "dark" | "light" | null
}, [renderer])
return <text>Hello</text>
}
// Listen for theme mode changes
function ThemedApp() {
const renderer = useRenderer()
const [theme, setTheme] = useState(renderer.themeMode ?? "dark")
useEffect(() => {
const handler = (mode: "dark" | "light") => setTheme(mode)
renderer.on("theme_mode", handler)
return () => renderer.off("theme_mode", handler)
}, [renderer])
return (
<box backgroundColor={theme === "dark" ? "#1a1a2e" : "#ffffff"}>
<text fg={theme === "dark" ? "#fff" : "#000"}>
Current theme: {theme}
</text>
</box>
)
}
```
### useKeyboard(handler, options?)
Handle keyboard events.
```tsx
import { useKeyboard, useRenderer } from "@opentui/react"
function App() {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "escape") {
renderer.destroy() // Never use process.exit() directly!
}
if (key.ctrl && key.name === "s") {
saveDocument()
}
})
return <text>Press ESC to exit</text>
}
// With release events
function GameControls() {
const [pressed, setPressed] = useState(new Set<string>())
useKeyboard(
(event) => {
setPressed(keys => {
const newKeys = new Set(keys)
if (event.eventType === "release") {
newKeys.delete(event.name)
} else {
newKeys.add(event.name)
}
return newKeys
})
},
{ release: true } // Include release events
)
return <text>Pressed: {Array.from(pressed).join(", ")}</text>
}
```
**Options:**
- `release?: boolean` - Include key release events (default: false)
**KeyEvent properties:**
- `name: string` - Key name ("a", "escape", "f1", etc.)
- `sequence: string` - Raw escape sequence
- `ctrl: boolean` - Ctrl modifier
- `shift: boolean` - Shift modifier
- `meta: boolean` - Alt modifier
- `option: boolean` - Option modifier (macOS)
- `eventType: "press" | "release" | "repeat"`
- `repeated: boolean` - Key is being held
### useOnResize(callback)
Handle terminal resize events.
```tsx
import { useOnResize } from "@opentui/react"
function App() {
useOnResize((width, height) => {
console.log(`Resized to ${width}x${height}`)
})
return <text>Resize the terminal</text>
}
```
### useTerminalDimensions()
Get reactive terminal dimensions.
```tsx
import { useTerminalDimensions } from "@opentui/react"
function ResponsiveLayout() {
const { width, height } = useTerminalDimensions()
return (
<box flexDirection={width > 80 ? "row" : "column"}>
<box flexGrow={1}>
<text>Width: {width}</text>
</box>
<box flexGrow={1}>
<text>Height: {height}</text>
</box>
</box>
)
}
```
### useTimeline(options?)
Create animations with the timeline system.
```tsx
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
function AnimatedBox() {
const [width, setWidth] = useState(0)
const timeline = useTimeline({
duration: 2000,
loop: false,
})
useEffect(() => {
timeline.add(
{ width: 0 },
{
width: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].width))
},
}
)
}, [timeline])
return <box style={{ width, height: 3, backgroundColor: "#6a5acd" }} />
}
```
**Options:**
- `duration?: number` - Default duration (ms)
- `loop?: boolean` - Loop the timeline
- `autoplay?: boolean` - Auto-start (default: true)
- `onComplete?: () => void` - Completion callback
- `onPause?: () => void` - Pause callback
**Timeline methods:**
- `add(target, properties, startTime?)` - Add animation
- `play()` - Start playback
- `pause()` - Pause playback
- `restart()` - Restart from beginning
## Components
### Text Component
```tsx
<text
content="Hello" // Or use children
fg="#FFFFFF" // Foreground color
bg="#000000" // Background color
selectable={true} // Allow text selection
>
{/* Use nested modifier tags for styling */}
<span fg="red">Red</span>
<strong>Bold</strong>
<em>Italic</em>
<u>Underline</u>
<br />
<a href="https://...">Link</a>
</text>
```
> **Note**: Do NOT use `bold`, `italic`, `underline` as props on `<text>`. Use nested modifier tags like `<strong>`, `<em>`, `<u>` instead.
### Box Component
```tsx
<box
// Borders
border // Enable border
borderStyle="single" // single | double | rounded | bold
borderColor="#FFFFFF"
title="Title"
titleAlignment="center" // left | center | right
// Colors
backgroundColor="#1a1a2e"
// Layout (see layout/REFERENCE.md)
flexDirection="row"
justifyContent="center"
alignItems="center"
gap={2}
// Spacing
padding={2}
paddingTop={1}
paddingX={2} // Horizontal (left + right)
paddingY={1} // Vertical (top + bottom)
margin={1}
marginX={2} // Horizontal (left + right)
marginY={1} // Vertical (top + bottom)
// Dimensions
width={40}
height={10}
flexGrow={1}
// Focus
focusable // Allow box to receive focus
focused={isFocused} // Controlled focus state
// Events
onMouseDown={(e) => {}}
onMouseUp={(e) => {}}
onMouseMove={(e) => {}}
>
{children}
</box>
```
### Scrollbox Component
```tsx
<scrollbox
focused // Enable keyboard scrolling
style={{
rootOptions: { backgroundColor: "#24283b" },
wrapperOptions: { backgroundColor: "#1f2335" },
viewportOptions: { backgroundColor: "#1a1b26" },
contentOptions: { backgroundColor: "#16161e" },
scrollbarOptions: {
showArrows: true,
trackOptions: {
foregroundColor: "#7aa2f7",
backgroundColor: "#414868",
},
},
}}
>
{/* Scrollable content */}
{items.map((item, i) => (
<box key={i}>
<text>{item}</text>
</box>
))}
</scrollbox>
```
### Input Component
```tsx
<input
value={value}
onChange={(newValue) => setValue(newValue)}
placeholder="Enter text..."
focused // Start focused
width={30}
backgroundColor="#1a1a1a"
textColor="#FFFFFF"
cursorColor="#00FF00"
focusedBackgroundColor="#2a2a2a"
/>
```
### Textarea Component
```tsx
<textarea
value={text}
onChange={(newValue) => setText(newValue)}
placeholder="Enter multiple lines..."
focused
width={40}
height={10}
showLineNumbers
wrapText
/>
```
### Select Component
```tsx
<select
options={[
{ name: "Option 1", description: "First option", value: "1" },
{ name: "Option 2", description: "Second option", value: "2" },
]}
onChange={(index, option) => setSelected(option)}
selectedIndex={0}
focused
showScrollIndicator
height={8}
/>
```
### Tab Select Component
```tsx
<tab-select
options={[
{ name: "Home", description: "Dashboard" },
{ name: "Settings", description: "Configuration" },
]}
onChange={(index, option) => setTab(option)}
tabWidth={20}
focused
/>
```
### ASCII Font Component
```tsx
<ascii-font
text="TITLE"
font="tiny" // tiny | block | slick | shade
color="#FFFFFF"
/>
```
### Code Component
```tsx
<code
code={sourceCode}
language="typescript"
showLineNumbers
highlightLines={[1, 5, 10]}
/>
```
### Line Number Component
```tsx
<line-number
code={sourceCode}
language="typescript"
startLine={1}
highlightedLines={[5]}
diagnostics={[
{ line: 3, severity: "error", message: "Syntax error" }
]}
/>
```
### Diff Component
```tsx
<diff
oldCode={originalCode}
newCode={modifiedCode}
language="typescript"
mode="unified" // unified | split
syncScroll // Sync scroll between split view panes
showLineNumbers
/>
```
## Type Exports
```tsx
import type {
// Component props
TextProps,
BoxProps,
InputProps,
SelectProps,
// Hook types
KeyEvent,
// From core
CliRenderer,
} from "@opentui/react"
```
@@ -1,302 +0,0 @@
# React Configuration
## Project Setup
### Quick Start
```bash
bunx create-tui@latest -t react my-app
cd my-app && bun install
```
The CLI creates the `my-app` directory for you - it must **not already exist**.
Options: `--no-git` (skip git init), `--no-install` (skip bun install)
### Manual Setup
```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/react @opentui/core react
```
## TypeScript Configuration
### tsconfig.json
```json
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["bun-types"]
},
"include": ["src/**/*"]
}
```
**Critical settings:**
- `jsx: "react-jsx"` - Use the new JSX transform
- `jsxImportSource: "@opentui/react"` - Import JSX runtime from OpenTUI
- `module` / `moduleResolution: "NodeNext"` - Recommended for OpenTUI compatibility
### Why DOM lib?
The `DOM` lib is needed for React types. OpenTUI's JSX types extend React's.
## Package Configuration
### package.json
```json
{
"name": "my-tui-app",
"type": "module",
"scripts": {
"start": "bun run src/index.tsx",
"dev": "bun --watch run src/index.tsx",
"test": "bun test",
"build": "bun build src/index.tsx --outdir=dist --target=bun"
},
"dependencies": {
"@opentui/core": "latest",
"@opentui/react": "latest",
"react": ">=19.0.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": ">=19.0.0",
"typescript": "latest"
}
}
```
## Project Structure
Recommended structure:
```
my-tui-app/
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── Sidebar.tsx
│ │ └── MainContent.tsx
│ ├── hooks/
│ │ └── useAppState.ts
│ ├── App.tsx
│ └── index.tsx
├── package.json
└── tsconfig.json
```
### Entry Point (src/index.tsx)
```tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
import { App } from "./App"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
})
createRoot(renderer).render(<App />)
```
### App Component (src/App.tsx)
```tsx
import { Header } from "./components/Header"
import { Sidebar } from "./components/Sidebar"
import { MainContent } from "./components/MainContent"
export function App() {
return (
<box flexDirection="column" width="100%" height="100%">
<Header />
<box flexDirection="row" flexGrow={1}>
<Sidebar />
<MainContent />
</box>
</box>
)
}
```
## Renderer Configuration
### createCliRenderer Options
```tsx
import { createCliRenderer, ConsolePosition } from "@opentui/core"
const renderer = await createCliRenderer({
// Rendering
targetFPS: 60,
// Behavior
exitOnCtrlC: true, // Set false to handle Ctrl+C yourself
autoFocus: true, // Auto-focus elements on click (default: true)
useMouse: true, // Enable mouse support (default: true)
// Debug console
consoleOptions: {
position: ConsolePosition.BOTTOM,
sizePercent: 30,
startInDebugMode: false,
},
// Cleanup
onDestroy: () => {
// Cleanup code
},
})
```
## Building for Distribution
### Bundling with Bun
```typescript
// build.ts
await Bun.build({
entrypoints: ["./src/index.tsx"],
outdir: "./dist",
target: "bun",
minify: true,
})
```
Run: `bun run build.ts`
### Creating Executables
```typescript
// build.ts
await Bun.build({
entrypoints: ["./src/index.tsx"],
outdir: "./dist",
target: "bun",
compile: {
target: "bun-darwin-arm64", // or bun-linux-x64, etc.
outfile: "my-app",
},
})
```
## Environment Variables
Create `.env` for development:
```env
# Debug settings
OTUI_SHOW_STATS=false
SHOW_CONSOLE=false
# App settings
API_URL=https://api.example.com
```
Bun auto-loads `.env` files. Access via `process.env`:
```tsx
const apiUrl = process.env.API_URL
```
## React DevTools
OpenTUI React supports React DevTools for debugging.
### Setup
1. Install DevTools as a dev dependency (must use version 7):
```bash
bun add react-devtools-core@7 -d
```
2. Run DevTools standalone app:
```bash
npx react-devtools@7
```
3. Start your app with `DEV=true` environment variable:
```bash
DEV=true bun run src/index.tsx
```
**Important**: Auto-connect to DevTools ONLY happens when `DEV=true` is set. Without this environment variable, the DevTools connection code is not loaded.
### How It Works
OpenTUI checks for `process.env["DEV"] === "true"` at startup. When true, it dynamically imports `react-devtools-core` and connects to the standalone DevTools app.
## Testing Configuration
### Test Setup
```typescript
// src/test-utils.tsx
import { createTestRenderer } from "@opentui/core/testing"
import { createRoot } from "@opentui/react"
export async function renderForTest(
element: React.ReactElement,
options = { width: 80, height: 24 }
) {
const testSetup = await createTestRenderer(options)
createRoot(testSetup.renderer).render(element)
return testSetup
}
```
### Test Example
```typescript
// src/components/Counter.test.tsx
import { test, expect } from "bun:test"
import { renderForTest } from "../test-utils"
import { Counter } from "./Counter"
test("Counter renders initial value", async () => {
const { snapshot } = await renderForTest(<Counter initialValue={5} />)
expect(snapshot()).toContain("Count: 5")
})
```
## Common Issues
### JSX Types Not Working
Ensure `jsxImportSource` is set:
```json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react"
}
}
```
### React Version Mismatch
Ensure React 19+:
```bash
bun install react@19 @types/react@19
```
### Module Resolution Errors
Use `moduleResolution: "bundler"` for Bun compatibility.
@@ -1,443 +0,0 @@
# React Gotchas
## Critical
### Never use `process.exit()` directly
**This is the most common mistake.** Using `process.exit()` leaves the terminal in a broken state (cursor hidden, raw mode, alternate screen).
```tsx
// WRONG - Terminal left in broken state
process.exit(0)
// CORRECT - Use renderer.destroy()
import { useRenderer } from "@opentui/react"
function App() {
const renderer = useRenderer()
const handleExit = () => {
renderer.destroy() // Cleans up and exits properly
}
}
```
`renderer.destroy()` restores the terminal (exits alternate screen, restores cursor, etc.) before exiting.
### Signal Handling
OpenTUI automatically handles cleanup for these signals:
- `SIGINT` (Ctrl+C), `SIGTERM`, `SIGQUIT` - Standard termination
- `SIGHUP` - Terminal closed/hangup
- `SIGBREAK` - Ctrl+Break (Windows)
- `SIGPIPE` - Broken pipe (output closed)
- `SIGBUS`, `SIGFPE` - Hardware errors
This ensures terminal state is restored even on unexpected termination. If you need custom signal handling, use `exitOnCtrlC: false` and handle signals yourself while still calling `renderer.destroy()`.
## JSX Configuration
### Missing jsxImportSource
**Symptom**: JSX elements have wrong types, components don't render
```
// Error: Property 'text' does not exist on type 'JSX.IntrinsicElements'
```
**Fix**: Configure tsconfig.json:
```json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react"
}
}
```
### HTML Elements vs TUI Elements
OpenTUI's JSX elements are **not** HTML elements:
```tsx
// WRONG - These are HTML concepts
<div>Not supported</div>
<button>Not supported</button>
<span>Only works inside <text></span>
// CORRECT - OpenTUI elements
<box>Container</box>
<text>Display text</text>
<text><span>Inline styled</span></text>
```
## Component Issues
### Text Modifiers Outside Text
Text modifiers only work inside `<text>`:
```tsx
// WRONG
<box>
<strong>This won't work</strong>
</box>
// CORRECT
<box>
<text>
<strong>This works</strong>
</text>
</box>
```
### Focus Not Working
Components must be explicitly focused:
```tsx
// WRONG - Won't receive keyboard input
<input placeholder="Type here..." />
// CORRECT
<input placeholder="Type here..." focused />
// Or manage focus state
const [isFocused, setIsFocused] = useState(true)
<input placeholder="Type here..." focused={isFocused} />
```
### Select Not Responding
Select requires focus and proper options format:
```tsx
// WRONG - Missing required properties
<select options={["a", "b", "c"]} />
// CORRECT
<select
options={[
{ name: "Option A", description: "First option", value: "a" },
{ name: "Option B", description: "Second option", value: "b" },
]}
onSelect={(index, option) => {
// Called when Enter is pressed
console.log("Selected:", option.name)
}}
focused
/>
```
### Select Events Confusion
Remember: `onSelect` fires on Enter (selection confirmed), `onChange` fires on navigation:
```tsx
// WRONG - expecting onChange to fire on Enter
<select
options={options}
onChange={(i, opt) => submitForm(opt)} // This fires on arrow keys!
/>
// CORRECT
<select
options={options}
onSelect={(i, opt) => submitForm(opt)} // Enter pressed - submit
onChange={(i, opt) => showPreview(opt)} // Arrow keys - preview
/>
```
## Hook Issues
### useKeyboard Not Firing
Multiple `useKeyboard` hooks can conflict:
```tsx
// Both handlers fire - may cause issues
function App() {
useKeyboard((key) => { /* parent handler */ })
return <ChildWithKeyboard />
}
function ChildWithKeyboard() {
useKeyboard((key) => { /* child handler */ })
return <text>Child</text>
}
```
**Solution**: Use a single keyboard handler or implement event stopping:
```tsx
function App() {
const [handled, setHandled] = useState(false)
useKeyboard((key) => {
if (handled) {
setHandled(false)
return
}
// Handle at app level
})
return <Child onKeyHandled={() => setHandled(true)} />
}
```
### useEffect Cleanup
Always clean up intervals and listeners:
```tsx
// WRONG - Memory leak
useEffect(() => {
setInterval(() => updateData(), 1000)
}, [])
// CORRECT
useEffect(() => {
const interval = setInterval(() => updateData(), 1000)
return () => clearInterval(interval) // Cleanup!
}, [])
```
## Styling Issues
### Colors Not Applying
Check color format:
```tsx
// CORRECT formats
<text fg="#FF0000">Red</text>
<text fg="red">Red</text>
<box backgroundColor="#1a1a2e">Box</box>
// WRONG
<text fg="FF0000">Missing #</text>
<text color="#FF0000">Wrong prop name (use fg)</text>
```
### Layout Not Working
Ensure parent has dimensions:
```tsx
// WRONG - Parent has no height
<box flexDirection="column">
<box flexGrow={1}>Won't grow</box>
</box>
// CORRECT
<box flexDirection="column" height="100%">
<box flexGrow={1}>Will grow</box>
</box>
```
### Percentage Widths Not Working
Parent must have explicit dimensions:
```tsx
// WRONG
<box>
<box width="50%">Won't work</box>
</box>
// CORRECT
<box width="100%">
<box width="50%">Works</box>
</box>
```
## Performance Issues
### Too Many Re-renders
Avoid inline objects/functions in props:
```tsx
// WRONG - New object every render
<box style={{ padding: 2 }}>Content</box>
// BETTER - Use direct props
<box padding={2}>Content</box>
// OR memoize style objects
const style = useMemo(() => ({ padding: 2 }), [])
<box style={style}>Content</box>
```
### Heavy Components
Use React.memo for expensive components:
```tsx
const ExpensiveList = React.memo(function ExpensiveList({
items
}: {
items: Item[]
}) {
return (
<box flexDirection="column">
{items.map(item => (
<text key={item.id}>{item.name}</text>
))}
</box>
)
})
```
### State Updates During Render
Don't update state during render:
```tsx
// WRONG
function Component({ value }: { value: number }) {
const [count, setCount] = useState(0)
// This causes infinite loop!
if (value > 10) {
setCount(value)
}
return <text>{count}</text>
}
// CORRECT
function Component({ value }: { value: number }) {
const [count, setCount] = useState(0)
useEffect(() => {
if (value > 10) {
setCount(value)
}
}, [value])
return <text>{count}</text>
}
```
## Debugging
### Console Not Visible
OpenTUI captures console output. Show the overlay:
```tsx
import { useRenderer } from "@opentui/react"
import { useEffect } from "react"
function App() {
const renderer = useRenderer()
useEffect(() => {
renderer.console.show()
console.log("Now you can see this!")
}, [renderer])
return <box>{/* ... */}</box>
}
```
### Component Not Rendering
Check if component is in the tree:
```tsx
// WRONG - Conditional returns nothing
function MaybeComponent({ show }: { show: boolean }) {
if (!show) return // Returns undefined!
return <text>Visible</text>
}
// CORRECT
function MaybeComponent({ show }: { show: boolean }) {
if (!show) return null // Explicit null
return <text>Visible</text>
}
```
### Events Not Firing
Check event handler names:
```tsx
// WRONG
<box onClick={() => {}}>Click</box> // No onClick in TUI
// CORRECT
<box onMouseDown={() => {}}>Click</box>
<box onMouseUp={() => {}}>Click</box>
```
## Runtime Issues
### Use Bun, Not Node
```bash
# WRONG
node src/index.tsx
npm run start
# CORRECT
bun run src/index.tsx
bun run start
```
### Async Top-level
Bun supports top-level await, but be careful:
```tsx
// index.tsx - This works in Bun
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
// If you need to handle errors
try {
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
} catch (error) {
console.error("Failed to initialize:", error)
process.exit(1)
}
```
## Common Error Messages
### "Cannot read properties of undefined (reading 'root')"
Renderer not initialized:
```tsx
// WRONG
const renderer = createCliRenderer() // Missing await!
createRoot(renderer).render(<App />)
// CORRECT
const renderer = await createCliRenderer()
createRoot(renderer).render(<App />)
```
### "Invalid hook call"
Hooks called outside component:
```tsx
// WRONG
const dimensions = useTerminalDimensions() // Outside component!
function App() {
return <text>{dimensions.width}</text>
}
// CORRECT
function App() {
const dimensions = useTerminalDimensions()
return <text>{dimensions.width}</text>
}
```
@@ -1,501 +0,0 @@
# React Patterns
## State Management
### Local State with useState
```tsx
import { useState } from "react"
function Counter() {
const [count, setCount] = useState(0)
return (
<box flexDirection="row" gap={2}>
<text>Count: {count}</text>
<box border onMouseDown={() => setCount(c => c - 1)}>
<text>-</text>
</box>
<box border onMouseDown={() => setCount(c => c + 1)}>
<text>+</text>
</box>
</box>
)
}
```
### Complex State with useReducer
```tsx
import { useReducer } from "react"
type State = {
items: string[]
selectedIndex: number
}
type Action =
| { type: "ADD_ITEM"; item: string }
| { type: "REMOVE_ITEM"; index: number }
| { type: "SELECT"; index: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case "ADD_ITEM":
return { ...state, items: [...state.items, action.item] }
case "REMOVE_ITEM":
return {
...state,
items: state.items.filter((_, i) => i !== action.index),
}
case "SELECT":
return { ...state, selectedIndex: action.index }
}
}
function ItemList() {
const [state, dispatch] = useReducer(reducer, {
items: [],
selectedIndex: 0,
})
// Use state and dispatch...
}
```
### Context for Global State
```tsx
import { createContext, useContext, useState, ReactNode } from "react"
type Theme = "dark" | "light"
const ThemeContext = createContext<{
theme: Theme
setTheme: (theme: Theme) => void
} | null>(null)
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("dark")
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
function useTheme() {
const context = useContext(ThemeContext)
if (!context) throw new Error("useTheme must be used within ThemeProvider")
return context
}
// Usage
function App() {
return (
<ThemeProvider>
<ThemedBox />
</ThemeProvider>
)
}
function ThemedBox() {
const { theme } = useTheme()
return (
<box backgroundColor={theme === "dark" ? "#1a1a2e" : "#f0f0f0"}>
<text fg={theme === "dark" ? "#fff" : "#000"}>
Current theme: {theme}
</text>
</box>
)
}
```
## Focus Management
### Focus State
```tsx
import { useState } from "react"
import { useKeyboard } from "@opentui/react"
function FocusableForm() {
const [focusIndex, setFocusIndex] = useState(0)
const fields = ["name", "email", "message"]
useKeyboard((key) => {
if (key.name === "tab") {
setFocusIndex(i => (i + 1) % fields.length)
}
if (key.shift && key.name === "tab") {
setFocusIndex(i => (i - 1 + fields.length) % fields.length)
}
})
return (
<box flexDirection="column" gap={1}>
{fields.map((field, i) => (
<input
key={field}
placeholder={`Enter ${field}...`}
focused={i === focusIndex}
/>
))}
</box>
)
}
```
### Ref-based Focus
```tsx
import { useRef, useEffect } from "react"
function AutoFocusInput() {
const inputRef = useRef<any>(null)
useEffect(() => {
// Focus on mount
inputRef.current?.focus()
}, [])
return <input ref={inputRef} placeholder="Auto-focused" />
}
```
## Keyboard Navigation
### Global Shortcuts
```tsx
import { useKeyboard, useRenderer } from "@opentui/react"
function App() {
const renderer = useRenderer()
useKeyboard((key) => {
// Quit on Escape or Ctrl+C - use renderer.destroy(), never process.exit()
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
renderer.destroy()
return
}
// Toggle help on ?
if (key.name === "?" || (key.shift && key.name === "/")) {
setShowHelp(h => !h)
}
// Vim-style navigation
if (key.name === "j") moveDown()
if (key.name === "k") moveUp()
})
return <box>{/* ... */}</box>
}
```
### Component-level Shortcuts
```tsx
function Editor() {
const [mode, setMode] = useState<"normal" | "insert">("normal")
useKeyboard((key) => {
if (mode === "normal") {
if (key.name === "i") setMode("insert")
if (key.name === "escape") setMode("normal")
} else {
if (key.name === "escape") setMode("normal")
// Handle text input in insert mode
}
})
return (
<box>
<text>Mode: {mode}</text>
<textarea focused={mode === "insert"} />
</box>
)
}
```
## Form Handling
### Controlled Inputs
```tsx
import { useState } from "react"
function LoginForm() {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const handleSubmit = () => {
console.log("Login:", { username, password })
}
return (
<box flexDirection="column" gap={1} padding={2} border>
<text>Login</text>
<box flexDirection="row" gap={1}>
<text>Username:</text>
<input
value={username}
onChange={setUsername}
width={20}
/>
</box>
<box flexDirection="row" gap={1}>
<text>Password:</text>
<input
value={password}
onChange={setPassword}
width={20}
/>
</box>
<box border onMouseDown={handleSubmit}>
<text>Submit</text>
</box>
</box>
)
}
```
### Form Validation
```tsx
function ValidatedForm() {
const [email, setEmail] = useState("")
const [error, setError] = useState("")
const validateEmail = (value: string) => {
if (!value.includes("@")) {
setError("Invalid email address")
} else {
setError("")
}
setEmail(value)
}
return (
<box flexDirection="column" gap={1}>
<input
value={email}
onChange={validateEmail}
placeholder="Email"
/>
{error && <text fg="red">{error}</text>}
</box>
)
}
```
## Responsive Design
### Terminal-size Responsive
```tsx
import { useTerminalDimensions } from "@opentui/react"
function ResponsiveLayout() {
const { width } = useTerminalDimensions()
// Stack vertically on narrow terminals
const isNarrow = width < 80
return (
<box flexDirection={isNarrow ? "column" : "row"}>
<box flexGrow={isNarrow ? 0 : 1} height={isNarrow ? 10 : "100%"}>
<text>Sidebar</text>
</box>
<box flexGrow={1}>
<text>Main Content</text>
</box>
</box>
)
}
```
### Dynamic Layouts
```tsx
function DynamicGrid({ items }: { items: string[] }) {
const { width } = useTerminalDimensions()
const columns = Math.max(1, Math.floor(width / 20))
return (
<box flexDirection="row" flexWrap="wrap">
{items.map((item, i) => (
<box key={i} width={`${100 / columns}%`} padding={1}>
<text>{item}</text>
</box>
))}
</box>
)
}
```
## Async Data Loading
### Loading States
```tsx
import { useState, useEffect } from "react"
function DataDisplay() {
const [data, setData] = useState<string[] | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function load() {
try {
const response = await fetch("https://api.example.com/data")
const json = await response.json()
setData(json.items)
} catch (e) {
setError(e instanceof Error ? e.message : "Unknown error")
} finally {
setLoading(false)
}
}
load()
}, [])
if (loading) {
return <text>Loading...</text>
}
if (error) {
return <text fg="red">Error: {error}</text>
}
return (
<box flexDirection="column">
{data?.map((item, i) => (
<text key={i}>{item}</text>
))}
</box>
)
}
```
## Animation Patterns
### Simple Animations
```tsx
import { useState, useEffect } from "react"
import { useTimeline } from "@opentui/react"
function ProgressBar() {
const [progress, setProgress] = useState(0)
const timeline = useTimeline({ duration: 3000 })
useEffect(() => {
timeline.add(
{ value: 0 },
{
value: 100,
duration: 3000,
ease: "linear",
onUpdate: (anim) => {
setProgress(Math.round(anim.targets[0].value))
},
}
)
}, [])
return (
<box flexDirection="column" gap={1}>
<text>Progress: {progress}%</text>
<box width={50} height={1} backgroundColor="#333">
<box
width={`${progress}%`}
height={1}
backgroundColor="#00ff00"
/>
</box>
</box>
)
}
```
### Interval-based Updates
```tsx
function Clock() {
const [time, setTime] = useState(new Date())
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date())
}, 1000)
return () => clearInterval(interval)
}, [])
return <text>{time.toLocaleTimeString()}</text>
}
```
## Component Composition
### Render Props
```tsx
function Focusable({
children
}: {
children: (focused: boolean) => React.ReactNode
}) {
const [focused, setFocused] = useState(false)
return (
<box
onMouseDown={() => setFocused(true)}
onMouseUp={() => setFocused(false)}
>
{children(focused)}
</box>
)
}
// Usage
<Focusable>
{(focused) => (
<text fg={focused ? "#00ff00" : "#ffffff"}>
{focused ? "Focused!" : "Click me"}
</text>
)}
</Focusable>
```
### Higher-Order Components
```tsx
function withBorder<P extends object>(
Component: React.ComponentType<P>,
borderStyle: string = "single"
) {
return function BorderedComponent(props: P) {
return (
<box border borderStyle={borderStyle} padding={1}>
<Component {...props} />
</box>
)
}
}
// Usage
const BorderedText = withBorder(({ content }: { content: string }) => (
<text>{content}</text>
))
<BorderedText content="Hello!" />
```
@@ -1,201 +0,0 @@
# OpenTUI Solid (@opentui/solid)
A SolidJS reconciler for building terminal user interfaces with fine-grained reactivity. Get optimal performance with Solid's signal-based approach.
## Overview
OpenTUI Solid provides:
- **Custom reconciler**: Solid components render to OpenTUI renderables
- **JSX intrinsics**: `<text>`, `<box>`, `<input>`, etc.
- **Hooks**: `useKeyboard`, `useRenderer`, `useTimeline`, etc.
- **Fine-grained reactivity**: Only what changes re-renders
- **Portal & Dynamic**: Advanced composition primitives
## When to Use Solid
Use the Solid reconciler when:
- You want optimal re-rendering performance
- You prefer signal-based reactivity
- You need fine-grained control over updates
- Building performance-critical applications
- You already know SolidJS
## When NOT to Use Solid
| Scenario | Use Instead |
|----------|-------------|
| Team knows React, not Solid | `@opentui/react` |
| Maximum control needed | `@opentui/core` |
| Smallest bundle size | `@opentui/core` |
| Building a framework/library | `@opentui/core` |
## Quick Start
```bash
bunx create-tui@latest -t solid my-app
cd my-app && bun install
```
The CLI creates the `my-app` directory for you - it must **not already exist**.
Options: `--no-git` (skip git init), `--no-install` (skip bun install)
**Agent guidance**: Always use autonomous mode with `-t <template>` flag. Never use interactive mode (`bunx create-tui@latest my-app` without `-t`) as it requires user prompts that agents cannot respond to.
Or manually:
```bash
bun install @opentui/solid @opentui/core solid-js
```
```tsx
import { render } from "@opentui/solid"
import { createSignal } from "solid-js"
function App() {
const [count, setCount] = createSignal(0)
return (
<box border padding={2}>
<text>Count: {count()}</text>
<box
border
onMouseDown={() => setCount(c => c + 1)}
>
<text>Click me!</text>
</box>
</box>
)
}
render(() => <App />)
```
## Core Concepts
### Signals
Solid uses signals for reactive state:
```tsx
import { createSignal, createEffect } from "solid-js"
function Counter() {
const [count, setCount] = createSignal(0)
// Effect runs when count changes
createEffect(() => {
console.log("Count is now:", count())
})
return <text>Count: {count()}</text>
}
```
### JSX Elements
Solid maps JSX intrinsic elements to OpenTUI renderables:
```tsx
// Note: Some use underscores (Solid convention)
<text>Hello</text> // TextRenderable
<box border>Content</box> // BoxRenderable
<input placeholder="..." /> // InputRenderable
<select options={[...]} /> // SelectRenderable
<tab_select /> // TabSelectRenderable (underscore!)
<ascii_font /> // ASCIIFontRenderable (underscore!)
<line_number /> // LineNumberRenderable (underscore!)
```
### Text Modifiers
Inside `<text>`, use modifier elements:
```tsx
<text>
<strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
<span fg="red">Colored text</span>
<br />
New line with <a href="https://example.com">link</a>
</text>
```
## Available Components
### Layout & Display
- `<text>` - Styled text content
- `<box>` - Container with borders and layout
- `<scrollbox>` - Scrollable container
- `<ascii_font>` - ASCII art text (note underscore)
### Input
- `<input>` - Single-line text input
- `<textarea>` - Multi-line text input
- `<select>` - List selection
- `<tab_select>` - Tab-based selection (note underscore)
### Code & Diff
- `<code>` - Syntax-highlighted code
- `<line_number>` - Code with line numbers (note underscore)
- `<diff>` - Unified or split diff viewer
### Text Modifiers (inside `<text>`)
- `<span>` - Inline styled text
- `<strong>`, `<b>` - Bold
- `<em>`, `<i>` - Italic
- `<u>` - Underline
- `<br>` - Line break
- `<a>` - Link
## Special Components
### Portal
Render children to a different mount node:
```tsx
import { Portal } from "@opentui/solid"
function Overlay() {
return (
<Portal mount={renderer.root}>
<box position="absolute" left={10} top={5} border>
<text>Overlay content</text>
</box>
</Portal>
)
}
```
### Dynamic
Render components dynamically:
```tsx
import { Dynamic } from "@opentui/solid"
function DynamicInput(props: { multiline: boolean }) {
return (
<Dynamic
component={props.multiline ? "textarea" : "input"}
placeholder="Enter text..."
/>
)
}
```
## In This Reference
- [Configuration](./configuration.md) - Project setup, tsconfig, bunfig, building
- [API](./api.md) - Components, hooks, render function
- [Patterns](./patterns.md) - Signals, stores, control flow, composition
- [Gotchas](./gotchas.md) - Common issues, debugging, limitations
## See Also
- [Core](../core/REFERENCE.md) - Underlying imperative API
- [React](../react/REFERENCE.md) - Alternative declarative approach
- [Components](../components/REFERENCE.md) - Component reference by category
- [Layout](../layout/REFERENCE.md) - Flexbox layout system
- [Keyboard](../keyboard/REFERENCE.md) - Input handling and shortcuts
- [Testing](../testing/REFERENCE.md) - Test renderer and snapshots
@@ -1,564 +0,0 @@
# Solid API Reference
## Rendering
### render(node, rendererOrConfig?)
Renders a Solid component tree into a CLI renderer.
```tsx
import { render } from "@opentui/solid"
// Simple usage - creates renderer automatically
render(() => <App />)
// With config
render(() => <App />, {
exitOnCtrlC: false,
targetFPS: 60,
})
// With existing renderer
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
render(() => <App />, renderer)
```
### testRender(node, options?)
Create a test renderer for snapshots and tests.
```tsx
import { testRender } from "@opentui/solid"
const testSetup = await testRender(() => <App />, {
width: 40,
height: 10,
})
// Access test utilities
testSetup.snapshot() // Get current render
testSetup.renderer // Access renderer
```
### extend(components)
Register custom renderables as JSX intrinsic elements.
```tsx
import { extend } from "@opentui/solid"
import { CustomRenderable } from "./custom"
extend({
custom: CustomRenderable,
})
// Now usable in JSX
<custom prop="value" />
```
### getComponentCatalogue()
Returns the current component catalogue.
```tsx
import { getComponentCatalogue } from "@opentui/solid"
const catalogue = getComponentCatalogue()
console.log(Object.keys(catalogue))
```
## Hooks
### useRenderer()
Access the OpenTUI renderer instance.
```tsx
import { useRenderer } from "@opentui/solid"
import { onMount } from "solid-js"
function App() {
const renderer = useRenderer()
onMount(() => {
console.log(`Terminal: ${renderer.width}x${renderer.height}`)
renderer.console.show()
// Access theme mode (dark/light based on terminal settings)
console.log(`Theme: ${renderer.themeMode}`) // "dark" | "light" | null
})
return <text>Hello</text>
}
// Listen for theme mode changes
function ThemedApp() {
const renderer = useRenderer()
const [theme, setTheme] = createSignal(renderer.themeMode ?? "dark")
onMount(() => {
renderer.on("theme_mode", (mode: "dark" | "light") => setTheme(mode))
})
return (
<box backgroundColor={theme() === "dark" ? "#1a1a2e" : "#ffffff"}>
<text fg={theme() === "dark" ? "#fff" : "#000"}>
Current theme: {theme()}
</text>
</box>
)
}
```
### useKeyboard(handler, options?)
Handle keyboard events.
```tsx
import { useKeyboard, useRenderer } from "@opentui/solid"
function App() {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "escape") {
renderer.destroy() // Never use process.exit() directly!
}
if (key.ctrl && key.name === "s") {
saveDocument()
}
})
return <text>Press ESC to exit</text>
}
// With release events
function GameControls() {
const [pressed, setPressed] = createSignal(new Set<string>())
useKeyboard(
(event) => {
setPressed(keys => {
const newKeys = new Set(keys)
if (event.eventType === "release") {
newKeys.delete(event.name)
} else {
newKeys.add(event.name)
}
return newKeys
})
},
{ release: true }
)
return <text>Pressed: {Array.from(pressed()).join(", ")}</text>
}
```
### usePaste(handler)
Handle paste events. Receives a `PasteEvent` with raw bytes.
```tsx
import { usePaste } from "@opentui/solid"
import { decodePasteBytes } from "@opentui/core"
function PasteHandler() {
usePaste((event) => {
const text = decodePasteBytes(event.bytes)
console.log("Pasted:", text)
})
return <text>Paste something</text>
}
```
### onResize(callback)
Handle terminal resize events.
```tsx
import { onResize } from "@opentui/solid"
function App() {
onResize((width, height) => {
console.log(`Resized to ${width}x${height}`)
})
return <text>Resize the terminal</text>
}
```
### useTerminalDimensions()
Get reactive terminal dimensions.
```tsx
import { useTerminalDimensions } from "@opentui/solid"
function ResponsiveLayout() {
const dimensions = useTerminalDimensions()
return (
<box flexDirection={dimensions().width > 80 ? "row" : "column"}>
<text>Width: {dimensions().width}</text>
<text>Height: {dimensions().height}</text>
</box>
)
}
```
### onFocus(callback) / onBlur(callback)
Handle terminal window focus and blur events. Solid-only hooks.
```tsx
import { onFocus, onBlur } from "@opentui/solid"
function App() {
onFocus(() => {
console.log("Terminal window gained focus")
})
onBlur(() => {
console.log("Terminal window lost focus")
})
return <text>Focus/blur tracking</text>
}
```
These hooks fire when the terminal emulator window gains or loses operating system focus. The renderer deduplicates events (won't re-emit the same focus state).
### useSelectionHandler(handler)
Handle text selection events. Fires when the user finishes a mouse selection (mouse-up). Solid-only hook - React does not have this.
```tsx
import { useSelectionHandler } from "@opentui/solid"
import type { Selection } from "@opentui/core"
function SelectableText() {
const [selected, setSelected] = createSignal("")
const renderer = useRenderer()
useSelectionHandler((selection: Selection) => {
const text = selection.getSelectedText()
if (text) {
setSelected(text)
renderer.copyToClipboardOSC52(text)
}
})
return (
<box flexDirection="column">
<text selectable>Select this text with your mouse</text>
<text fg="#888">Selected: {selected()}</text>
</box>
)
}
```
The `Selection` object aggregates selected text from all selectable renderables in the tree. See `keyboard/REFERENCE.md` (selection) for full details on the selection API and traversal model.
### useTimeline(options?)
Create animations with the timeline system.
```tsx
import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"
function AnimatedBox() {
const [width, setWidth] = createSignal(0)
const timeline = useTimeline({
duration: 2000,
loop: false,
})
onMount(() => {
timeline.add(
{ width: 0 },
{
width: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].width))
},
}
)
})
return <box style={{ width: width(), height: 3, backgroundColor: "#6a5acd" }} />
}
```
## Components
### Text Component
```tsx
<text
content="Hello" // Or use children
fg="#FFFFFF" // Foreground color
bg="#000000" // Background color
selectable={true} // Allow text selection
>
{/* Use nested modifier tags for styling */}
<span fg="red">Red</span>
<strong>Bold</strong>
<em>Italic</em>
<u>Underline</u>
<br />
<a href="https://...">Link</a>
</text>
```
> **Note**: Do NOT use `bold`, `italic`, `underline` as props on `<text>`. Use nested modifier tags like `<strong>`, `<em>`, `<u>` instead.
### Box Component
```tsx
<box
// Borders
border // Enable border
borderStyle="single" // single | double | rounded | bold
borderColor="#FFFFFF"
title="Title"
titleAlignment="center" // left | center | right
// Colors
backgroundColor="#1a1a2e"
// Layout
flexDirection="row"
justifyContent="center"
alignItems="center"
gap={2}
// Spacing
padding={2}
paddingX={2} // Horizontal (left + right)
paddingY={1} // Vertical (top + bottom)
margin={1}
marginX={2} // Horizontal (left + right)
marginY={1} // Vertical (top + bottom)
// Dimensions
width={40}
height={10}
flexGrow={1}
// Focus
focusable // Allow box to receive focus
focused={isFocused()} // Controlled focus state
// Events
onMouseDown={(e) => {}}
onMouseUp={(e) => {}}
>
{children}
</box>
```
### Scrollbox Component
```tsx
<scrollbox
focused // Enable keyboard scrolling
style={{
scrollbarOptions: {
showArrows: true,
trackOptions: {
foregroundColor: "#7aa2f7",
backgroundColor: "#414868",
},
},
}}
>
<For each={items()}>
{(item) => <text>{item}</text>}
</For>
</scrollbox>
```
### Input Component
```tsx
<input
value={value()}
onInput={(newValue) => setValue(newValue)}
placeholder="Enter text..."
focused
width={30}
/>
```
### Textarea Component
```tsx
<textarea
value={text()}
onInput={(newValue) => setText(newValue)}
placeholder="Enter multiple lines..."
focused
width={40}
height={10}
/>
```
### Select Component
```tsx
<select
options={[
{ name: "Option 1", description: "First", value: "1" },
{ name: "Option 2", description: "Second", value: "2" },
]}
onChange={(index, option) => setSelected(option)}
selectedIndex={0}
focused
/>
```
### Tab Select Component (Note: underscore)
```tsx
<tab_select
options={[
{ name: "Home", description: "Dashboard" },
{ name: "Settings", description: "Configuration" },
]}
onChange={(index, option) => setTab(option)}
tabWidth={20}
focused
/>
```
### ASCII Font Component (Note: underscore)
```tsx
<ascii_font
text="TITLE"
font="tiny" // tiny | block | slick | shade
color="#FFFFFF"
/>
```
### Code Component
```tsx
<code
code={sourceCode}
language="typescript"
/>
```
### Line Number Component (Note: underscore)
```tsx
<line_number
code={sourceCode}
language="typescript"
startLine={1}
highlightedLines={[5]}
/>
```
### Diff Component
```tsx
<diff
oldCode={originalCode}
newCode={modifiedCode}
language="typescript"
mode="unified" // unified | split
syncScroll // Sync scroll between split view panes
/>
```
## Control Flow
Solid's control flow components work with OpenTUI:
### For
```tsx
import { For } from "solid-js"
<For each={items()}>
{(item, index) => (
<box key={index()}>
<text>{item.name}</text>
</box>
)}
</For>
```
### Show
```tsx
import { Show } from "solid-js"
<Show when={isVisible()} fallback={<text>Hidden</text>}>
<text>Visible content</text>
</Show>
```
### Switch/Match
```tsx
import { Switch, Match } from "solid-js"
<Switch>
<Match when={status() === "loading"}>
<text>Loading...</text>
</Match>
<Match when={status() === "error"}>
<text fg="red">Error!</text>
</Match>
<Match when={status() === "success"}>
<text fg="green">Success!</text>
</Match>
</Switch>
```
### Index
```tsx
import { Index } from "solid-js"
<Index each={items()}>
{(item, index) => (
<text>{index}: {item().name}</text>
)}
</Index>
```
## Special Components
### Portal
```tsx
import { Portal } from "@opentui/solid"
<Portal mount={targetNode}>
<box>Portal content</box>
</Portal>
```
### Dynamic
```tsx
import { Dynamic } from "@opentui/solid"
<Dynamic
component={isMultiline() ? "textarea" : "input"}
placeholder="Enter text..."
focused
/>
```
@@ -1,316 +0,0 @@
# Solid Configuration
## Project Setup
### Quick Start
```bash
bunx create-tui@latest -t solid my-app
cd my-app && bun install
```
The CLI creates the `my-app` directory for you - it must **not already exist**.
Options: `--no-git` (skip git init), `--no-install` (skip bun install)
### Manual Setup
```bash
mkdir my-tui && cd my-tui
bun init
bun install @opentui/solid @opentui/core solid-js
```
## TypeScript Configuration
### tsconfig.json
```json
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "preserve",
"jsxImportSource": "@opentui/solid",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["bun-types"]
},
"include": ["src/**/*"]
}
```
**Critical settings:**
- `jsx: "preserve"` - Let Solid's compiler handle JSX
- `jsxImportSource: "@opentui/solid"` - Import JSX runtime from OpenTUI Solid
- `module` / `moduleResolution: "NodeNext"` - Recommended for OpenTUI compatibility
## Bun Configuration
### bunfig.toml
**Required** for the Solid compiler:
```toml
preload = ["@opentui/solid/preload"]
```
This loads the Solid JSX transform before your code runs.
## Package Configuration
### package.json
```json
{
"name": "my-tui-app",
"type": "module",
"scripts": {
"start": "bun run src/index.tsx",
"dev": "bun --watch run src/index.tsx",
"test": "bun test",
"build": "bun run build.ts"
},
"dependencies": {
"@opentui/core": "latest",
"@opentui/solid": "latest",
"solid-js": "latest"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "latest"
}
}
```
## Project Structure
Recommended structure:
```
my-tui-app/
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── Sidebar.tsx
│ │ └── MainContent.tsx
│ ├── stores/
│ │ └── appStore.ts
│ ├── App.tsx
│ └── index.tsx
├── bunfig.toml # Required!
├── package.json
└── tsconfig.json
```
### Entry Point (src/index.tsx)
```tsx
import { render } from "@opentui/solid"
import { App } from "./App"
render(() => <App />)
```
### App Component (src/App.tsx)
```tsx
import { Header } from "./components/Header"
import { Sidebar } from "./components/Sidebar"
import { MainContent } from "./components/MainContent"
export function App() {
return (
<box flexDirection="column" width="100%" height="100%">
<Header />
<box flexDirection="row" flexGrow={1}>
<Sidebar />
<MainContent />
</box>
</box>
)
}
```
## Renderer Configuration
### render() Options
```tsx
import { render } from "@opentui/solid"
import { ConsolePosition } from "@opentui/core"
render(() => <App />, {
// Rendering
targetFPS: 60,
// Behavior
exitOnCtrlC: true,
autoFocus: true, // Auto-focus elements on click (default: true)
useMouse: true, // Enable mouse support (default: true)
// Debug console
consoleOptions: {
position: ConsolePosition.BOTTOM,
sizePercent: 30,
startInDebugMode: false,
},
// Cleanup
onDestroy: () => {
// Cleanup code
},
})
```
### Using Existing Renderer
```tsx
import { render } from "@opentui/solid"
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: false,
})
render(() => <App />, renderer)
```
## Building for Distribution
### Build Script (build.ts)
```typescript
import solidPlugin from "@opentui/solid/bun-plugin"
await Bun.build({
entrypoints: ["./src/index.tsx"],
outdir: "./dist",
target: "bun",
minify: true,
plugins: [solidPlugin],
})
console.log("Build complete!")
```
Run: `bun run build.ts`
### Creating Executables
```typescript
import solidPlugin from "@opentui/solid/bun-plugin"
await Bun.build({
entrypoints: ["./src/index.tsx"],
target: "bun",
plugins: [solidPlugin],
compile: {
target: "bun-darwin-arm64", // or bun-linux-x64, etc.
outfile: "my-app",
},
})
```
**Available targets:**
- `bun-darwin-arm64` - macOS Apple Silicon
- `bun-darwin-x64` - macOS Intel
- `bun-linux-x64` - Linux x64
- `bun-linux-arm64` - Linux ARM64
- `bun-windows-x64` - Windows x64
## Environment Variables
Create `.env` for development:
```env
# Debug settings
OTUI_SHOW_STATS=false
SHOW_CONSOLE=false
# App settings
API_URL=https://api.example.com
```
Bun auto-loads `.env` files:
```tsx
const apiUrl = process.env.API_URL
```
## Testing Configuration
### Test Setup
```typescript
// src/test-utils.tsx
import { testRender } from "@opentui/solid"
export async function renderForTest(
Component: () => JSX.Element,
options = { width: 80, height: 24 }
) {
return await testRender(Component, options)
}
```
### Test Example
```typescript
// src/components/Counter.test.tsx
import { test, expect } from "bun:test"
import { renderForTest } from "../test-utils"
import { Counter } from "./Counter"
test("Counter renders initial value", async () => {
const { snapshot } = await renderForTest(() => <Counter initialValue={5} />)
expect(snapshot()).toContain("Count: 5")
})
```
## Common Configuration Issues
### Missing bunfig.toml
**Symptom**: JSX not transformed, syntax errors
**Fix**: Create `bunfig.toml` with preload:
```toml
preload = ["@opentui/solid/preload"]
```
### Wrong JSX Settings
**Symptom**: JSX compiles to React calls
**Fix**: Ensure tsconfig has:
```json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@opentui/solid"
}
}
```
### Build Missing Plugin
**Symptom**: Built output has untransformed JSX
**Fix**: Add Solid plugin to build:
```typescript
import solidPlugin from "@opentui/solid/bun-plugin"
await Bun.build({
// ...
plugins: [solidPlugin],
})
```
@@ -1,427 +0,0 @@
# Solid Gotchas
## Critical
### Never use `process.exit()` directly
**This is the most common mistake.** Using `process.exit()` leaves the terminal in a broken state (cursor hidden, raw mode, alternate screen).
```tsx
// WRONG - Terminal left in broken state
process.exit(0)
// CORRECT - Use renderer.destroy()
import { useRenderer } from "@opentui/solid"
function App() {
const renderer = useRenderer()
const handleExit = () => {
renderer.destroy() // Cleans up and exits properly
}
}
```
`renderer.destroy()` restores the terminal (exits alternate screen, restores cursor, etc.) before exiting.
## Configuration Issues
### Missing bunfig.toml
**Symptom**: JSX syntax errors, components not rendering
```
SyntaxError: Unexpected token '<'
```
**Fix**: Create `bunfig.toml` in project root:
```toml
preload = ["@opentui/solid/preload"]
```
### Wrong JSX Settings
**Symptom**: JSX compiles to React, errors about React not found
**Fix**: Ensure tsconfig.json has:
```json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@opentui/solid"
}
}
```
### Build Without Plugin
**Symptom**: Built bundle has raw JSX
**Fix**: Add Solid plugin to build:
```typescript
import solidPlugin from "@opentui/solid/bun-plugin"
await Bun.build({
// ...
plugins: [solidPlugin],
})
```
## Reactivity Issues
### Accessing Signals Without Calling
**Symptom**: Value never updates, shows `[Function]`
```tsx
// WRONG - Missing ()
const [count, setCount] = createSignal(0)
<text>Count: {count}</text> // Shows [Function]
// CORRECT
<text>Count: {count()}</text>
```
### Breaking Reactivity with Destructuring
**Symptom**: Props stop being reactive
```tsx
// WRONG - Breaks reactivity
function Component(props: { value: number }) {
const { value } = props // Destructured once, never updates!
return <text>{value}</text>
}
// CORRECT - Keep props reactive
function Component(props: { value: number }) {
return <text>{props.value}</text>
}
// OR use splitProps
function Component(props: { value: number; other: string }) {
const [local, rest] = splitProps(props, ["value"])
return <text>{local.value}</text>
}
```
### Effects Not Running
**Symptom**: createEffect doesn't trigger
```tsx
// WRONG - Signal not accessed in effect
const [count, setCount] = createSignal(0)
createEffect(() => {
console.log("Count changed") // Never runs after initial!
})
// CORRECT - Access the signal
createEffect(() => {
console.log("Count:", count()) // Runs when count changes
})
```
## HTML Entity Decoding
Solid's reconciler automatically decodes HTML entities in JSX text content. This means `&lt;`, `&gt;`, `&amp;`, etc. render as their literal characters:
```tsx
// These render correctly in Solid
<text>Use &lt;box&gt; for containers</text> // Displays: Use <box> for containers
<text>A &amp; B</text> // Displays: A & B
```
This applies to text nodes, the `content` prop, and the `text` prop.
## Component Naming
### Underscore vs Hyphen
Solid uses underscores for multi-word component names:
```tsx
// WRONG - React-style naming
<tab-select /> // Error!
<ascii-font /> // Error!
<line-number /> // Error!
// CORRECT - Solid naming
<tab_select />
<ascii_font />
<line_number />
```
**Component mapping:**
| Concept | React | Solid |
|---------|-------|-------|
| Tab Select | `<tab-select>` | `<tab_select>` |
| ASCII Font | `<ascii-font>` | `<ascii_font>` |
| Line Number | `<line-number>` | `<line_number>` |
## Focus Issues
### Focus Not Working
Components need explicit focus:
```tsx
// WRONG
<input placeholder="Type here..." />
// CORRECT
<input placeholder="Type here..." focused />
```
### Select Not Responding
```tsx
// WRONG
<select options={["a", "b"]} />
// CORRECT
<select
options={[
{ name: "A", description: "Option A", value: "a" },
{ name: "B", description: "Option B", value: "b" },
]}
onSelect={(index, option) => {
// Called when Enter is pressed
console.log("Selected:", option.name)
}}
focused
/>
```
### Select Events Confusion
Remember: `onSelect` fires on Enter (selection confirmed), `onChange` fires on navigation:
```tsx
// WRONG - expecting onChange to fire on Enter
<select
options={options()}
onChange={(i, opt) => submitForm(opt)} // This fires on arrow keys!
/>
// CORRECT
<select
options={options()}
onSelect={(i, opt) => submitForm(opt)} // Enter pressed - submit
onChange={(i, opt) => showPreview(opt)} // Arrow keys - preview
/>
```
## Control Flow Issues
### For vs Index
Use `For` for arrays of objects, `Index` for primitives:
```tsx
// For objects - item is reactive
<For each={objects()}>
{(obj) => <text>{obj.name}</text>}
</For>
// For primitives - use Index, item() is reactive
<Index each={strings()}>
{(str, index) => <text>{index}: {str()}</text>}
</Index>
```
### Missing Fallback
Show requires fallback for proper rendering:
```tsx
// May cause issues
<Show when={data()}>
<Component />
</Show>
// Better - explicit fallback
<Show when={data()} fallback={<text>Loading...</text>}>
<Component />
</Show>
```
## Cleanup Issues
### Forgetting onCleanup
**Symptom**: Memory leaks, multiple intervals running
```tsx
// WRONG - Interval never cleared
function Timer() {
const [time, setTime] = createSignal(0)
setInterval(() => setTime(t => t + 1), 1000)
return <text>{time()}</text>
}
// CORRECT
function Timer() {
const [time, setTime] = createSignal(0)
const interval = setInterval(() => setTime(t => t + 1), 1000)
onCleanup(() => clearInterval(interval))
return <text>{time()}</text>
}
```
### Effect Cleanup
```tsx
createEffect(() => {
const subscription = subscribe(data())
// WRONG - No cleanup
// subscription stays active
// CORRECT
onCleanup(() => subscription.unsubscribe())
})
```
## Store Issues
### Mutating Store Directly
**Symptom**: Changes don't trigger updates
```tsx
const [state, setState] = createStore({ items: [] })
// WRONG - Direct mutation
state.items.push(newItem) // Won't trigger updates!
// CORRECT - Use setState
setState("items", items => [...items, newItem])
```
### Nested Updates
```tsx
const [state, setState] = createStore({
user: { profile: { name: "John" } }
})
// WRONG
state.user.profile.name = "Jane"
// CORRECT
setState("user", "profile", "name", "Jane")
```
## Debugging
### Console Not Visible
OpenTUI captures console output:
```tsx
import { useRenderer } from "@opentui/solid"
import { onMount } from "solid-js"
function App() {
const renderer = useRenderer()
onMount(() => {
renderer.console.show()
console.log("Now visible!")
})
return <box>{/* ... */}</box>
}
```
### Tracking Reactivity
Use `createEffect` to debug:
```tsx
createEffect(() => {
console.log("State:", {
count: count(),
items: items(),
})
})
```
## Runtime Issues
### Use Bun
```bash
# WRONG
node src/index.tsx
npm run start
# CORRECT
bun run src/index.tsx
bun run start
```
### Async render()
The render function is async when creating a renderer:
```tsx
// This is fine - Bun supports top-level await
render(() => <App />)
// If you need the renderer
import { createCliRenderer } from "@opentui/core"
import { render } from "@opentui/solid"
const renderer = await createCliRenderer()
render(() => <App />, renderer)
```
## Common Error Messages
### "Cannot read properties of undefined"
Usually a missing reactive access:
```tsx
// Check if signal is being called
<text>{count()}</text> // Note the ()
// Check if props are being accessed correctly
<text>{props.value}</text> // Not destructured
```
### "JSX element has no corresponding closing tag"
Check component naming:
```tsx
// Wrong
<tab-select></tab-select>
// Correct
<tab_select></tab_select>
```
### "store is not a function"
Stores aren't called like signals:
```tsx
const [store, setStore] = createStore({ count: 0 })
// WRONG
<text>{store().count}</text>
// CORRECT
<text>{store.count}</text>
```
@@ -1,560 +0,0 @@
# Solid Patterns
## Reactive State
### Signals
Basic reactive state with signals:
```tsx
import { createSignal } from "solid-js"
function Counter() {
const [count, setCount] = createSignal(0)
return (
<box flexDirection="row" gap={2}>
<text>Count: {count()}</text>
<box border onMouseDown={() => setCount(c => c - 1)}>
<text>-</text>
</box>
<box border onMouseDown={() => setCount(c => c + 1)}>
<text>+</text>
</box>
</box>
)
}
```
### Derived State
Compute values from signals:
```tsx
import { createSignal, createMemo } from "solid-js"
function PriceCalculator() {
const [quantity, setQuantity] = createSignal(1)
const [price, setPrice] = createSignal(9.99)
// Derived value - only recalculates when dependencies change
const total = createMemo(() => quantity() * price())
const formatted = createMemo(() => `$${total().toFixed(2)}`)
return (
<box flexDirection="column">
<text>Quantity: {quantity()}</text>
<text>Price: ${price()}</text>
<text>Total: {formatted()}</text>
</box>
)
}
```
### Effects
React to state changes:
```tsx
import { createSignal, createEffect, onCleanup } from "solid-js"
function AutoSave() {
const [content, setContent] = createSignal("")
createEffect(() => {
const text = content()
// Debounced save
const timeout = setTimeout(() => {
saveToFile(text)
}, 1000)
// Cleanup on next run or disposal
onCleanup(() => clearTimeout(timeout))
})
return (
<textarea
value={content()}
onInput={setContent}
placeholder="Auto-saves after 1 second..."
/>
)
}
```
## Stores
### createStore for Complex State
```tsx
import { createStore } from "solid-js/store"
interface AppState {
user: { name: string; email: string } | null
items: Array<{ id: number; name: string; done: boolean }>
settings: { theme: "dark" | "light" }
}
function App() {
const [state, setState] = createStore<AppState>({
user: null,
items: [],
settings: { theme: "dark" },
})
const addItem = (name: string) => {
setState("items", items => [
...items,
{ id: Date.now(), name, done: false }
])
}
const toggleItem = (id: number) => {
setState("items", item => item.id === id, "done", done => !done)
}
const setTheme = (theme: "dark" | "light") => {
setState("settings", "theme", theme)
}
return (
<box backgroundColor={state.settings.theme === "dark" ? "#1a1a2e" : "#f0f0f0"}>
<For each={state.items}>
{(item) => (
<text
fg={item.done ? "#888" : "#fff"}
onMouseDown={() => toggleItem(item.id)}
>
{item.done ? "[x]" : "[ ]"} {item.name}
</text>
)}
</For>
</box>
)
}
```
### Store with Context
Share state across components:
```tsx
import { createStore } from "solid-js/store"
import { createContext, useContext, ParentComponent } from "solid-js"
interface Store {
count: number
items: string[]
}
type StoreContextValue = [
Store,
{
increment: () => void
addItem: (item: string) => void
}
]
const StoreContext = createContext<StoreContextValue>()
const StoreProvider: ParentComponent = (props) => {
const [state, setState] = createStore<Store>({
count: 0,
items: [],
})
const actions = {
increment: () => setState("count", c => c + 1),
addItem: (item: string) => setState("items", i => [...i, item]),
}
return (
<StoreContext.Provider value={[state, actions]}>
{props.children}
</StoreContext.Provider>
)
}
function useStore() {
const context = useContext(StoreContext)
if (!context) throw new Error("useStore must be used within StoreProvider")
return context
}
// Usage
function Counter() {
const [state, { increment }] = useStore()
return (
<box onMouseDown={increment}>
<text>Count: {state.count}</text>
</box>
)
}
```
## Control Flow
### Conditional Rendering with Show
```tsx
import { Show, createSignal } from "solid-js"
function ToggleableContent() {
const [visible, setVisible] = createSignal(false)
return (
<box flexDirection="column">
<box border onMouseDown={() => setVisible(v => !v)}>
<text>Toggle</text>
</box>
<Show
when={visible()}
fallback={<text fg="#888">Content is hidden</text>}
>
<text fg="#0f0">Content is visible!</text>
</Show>
</box>
)
}
```
### Lists with For
```tsx
import { For, createSignal } from "solid-js"
function TodoList() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "Learn Solid", done: false },
{ id: 2, text: "Build TUI", done: false },
])
const toggle = (id: number) => {
setTodos(todos =>
todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
)
)
}
return (
<box flexDirection="column">
<For each={todos()}>
{(todo) => (
<box onMouseDown={() => toggle(todo.id)}>
<text fg={todo.done ? "#888" : "#fff"}>
{todo.done ? "[x]" : "[ ]"} {todo.text}
</text>
</box>
)}
</For>
</box>
)
}
```
### Index for Primitive Arrays
Use `Index` when array items are primitives:
```tsx
import { Index, createSignal } from "solid-js"
function StringList() {
const [items, setItems] = createSignal(["apple", "banana", "cherry"])
return (
<box flexDirection="column">
<Index each={items()}>
{(item, index) => (
<text>{index}: {item()}</text>
)}
</Index>
</box>
)
}
```
### Switch/Match for Multiple Conditions
```tsx
import { Switch, Match, createSignal } from "solid-js"
type Status = "idle" | "loading" | "success" | "error"
function StatusDisplay() {
const [status, setStatus] = createSignal<Status>("idle")
return (
<Switch>
<Match when={status() === "idle"}>
<text>Ready</text>
</Match>
<Match when={status() === "loading"}>
<text fg="#ff0">Loading...</text>
</Match>
<Match when={status() === "success"}>
<text fg="#0f0">Success!</text>
</Match>
<Match when={status() === "error"}>
<text fg="#f00">Error occurred</text>
</Match>
</Switch>
)
}
```
## Focus Management
### Focus State
```tsx
import { createSignal } from "solid-js"
import { useKeyboard } from "@opentui/solid"
function FocusableForm() {
const [focusIndex, setFocusIndex] = createSignal(0)
const fields = ["name", "email", "message"]
useKeyboard((key) => {
if (key.name === "tab") {
setFocusIndex(i => (i + 1) % fields.length)
}
if (key.shift && key.name === "tab") {
setFocusIndex(i => (i - 1 + fields.length) % fields.length)
}
})
return (
<box flexDirection="column" gap={1}>
<Index each={fields}>
{(field, i) => (
<input
placeholder={`Enter ${field()}...`}
focused={i === focusIndex()}
/>
)}
</Index>
</box>
)
}
```
## Keyboard Navigation
### Global Shortcuts
```tsx
import { useKeyboard } from "@opentui/solid"
function App() {
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "escape") {
renderer.destroy() // Never use process.exit() directly!
}
if (key.ctrl && key.name === "s") {
save()
}
// Vim-style
if (key.name === "j") moveDown()
if (key.name === "k") moveUp()
})
return <box>{/* ... */}</box>
}
```
## Responsive Design
### Terminal-size Responsive
```tsx
import { useTerminalDimensions } from "@opentui/solid"
function ResponsiveLayout() {
const dims = useTerminalDimensions()
return (
<box flexDirection={dims().width > 80 ? "row" : "column"}>
<box flexGrow={1}>
<text>Panel 1</text>
</box>
<box flexGrow={1}>
<text>Panel 2</text>
</box>
</box>
)
}
```
## Async Data
### Resources
```tsx
import { createResource, Suspense } from "solid-js"
async function fetchData() {
const response = await fetch("https://api.example.com/data")
return response.json()
}
function DataDisplay() {
const [data] = createResource(fetchData)
return (
<Suspense fallback={<text>Loading...</text>}>
<Show when={data()}>
{(items) => (
<For each={items()}>
{(item) => <text>{item.name}</text>}
</For>
)}
</Show>
</Suspense>
)
}
```
### Error Handling
```tsx
import { createResource, Show, ErrorBoundary } from "solid-js"
function SafeDataDisplay() {
const [data] = createResource(fetchData)
return (
<ErrorBoundary fallback={(err) => <text fg="red">Error: {err.message}</text>}>
<Show
when={!data.loading}
fallback={<text>Loading...</text>}
>
<Show
when={!data.error}
fallback={<text fg="red">Failed to load</text>}
>
<For each={data()}>
{(item) => <text>{item.name}</text>}
</For>
</Show>
</Show>
</ErrorBoundary>
)
}
```
## Component Composition
### Props and Children
```tsx
import { ParentComponent, JSX } from "solid-js"
interface PanelProps {
title: string
children: JSX.Element
}
const Panel: ParentComponent<{ title: string }> = (props) => {
return (
<box border padding={1} flexDirection="column">
<text fg="#0ff">{props.title}</text>
<box marginTop={1}>
{props.children}
</box>
</box>
)
}
// Usage
<Panel title="Settings">
<text>Panel content here</text>
</Panel>
```
### Spread Props
```tsx
import { splitProps } from "solid-js"
interface ButtonProps {
label: string
onClick: () => void
// ...rest goes to box
}
function Button(props: ButtonProps) {
const [local, rest] = splitProps(props, ["label", "onClick"])
return (
<box border onMouseDown={local.onClick} {...rest}>
<text>{local.label}</text>
</box>
)
}
```
## Animation
### With Timeline
```tsx
import { createSignal, onMount } from "solid-js"
import { useTimeline } from "@opentui/solid"
function AnimatedProgress() {
const [width, setWidth] = createSignal(0)
const timeline = useTimeline({
duration: 2000,
})
onMount(() => {
timeline.add(
{ value: 0 },
{
value: 50,
duration: 2000,
ease: "easeOutQuad",
onUpdate: (anim) => {
setWidth(Math.round(anim.targets[0].value))
},
}
)
})
return (
<box flexDirection="column" gap={1}>
<text>Progress: {width()}%</text>
<box width={50} height={1} backgroundColor="#333">
<box width={width()} height={1} backgroundColor="#0f0" />
</box>
</box>
)
}
```
### Interval-based
```tsx
import { createSignal, onCleanup } from "solid-js"
function Clock() {
const [time, setTime] = createSignal(new Date())
const interval = setInterval(() => {
setTime(new Date())
}, 1000)
onCleanup(() => clearInterval(interval))
return <text>{time().toLocaleTimeString()}</text>
}
```
@@ -1,614 +0,0 @@
# Testing OpenTUI Applications
How to test terminal user interfaces built with OpenTUI.
## Overview
OpenTUI provides:
- **Test Renderer**: Headless renderer for testing
- **Snapshot Testing**: Verify visual output
- **Interaction Testing**: Simulate user input
## When to Use
Use this reference when you need snapshot tests, interaction testing, or renderer-based regression checks.
## Test Setup
### Bun Test Runner
OpenTUI uses Bun's built-in test runner:
```typescript
import { test, expect, beforeEach, afterEach } from "bun:test"
```
### Test Renderer
Create a test renderer for headless testing:
```typescript
import { createTestRenderer } from "@opentui/core/testing"
const testSetup = await createTestRenderer({
width: 80, // Terminal width
height: 24, // Terminal height
})
```
## Core Testing
### Basic Test
```typescript
import { test, expect } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { TextRenderable } from "@opentui/core"
test("renders text", async () => {
const testSetup = await createTestRenderer({
width: 40,
height: 10,
})
const text = new TextRenderable(testSetup.renderer, {
id: "greeting",
content: "Hello, World!",
})
testSetup.renderer.root.add(text)
await testSetup.renderOnce()
expect(testSetup.captureCharFrame()).toContain("Hello, World!")
})
```
### Snapshot Testing
```typescript
import { test, expect, afterEach } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { BoxRenderable, TextRenderable } from "@opentui/core"
let testSetup: Awaited<ReturnType<typeof createTestRenderer>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("component matches snapshot", async () => {
testSetup = await createTestRenderer({
width: 40,
height: 10,
})
const box = new BoxRenderable(testSetup.renderer, {
id: "box",
border: true,
width: 20,
height: 5,
})
box.add(new TextRenderable(testSetup.renderer, {
content: "Content",
}))
testSetup.renderer.root.add(box)
await testSetup.renderOnce()
expect(testSetup.captureCharFrame()).toMatchSnapshot()
})
```
## React Testing
### Test Utilities
React provides a built-in `testRender` utility via the `@opentui/react/test-utils` subpath export:
```tsx
import { testRender } from "@opentui/react/test-utils"
```
This utility:
- Creates a headless test renderer
- Sets up the React Act environment automatically
- Handles proper unmounting on destroy
- Returns the standard test setup object
### Basic Component Test
```tsx
import { test, expect } from "bun:test"
import { testRender } from "@opentui/react/test-utils"
function Greeting({ name }: { name: string }) {
return <text>Hello, {name}!</text>
}
test("Greeting renders name", async () => {
const testSetup = await testRender(
<Greeting name="World" />,
{ width: 80, height: 24 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toContain("Hello, World!")
})
```
### Snapshot Testing
```tsx
import { test, expect, afterEach } from "bun:test"
import { testRender } from "@opentui/react/test-utils"
let testSetup: Awaited<ReturnType<typeof testRender>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("component matches snapshot", async () => {
testSetup = await testRender(
<box style={{ width: 20, height: 5, border: true }}>
<text>Content</text>
</box>,
{ width: 25, height: 8 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toMatchSnapshot()
})
```
### State Testing
```tsx
import { test, expect, afterEach } from "bun:test"
import { useState } from "react"
import { testRender } from "@opentui/react/test-utils"
let testSetup: Awaited<ReturnType<typeof testRender>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
function Counter() {
const [count, setCount] = useState(0)
return (
<box>
<text>Count: {count}</text>
</box>
)
}
test("Counter shows initial value", async () => {
testSetup = await testRender(
<Counter />,
{ width: 20, height: 5 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toContain("Count: 0")
})
```
### Test Setup/Teardown Pattern
For multiple tests, use beforeEach/afterEach to manage the renderer lifecycle:
```tsx
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { testRender } from "@opentui/react/test-utils"
let testSetup: Awaited<ReturnType<typeof testRender>>
describe("MyComponent", () => {
beforeEach(async () => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("renders correctly", async () => {
testSetup = await testRender(<MyComponent />, {
width: 40,
height: 10,
})
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toMatchSnapshot()
})
})
```
### Test Setup Return Object
The `testRender` function returns a test setup object with these properties:
| Property | Type | Description |
|----------|------|-------------|
| `renderer` | `Renderer` | The headless renderer instance |
| `renderOnce` | `() => Promise<void>` | Triggers a single render cycle |
| `captureCharFrame` | `() => string` | Captures current output as text |
| `resize` | `(width, height) => void` | Resize the virtual terminal |
## Solid Testing
### Test Utilities
Solid exports `testRender` directly from the main package:
```tsx
import { testRender } from "@opentui/solid"
```
Note: Unlike React, Solid's `testRender` takes a **function component** (not a JSX element).
### Basic Component Test
```tsx
import { test, expect } from "bun:test"
import { testRender } from "@opentui/solid"
function Greeting(props: { name: string }) {
return <text>Hello, {props.name}!</text>
}
test("Greeting renders name", async () => {
const testSetup = await testRender(
() => <Greeting name="World" />,
{ width: 80, height: 24 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toContain("Hello, World!")
})
```
### Snapshot Testing
```tsx
import { test, expect, afterEach } from "bun:test"
import { testRender } from "@opentui/solid"
let testSetup: Awaited<ReturnType<typeof testRender>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("component matches snapshot", async () => {
testSetup = await testRender(
() => (
<box style={{ width: 20, height: 5, border: true }}>
<text>Content</text>
</box>
),
{ width: 25, height: 8 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toMatchSnapshot()
})
```
## Snapshot Format
Snapshots capture the rendered terminal output as text:
```
┌──────────────────┐
│ Hello, World! │
│ │
└──────────────────┘
```
### Updating Snapshots
```bash
bun test --update-snapshots
```
## Interaction Testing
### Simulating Key Presses
```typescript
import { test, expect, afterEach } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
let testSetup: Awaited<ReturnType<typeof createTestRenderer>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("responds to keyboard", async () => {
testSetup = await createTestRenderer({
width: 40,
height: 10,
})
// Create component that responds to keys
// ...
// Simulate keypress
testSetup.renderer.keyInput.emit("keypress", {
name: "enter",
sequence: "\r",
ctrl: false,
shift: false,
meta: false,
option: false,
eventType: "press",
repeated: false,
})
// Render after the keypress
await testSetup.renderOnce()
expect(testSetup.captureCharFrame()).toContain("Selected")
})
```
### Testing Focus
```typescript
import { test, expect, afterEach } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { InputRenderable } from "@opentui/core"
let testSetup: Awaited<ReturnType<typeof createTestRenderer>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("input receives focus", async () => {
testSetup = await createTestRenderer({
width: 40,
height: 10,
})
const input = new InputRenderable(testSetup.renderer, {
id: "test-input",
placeholder: "Type here",
})
testSetup.renderer.root.add(input)
input.focus()
expect(input.isFocused()).toBe(true)
})
```
## Test Organization
### File Structure
```
src/
├── components/
│ ├── Button.tsx
│ └── Button.test.tsx
├── hooks/
│ ├── useCounter.ts
│ └── useCounter.test.ts
└── test-utils.tsx
```
### Running Tests
```bash
# Run all tests
bun test
# Run specific test file
bun test src/components/Button.test.tsx
# Run with filter
bun test --filter "Button"
# Watch mode
bun test --watch
```
## Patterns
### Testing Conditional Rendering (React)
```tsx
import { test, expect, afterEach } from "bun:test"
import { testRender } from "@opentui/react/test-utils"
let testSetup: Awaited<ReturnType<typeof testRender>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("shows loading state", async () => {
testSetup = await testRender(
<DataLoader loading={true} />,
{ width: 40, height: 10 }
)
await testSetup.renderOnce()
expect(testSetup.captureCharFrame()).toContain("Loading...")
})
test("shows data when loaded", async () => {
testSetup = await testRender(
<DataLoader loading={false} data={["Item 1", "Item 2"]} />,
{ width: 40, height: 10 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
expect(frame).toContain("Item 1")
expect(frame).toContain("Item 2")
})
```
### Testing Lists
```tsx
test("renders all items", async () => {
const items = ["Apple", "Banana", "Cherry"]
testSetup = await testRender(
<ItemList items={items} />,
{ width: 40, height: 10 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
items.forEach(item => {
expect(frame).toContain(item)
})
})
```
### Testing Layouts
```tsx
test("matches layout snapshot", async () => {
testSetup = await testRender(
<AppLayout />,
{ width: 120, height: 40 } // Larger viewport
)
await testSetup.renderOnce()
expect(testSetup.captureCharFrame()).toMatchSnapshot()
})
```
## Debugging Tests
### Print Frame Output
```tsx
import { testRender } from "@opentui/react/test-utils"
test("debug output", async () => {
const testSetup = await testRender(
<MyComponent />,
{ width: 40, height: 10 }
)
await testSetup.renderOnce()
const frame = testSetup.captureCharFrame()
// Print to see what's rendered
console.log(frame)
expect(frame).toContain("expected")
})
```
### Verbose Mode
```bash
bun test --verbose
```
## Gotchas
### Async Rendering
Always call `renderOnce()` after setting up your component to ensure rendering is complete:
```typescript
const testSetup = await testRender(<MyComponent />, { width: 40, height: 10 })
await testSetup.renderOnce() // Required before capturing frame
const frame = testSetup.captureCharFrame()
```
### Test Isolation and Cleanup
Always destroy the renderer after each test to avoid resource leaks:
```typescript
import { afterEach } from "bun:test"
let testSetup: Awaited<ReturnType<typeof testRender>>
afterEach(() => {
if (testSetup) {
testSetup.renderer.destroy()
}
})
test("test 1", async () => {
testSetup = await testRender(<Component1 />, { width: 40, height: 10 })
// ...
})
test("test 2", async () => {
testSetup = await testRender(<Component2 />, { width: 40, height: 10 })
// ...
})
```
### Snapshot Dimensions
Be consistent with test dimensions for stable snapshots:
```typescript
const testSetup = await createTestRenderer({
width: 80, // Standard width
height: 24, // Standard height
})
```
### Running from Package Directory
Run tests from the package directory:
```bash
cd packages/core
bun test
# Not from repo root for package-specific tests
```
## See Also
- [Core API](../core/api.md) - `createTestRenderer` and renderable classes
- [React Configuration](../react/configuration.md) - React test setup
- [Solid Configuration](../solid/configuration.md) - Solid test setup
- [Keyboard](../keyboard/REFERENCE.md) - Simulating key events in tests
-1
View File
@@ -1 +0,0 @@
../../.cline/skills/publish-cli
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: use correct base URL for Vertex AI global endpoint with Claude models
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/cline-sdk
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/opentui
-1
View File
@@ -1 +0,0 @@
../../.cline/skills/publish-cli
-266
View File
@@ -1,266 +0,0 @@
---
name: publish-cli
description: Use when preparing, tagging, and publishing an apps/cli npm release. Guides changelog drafting, apps/cli/package.json version bumps, cli-vX.Y.Z tags, local npm publishing, and the publish-cli GitHub workflow.
---
# CLI Release
Use this skill when the user asks to release the CLI, publish `cline`, bump the CLI version, draft release notes, create a `cli-vX.Y.Z` tag, or trigger the CLI publish workflow.
The CLI is npm-only. Do not add alternate distribution or signing steps.
> Working directory: run every command below from the repository root. Paths and scripts (e.g. `apps/cli/package.json`, `sdk/packages/`, `bun release cli`, `bun run version`) are written relative to the repo root.
The skill should guide the user through one release preparation flow, then offer the publish path options. The two normal publish paths are GitHub Actions and local publishing from an authenticated machine.
## Release contract
- SDK prerequisite: the CLI depends on the SDK via `workspace:*` (`@cline/core`, `@cline/shared`, and friends). If the SDK changed since its last release, release the SDK first and wait for it to finish publishing before releasing the CLI. See "Step 0: Release the SDK first if it changed" below.
- Version source: `apps/cli/package.json`.
- Main release tag: `cli-vX.Y.Z`, where `X.Y.Z` matches `apps/cli/package.json`.
- Nightly release version: `X.Y.Z-nightly.TIMESTAMP`.
- Release prep includes approved release notes, a version bump, and an `apps/cli/CHANGELOG.md` update.
- Publish paths:
- GitHub workflow: `.github/workflows/cli-publish.yml`.
- Local publish helper: `bun release cli`.
- npm dist-tags and git tags are separate. `--tag latest` and `--tag nightly` are npm registry channels. `cli-vX.Y.Z` is a git tag for source history and GitHub releases.
- The GitHub main release workflow runs from `main`, requires an existing `cli-vX.Y.Z` tag, checks out that tag, and publishes from it.
- The GitHub nightly workflow publishes to npm with the `nightly` dist-tag and does not create a tag.
- The local release helper requires a clean checkout and `cli-vX.Y.Z` to point at `HEAD` locally and on `origin` before publishing.
- Local GitHub release creation requires `gh` to be authenticated with release permissions for the repo.
- Always ask before pushing commits or tags.
- Do not amend commits unless explicitly requested.
## Step 0: Release the SDK first if it changed
Do this before anything else in the Workflow below.
The CLI builds and ships against the SDK source in the monorepo (`workspace:*` for `@cline/core`, `@cline/shared`, and the rest), so a CLI release always contains the latest SDK code whether or not the SDK was released. The build and tests use that source too, not anything from npm. Releasing the SDK alongside the CLI is still worth doing for two reasons:
- Hub freshness. The hub daemon lives in `@cline/core` and stamps a `buildId` that defaults to the `@cline/core` package version (`resolveHubBuildId` in `sdk/packages/core/src/hub/discovery/index.ts`). A running hub is only retired and respawned when that `buildId` changes (`isCompatibleHubRecord` / `retireIncompatibleHub` in `sdk/packages/core/src/hub/daemon/index.ts`). So if the SDK code changed but the version did not, a user who upgrades the CLI keeps talking to their already-running hub, which is still executing the old SDK code. Bumping the SDK version makes the new CLI's `buildId` differ, so the stale hub is detected as incompatible and respawned with the fresh code.
- Release hygiene. We want regular SDK releases; cutting one whenever we cut a CLI release keeps the published SDK in step with what the CLI ships.
So when the SDK has changed, release it first (which bumps the `@cline/core` version), then cut the CLI release on top of that bump. Leave the CLI's SDK dependency as `workspace:*` — the fix is to release the SDK, not to pin the CLI.
1. Check for unreleased SDK changes.
```sh
git fetch origin --tags
git tag --list 'sdk/sdk/v*' 'sdk-v*' --sort=-v:refname | head -1
git log <last-sdk-tag>..origin/main --oneline --no-merges -- sdk/packages
```
`sdk/<pkg>/v*` tags are created by the `sdk-publish.yml` workflow; `sdk-v*` tags are created by the local `bun release sdk` helper. Use whichever is newest as the baseline.
If `git log` prints no commits, the SDK is already up to date. Skip the rest of Step 0 and continue with the Workflow below.
If it prints commits, sanity-check the diff (ignore entries that are only the previous version-bump commit's lockfile or generated files), then release the SDK.
2. Decide the SDK version bump.
All SDK packages share one version, read from `sdk/packages/llms/package.json`. Ask whether this is patch, minor, major, or an explicit version. Patch is the default. Do not guess if the user has not made it clear.
3. Draft the SDK release notes and update the changelog.
Draft user-facing notes from the SDK commits found in step 1, translating commit messages into user-facing language (same approach as the CLI release notes below). Prepend a new `## <version>` section with those notes to the top of `sdk/CHANGELOG.md`, using the header format `## <version>` with no date — the same flat, newest-on-top format as `apps/cli/CHANGELOG.md`. This is the SDK changelog (all SDK packages share one version) and it is maintained by hand; the `sdk-publish.yml` workflow does not read it.
4. Bump versions and regenerate.
```sh
bun run version <version>
```
This bumps every SDK `package.json` to the new version, regenerates the lockfile and the generated model catalog, formats, and builds. Review the result.
5. Commit and push the bump to `main`.
The `sdk-publish.yml` workflow publishes the version that is committed on `main` and tags that commit, so the bump must land on `main` before the workflow runs.
```sh
git add -A
git commit -m "chore(sdk): release v<version>"
```
Ask before pushing:
```sh
git push origin HEAD
```
6. Trigger the SDK publish workflow on the `latest` channel.
```sh
gh workflow run sdk-publish.yml -f channel=latest -f confirm_publish=publish
gh run list --workflow=sdk-publish.yml --limit=1 --json databaseId,url,status,createdAt --jq '.[0]'
```
The workflow runs the SDK tests, publishes `@cline/shared`, `@cline/llms`, `@cline/agents`, `@cline/core`, and `@cline/sdk` to npm with the `latest` dist-tag in dependency order, and pushes `sdk/<pkg>/v<version>` git tags.
7. Wait for the SDK workflow to succeed before starting the CLI release.
```sh
gh run watch <run-id> --exit-status
```
Do not start the CLI release until this run has finished successfully. The CLI does not install the SDK from npm, but cutting the CLI release on top of a clean, completed SDK release keeps the two in step: the CLI release commit then sits on top of the `@cline/core` version bump, so the shipped CLI carries the new version that forces a running hub to respawn with the new code, and you are not building a CLI release on top of an SDK release that failed midway.
After the SDK release succeeds, pull `main` so the CLI release is prepared on top of the SDK version bump:
```sh
git checkout main && git pull --ff-only
```
Then continue with the Workflow below.
For a local SDK publish from an authenticated machine instead of the workflow, `bun release sdk <version>` exists, but prefer the `sdk-publish.yml` workflow for normal releases so the CLI release can gate on a single GitHub Actions run.
## Workflow
Complete Step 0 first. Only proceed once the SDK is released (or you confirmed no SDK release was needed).
1. Gather context.
```sh
git status --short --branch
git fetch origin --tags
git tag --list 'cli-v*' --sort=-v:refname | head -10
node -p "require('./apps/cli/package.json').version"
```
Find the latest CLI tag. If there is no `cli-v*` tag, use the first relevant CLI release commit as the baseline and say that the baseline is inferred.
2. Collect release commits.
```sh
git log <last-cli-tag>..HEAD --oneline --no-merges -- apps/cli sdk/packages sdk/scripts .github/workflows/cli-publish.yml
```
The `sdk/packages` commits matter here even though the SDK was released separately in Step 0: the CLI bundles the SDK, so SDK changes ship in this CLI release too. Read those commits and fold anything user-relevant to the CLI into the release notes (provider/model updates, behavior changes, fixes the CLI inherits). Skip SDK changes that are purely internal or have no CLI-visible effect.
3. Draft user-facing release notes.
Include user-facing features, fixes, behavior changes, compatibility changes, and notable install or release changes. Exclude pure refactors, tests, style, chores, and internal file moves unless they matter to users.
Write a flat bullet list. Translate commit messages into user-facing language. If a commit is unclear, read the full commit before summarizing it.
Present the draft and wait for approval before editing files.
4. Decide the version bump.
Ask whether this should be patch, minor, major, or an explicit version. Do not guess if the user has not made it clear.
5. Update release files.
Update `apps/cli/package.json` to the approved version.
Prepend a section to `apps/cli/CHANGELOG.md` for the approved version using the approved release notes. Use the header format `## X.Y.Z` with no date. The publish workflow extracts the top section of the changelog by matching `^## [0-9]` and pastes it verbatim into the GitHub release body and the Slack release announcement, so the section content is the release notes that get shipped.
6. Verify before committing.
Run focused checks first:
```sh
bun -F @cline/cli typecheck
bun -F @cline/cli test:unit
```
For higher confidence, run:
```sh
bun run types
bun --cwd apps/cli run build:platforms:single
```
If the user wants full release confidence before tagging, run:
```sh
bun run test
bun --cwd apps/cli run build:platforms
```
Known local-only test failure: `src/commands/distribution-package.test.ts > rejects direct source package packing by default` will fail on machines that have `ignore-scripts=true` in `~/.npmrc` (set by the npm supply-chain hardening guide). Bun reads npm's `ignore-scripts` from `~/.npmrc`, so `bun pm pack --dry-run` skips the source-publish `prepack` guard and exits 0, which the test reads as a failure. CI does not set `ignore-scripts`, so the test passes there. Confirm by running `bun pm pack --dry-run` directly: with `~/.npmrc` in place it exits 0 with no guard output; with `~/.npmrc` moved aside it exits 1 and prints the guard message. This is not a release blocker by itself, but it does mean the local-publish path (`bun release cli`) will also bypass the source-publish guard on this machine; prefer the GitHub Actions publish path on machines with `ignore-scripts=true` set globally, or temporarily unset it (`npm config delete ignore-scripts` or `mv ~/.npmrc ~/.npmrc.bak`) for the duration of a local publish.
7. Commit release changes.
Only after the user approves the notes and version:
```sh
git add apps/cli/package.json apps/cli/CHANGELOG.md
git commit -m "chore(cli): release vX.Y.Z"
```
Ask before pushing the release commit:
```sh
git push origin HEAD
```
For the GitHub main release path, ask before creating and pushing the release tag:
```sh
git tag -a cli-vX.Y.Z -m "CLI vX.Y.Z"
git push origin refs/tags/cli-vX.Y.Z
```
8. Publish.
Ask the user which path to use:
- GitHub main release. Use this after the release commit is on `main` and the matching `cli-vX.Y.Z` tag has been pushed. The workflow publishes to npm from that tag, creates the GitHub release, and posts to Slack.
- Local release. Use this when the user wants to publish from this machine. The local machine must be authenticated to npm and GitHub.
- GitHub nightly release.
- Stop after the version commit.
For GitHub main release:
```sh
gh workflow run cli-publish.yml -f publish_target=main -f git_tag=cli-vX.Y.Z -f confirm_publish=publish
gh run list --workflow=cli-publish.yml --limit=1 --json url,status,conclusion,createdAt --jq '.[0]'
```
For GitHub nightly release:
```sh
gh workflow run cli-publish.yml -f publish_target=nightly
```
For forced GitHub nightly release:
```sh
gh workflow run cli-publish.yml -f publish_target=nightly -f force_nightly_publish=true
```
For local publish:
```sh
gh auth status
npm whoami
git tag -a cli-vX.Y.Z -m "CLI vX.Y.Z"
git push origin refs/tags/cli-vX.Y.Z
bun release cli
```
After a successful local publish, ask before running:
```sh
gh release create cli-vX.Y.Z --verify-tag --title "CLI vX.Y.Z" --notes "Paste the approved release notes here."
```
If publishing with another npm dist-tag:
```sh
bun release cli --tag next
```
9. Final response.
Report:
- version
- tag
- changelog file updated
- commit hash
- whether anything was pushed
- publish path selected
- workflow URL or local publish result
- tests and builds run
+33
View File
@@ -0,0 +1,33 @@
# CLI Development
The CLI lives in `cli/` and uses React Ink for terminal UI.
- If needed, look at `cli/src/constants/colors.ts` for re-used terminal colors, e.g. `COLORS.primaryBlue` highlight color (selections, spinners, success states).
- Never use `dimColor` with gray (e.g. `<Text color="gray" dimColor>`) - it's too hard to read. Use `color="gray"` for secondary text and normal foreground (no color) for primary text.
- When thinking about how to handle state or messages from core, look at webview for how it communicates with the vs code extension.
- When updating the webview, consider and suggest to the user to update the CLI TUI since we want to provide a similar experience to our terminal users as we do our vs code extension users.
## Adding New API Providers
When adding a new API provider to the extension, you must also update the CLI:
1. **Update `cli/src/components/ModelPicker.tsx`**: Add the provider to the `providerModels` map so `getDefaultModelId()` returns the correct default model. Import the models and default ID from `@shared/api`:
```typescript
import { newProviderDefaultModelId, newProviderModels } from "@/shared/api"
export const providerModels = {
// ...existing providers
"new-provider": { models: newProviderModels, defaultId: newProviderDefaultModelId },
}
```
2. **Use `applyProviderConfig()` for auth flows**: When implementing OAuth or other auth flows for the provider, use the shared utility at `cli/src/utils/provider-config.ts`:
```typescript
import { applyProviderConfig } from "../utils/provider-config"
// After successful auth:
await applyProviderConfig({ providerId: "new-provider", controller })
```
This handles setting provider, default model, API key mapping, state persistence, and rebuilding the API handler.
3. **Provider-specific auth**: If the provider uses OAuth (like `openai-codex`), add handling in `SettingsPanelContent.tsx`'s `handleProviderSelect` callback. See the existing Codex OAuth flow as a reference.
+13 -9
View File
@@ -140,10 +140,12 @@ Adding a new key to global state requires updates in multiple places. Missing an
Required steps:
1. Type definition in `src/shared/storage/state-keys.ts` - Add to `GlobalState` or `Settings` interface
2. Add any default value or transform in `src/shared/storage/state-keys.ts` if the key needs one
3. Read and write the value through `StateManager` (`setGlobalState()` / `getGlobalStateKey()`) after initialization
2. Read from globalState in `src/core/storage/utils/state-helpers.ts`:
- Add `const myKey = context.globalState.get<GlobalStateAndSettings["myKey"]>("myKey")` in `readGlobalStateFromDisk()`
- Add to the return object: `myKey: myKey ?? defaultValue,`
3. StateManager handles read/write via `setGlobalState()`/`getGlobalStateKey()` after initialization
Persistent state is file-backed through `StateManager`; do not add new runtime reads or writes against VS Code `ExtensionContext` storage. That storage is only a legacy migration source.
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
@@ -157,20 +159,22 @@ Webview toggle gotcha: settings changes must also round-trip back in state paylo
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
## StateManager Cache vs Direct globalState Access
StateManager uses an in-memory cache populated during `StateManager.initialize()` from file-backed storage. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
Exception: host migration code may read legacy VS Code storage before file-backed storage is initialized.
Exception: State needed immediately at extension startup (before cache is ready)
Example pattern:
When Window A sets state and immediately opens Window B, the new window's StateManager cache is populated from `context.globalState` during initialization. If you need to read state in Window B right at startup (e.g., in `common.ts` during `initialize()`), read directly from `context.globalState.get()` instead of StateManager's cache.
Example pattern (see `lastShownAnnouncementId` and `worktreeAutoOpenPath`):
```typescript
// Writing (normal pattern)
controller.stateManager.setGlobalState("myKey", value)
// Reading after initialization
const value = controller.stateManager.getGlobalStateKey("myKey")
// Reading at startup in common.ts (bypass cache)
const value = context.globalState.get<string>("myKey")
```
Use `context.globalState` only in VS Code migration code that copies legacy ExtensionContext values into the shared file-backed stores.
This is only needed for cross-window state read during the brief startup window before StateManager cache is fully usable. Normal state access after initialization should use StateManager.
## ChatRow Cancelled/Interrupted States
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
+1 -1
View File
@@ -176,7 +176,7 @@ Present a final summary:
- Slack message copied to clipboard: yes
Remind the user to:
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/ext-vscode-publish-stable.yml (paste `v{VERSION}` as the tag)
1. Manually trigger the publish release GitHub Action at: https://github.com/cline/cline/actions/workflows/publish.yml (paste `v{VERSION}` as the tag)
2. Post the Slack message to announce the hotfix
## Important Notes
+1 -1
View File
@@ -43,7 +43,7 @@ git push origin v<version>
### 4) Trigger publish workflow
Tell the maintainer to run:
https://github.com/cline/cline/actions/workflows/ext-vscode-publish-stable.yml
https://github.com/cline/cline/actions/workflows/publish.yml
Use `v<version>` as the release tag.
+2 -3
View File
@@ -20,9 +20,8 @@ command = "chmod +x ./scripts/run-extension-host.sh && ./scripts/run-extension-h
name = "CLI"
icon = "run"
command = '''
cd sdk
bun install
bun run cli
npm run cli:build
npm run cli:run
'''
[[actions]]
+1
View File
@@ -1,4 +1,5 @@
demo.gif filter=lfs diff=lfs merge=lfs -text
assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
webview-ui/src/assets/cline_kanban_demo.webm filter=lfs diff=lfs merge=lfs -text
* text=auto eol=lf
+1 -1
View File
@@ -1,2 +1,2 @@
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @dominiccooney
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
/README.md @saoudrizwan @juanpflores
+3 -21
View File
@@ -7,10 +7,10 @@ body:
value: |
**Important:** All bug reports must be reproducible using Claude Sonnet 4.5. Cline uses complex prompts so less capable models may not work as expected.
- type: dropdown
id: cline-surface
id: plugin-type
attributes:
label: Cline Surface
description: Which Cline surface are you reporting a bug for?
label: Plugin Type
description: Which plugin are you reporting a bug for?
options:
- VSCode Extension
- JetBrains Plugin
@@ -26,12 +26,6 @@ body:
placeholder: 'e.g., 1.2.3'
validations:
required: true
- type: checkboxes
id: beta
attributes:
label: Beta version
options:
- label: I am using a beta version of Cline
- type: textarea
id: what-happened
attributes:
@@ -59,18 +53,6 @@ body:
placeholder: 'e.g., cline:anthropic/claude-sonnet-4.5, gemini:gemini-2.5-pro-exp-03-25'
validations:
required: false
- type: textarea
id: ide-diagnostics
attributes:
label: IDE / CLI Diagnostics
description: |
Paste the "About" diagnostics for your Cline surface. This captures the IDE build, runtime, and host details we need.
- VSCode Extension: open `Help → About` (Windows/Linux) or `Code → About Visual Studio Code` (macOS), then copy the info.
- JetBrains Plugin: open `Help → About` (Windows/Linux) or `<IDE name> → About` (macOS), then click `Copy` to grab build, runtime, OS, memory, and cores.
- CLI: there is no About dialog. Run `cline --version` and paste the output.
placeholder: Paste the copied About info or `cline --version` output here.
validations:
required: false
- type: textarea
id: system-info
attributes:
+3 -2
View File
@@ -5,6 +5,7 @@ This is a VS Code extension. Read `.clinerules/general.md` for tribal knowledge
## Architecture
- **Core** (`src/`): `extension.ts``WebviewProvider``Controller` (single source of truth) → `Task` (agent loop).
- **Webview** (`webview-ui/`): React/Vite app. State via `ExtensionStateContext.tsx`, synced through message passing.
- **CLI** (`cli/`): React Ink terminal UI sharing core logic. Update CLI when changing webview features.
- **Communication**: Protobuf-defined gRPC-like protocol over VS Code message passing. Schemas in `proto/`.
- **MCP**: `src/services/mcp/McpHub.ts`.
@@ -27,7 +28,7 @@ Three proto conversion updates are **required** or the provider silently resets
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts`.
3. `convertProtoToApiProvider()` in the same file.
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`.
Also update: `src/shared/api.ts`, `src/shared/providers/providers.json`, `src/core/api/index.ts`, `webview-ui/.../providerUtils.ts`, `webview-ui/.../validate.ts`, `webview-ui/.../ApiOptions.tsx`, and `cli/src/components/ModelPicker.tsx`.
For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/model-utils.ts` and set `apiFormat: ApiFormat.OPENAI_RESPONSES` on models.
@@ -44,7 +45,7 @@ For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/mod
Modular: `components/` (shared) + `variants/` (model-specific) + `templates/` (`{{PLACEHOLDER}}`). Variants override components via `componentOverrides` in `config.ts` or custom `template.ts`. XS variant is heavily condensed inline. Always regenerate snapshots after changes.
## Global State Keys (silent failure risk)
Adding a key requires updating the typed storage definitions in `src/shared/storage/state-keys.ts`; runtime reads and writes should go through `StateManager`, not VS Code `ExtensionContext` storage. Persistent state is file-backed so it works across VS Code, CLI, and JetBrains hosts.
Adding a key requires: type in `src/shared/storage/state-keys.ts`, read via `context.globalState.get()` in `src/core/storage/utils/state-helpers.ts` `readGlobalStateFromDisk()`, and add to return object. Missing the `.get()` call compiles fine but value is always `undefined`.
## Slash Commands (3 places)
- `src/core/slash-commands/index.ts` — definitions.
+2 -2
View File
@@ -2,7 +2,7 @@ version: 2
updates:
# Main extension dependencies
- package-ecosystem: "npm"
directory: "/apps/vscode"
directory: "/"
schedule:
interval: "weekly"
# Group all updates into a single PR
@@ -20,7 +20,7 @@ updates:
# Webview UI dependencies
- package-ecosystem: "npm"
directory: "/apps/vscode/webview-ui"
directory: "/webview-ui"
schedule:
interval: "weekly"
groups:
-435
View File
@@ -1,435 +0,0 @@
name: cli-publish
on:
schedule:
- cron: "0 12 * * *"
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
git_tag:
description: "Existing release tag to publish when publish_target=main, for example cli-v0.1.0"
required: false
type: string
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: .
jobs:
publish-main:
name: Publish cline
permissions:
contents: write
id-token: write
if: |
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git_tag }}
fetch-depth: 0
fetch-tags: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Verify publish tooling
run: |
NPM_VERSION=$(npm --version)
echo "npm ${NPM_VERSION}"
IFS=. read -r major minor patch <<EOF
${NPM_VERSION}
EOF
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
echo "npm 11.5.1 or newer is required for trusted publishing"
exit 1
fi
- name: Install dependencies
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Validate release tag
id: version
env:
TAG: ${{ github.event.inputs.git_tag }}
run: |
if [ -z "$TAG" ]; then
echo "git_tag is required when publish_target=main"
exit 1
fi
if ! printf "%s\n" "$TAG" | grep -Eq '^cli-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "git_tag must look like cli-vX.Y.Z, got: ${TAG}"
exit 1
fi
VERSION="${TAG#cli-v}"
PACKAGE_VERSION=$(node -p "require('./apps/cli/package.json').version")
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
echo "apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
exit 1
fi
if ! printf "%s\n" "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "apps/cli/package.json has invalid version: ${VERSION}"
exit 1
fi
TAG_COMMIT=$(git rev-parse "${TAG}^{commit}")
HEAD_COMMIT=$(git rev-parse HEAD)
if [ "$TAG_COMMIT" != "$HEAD_COMMIT" ]; then
echo "${TAG} does not point at the checked out commit"
exit 1
fi
git fetch origin +main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "$HEAD_COMMIT" origin/main; then
echo "${TAG} is not reachable from origin/main"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: Build SDK packages
run: bun run build:sdk
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
- name: Run tests
run: bun run test
- name: Build platform binaries
run: bun script/build.ts --install-native-variants --skip-sdk-build
working-directory: apps/cli
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
- name: Verify build output
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
EXPECTED=(
"@cline/cli-darwin-arm64"
"@cline/cli-darwin-x64"
"@cline/cli-linux-arm64"
"@cline/cli-linux-x64"
"@cline/cli-windows-arm64"
"@cline/cli-windows-x64"
)
for package_name in "${EXPECTED[@]}"; do
dir="apps/cli/dist/${package_name#@cline/}"
if [ ! -f "$dir/package.json" ]; then
echo "Missing package manifest: $dir/package.json"
exit 1
fi
actual_name=$(node -p "require('./$dir/package.json').name")
actual_version=$(node -p "require('./$dir/package.json').version")
if [ "$actual_name" != "$package_name" ]; then
echo "Expected $package_name, got $actual_name"
exit 1
fi
if [ "$actual_version" != "$VERSION" ]; then
echo "Expected $package_name@$VERSION, got $actual_version"
exit 1
fi
ls -lh "$dir/bin/"
done
- name: Publish to NPM with latest tag
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag latest
working-directory: apps/cli
- name: Get Previous CLI Tag
id: prev_tag
env:
CURRENT_TAG: ${{ steps.version.outputs.tag }}
run: |
PREV_TAG=$(git describe --tags --abbrev=0 --match 'cli-v*' "$CURRENT_TAG^" 2>/dev/null || echo "")
echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
- name: Get Changelog Entry
id: changelog
run: |
# Grab content between the first "## " header and the next one in apps/cli/CHANGELOG.md
CONTENT=$(awk '/^## [0-9]/{if(found) exit; found=1; next} found{print}' apps/cli/CHANGELOG.md)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version.outputs.tag }}
name: "CLI v${{ steps.version.outputs.version }}"
body: |
${{ steps.changelog.outputs.content }}
${{ steps.prev_tag.outputs.prev_tag != '' && format('**Full Changelog**: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Published cline@${VERSION} to npm with dist-tag 'latest'"
echo "Install with: npm install -g cline"
- name: Post release to Slack
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline CLI v${{ steps.version.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "Cline CLI v${{ steps.version.outputs.version }}"
- type: "section"
text:
type: "mrkdwn"
text: ${{ toJSON(steps.changelog.outputs.content) }}
- type: "context"
elements:
- type: "mrkdwn"
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>${{ steps.prev_tag.outputs.prev_tag != '' && format(' | Full Changelog: https://github.com/{0}/compare/{1}...{2}', github.repository, steps.prev_tag.outputs.prev_tag, steps.version.outputs.tag) || '' }}"
publish-nightly:
name: Publish cline nightly
permissions:
contents: read
id-token: write
if: |
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
(
github.event_name == 'schedule' ||
(
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'nightly'
)
)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for recent commits
id: check_commits
env:
FORCE_PUBLISH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
run: |
if [ "$FORCE_PUBLISH" = "true" ]; then
echo "force_nightly_publish enabled, proceeding with publish"
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$(git rev-list --count HEAD --since='24 hours ago')" -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
if: steps.check_commits.outputs.skip != 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Verify publish tooling
if: steps.check_commits.outputs.skip != 'true'
run: |
NPM_VERSION=$(npm --version)
echo "npm ${NPM_VERSION}"
IFS=. read -r major minor patch <<EOF
${NPM_VERSION}
EOF
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
echo "npm 11.5.1 or newer is required for trusted publishing"
exit 1
fi
- name: Install dependencies
if: steps.check_commits.outputs.skip != 'true'
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build SDK packages
if: steps.check_commits.outputs.skip != 'true'
run: bun run build:sdk
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
- name: Run tests
if: steps.check_commits.outputs.skip != 'true'
run: bun run test
- name: Generate nightly version
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
BASE_VERSION=$(node -p "require('./apps/cli/package.json').version")
TIMESTAMP=$(date +%s)
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: ${BASE_VERSION}"
echo "Generated nightly version: ${VERSION}"
echo "base_version=${BASE_VERSION}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Update nightly package version
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
node -e '
const fs = require("node:fs");
const path = "apps/cli/package.json";
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
pkg.version = process.env.VERSION;
fs.writeFileSync(path, `${JSON.stringify(pkg, null, "\t")}\n`);
'
cat apps/cli/package.json | grep '"version"'
- name: Build platform binaries
if: steps.check_commits.outputs.skip != 'true'
run: bun script/build.ts --install-native-variants --skip-sdk-build
working-directory: apps/cli
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
EXPECTED=(
"@cline/cli-darwin-arm64"
"@cline/cli-darwin-x64"
"@cline/cli-linux-arm64"
"@cline/cli-linux-x64"
"@cline/cli-windows-arm64"
"@cline/cli-windows-x64"
)
for package_name in "${EXPECTED[@]}"; do
dir="apps/cli/dist/${package_name#@cline/}"
if [ ! -f "$dir/package.json" ]; then
echo "Missing package manifest: $dir/package.json"
exit 1
fi
actual_name=$(node -p "require('./$dir/package.json').name")
actual_version=$(node -p "require('./$dir/package.json').version")
if [ "$actual_name" != "$package_name" ]; then
echo "Expected $package_name, got $actual_name"
exit 1
fi
if [ "$actual_version" != "$VERSION" ]; then
echo "Expected $package_name@$VERSION, got $actual_version"
exit 1
fi
ls -lh "$dir/bin/"
done
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag nightly
working-directory: apps/cli
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Published cline@${VERSION} to npm with dist-tag 'nightly'"
echo "Install with: npm install -g cline@nightly"
+83
View File
@@ -0,0 +1,83 @@
name: CLI TUI Tests
on:
pull_request:
branches:
- main
workflow_dispatch:
workflow_call:
permissions:
contents: read
jobs:
cli-tui-tests:
name: CLI TUI Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build CLI
run: npm run cli:build
- name: Run TUI Tests
id: tui_tests
run: |
npm run test:e2e:cli:tui 2>&1 | tee tui-test-output.log
exit_code=${PIPESTATUS[0]}
echo "tui_exit_code=$exit_code" >> $GITHUB_OUTPUT
exit $exit_code
- name: Write failure summary
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
run: |
echo "## ❌ CLI TUI Tests Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Step outcome:** \`${{ steps.tui_tests.outcome }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Output" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ -f tui-test-output.log ]; then
cat tui-test-output.log >> $GITHUB_STEP_SUMMARY
else
echo "(no test output captured — process may have been killed before output was flushed)" >> $GITHUB_STEP_SUMMARY
fi
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Debugging" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **TUI traces** are attached as artifacts below — download and inspect them to see terminal state at the point of failure." >> $GITHUB_STEP_SUMMARY
echo "- **To view a trace replay/Run a TUI Trace: ** run \`npx tui-test show-trace path/to/trace/file\` in your terminal" >> $GITHUB_STEP_SUMMARY
echo "- **Full test log** is also attached as an artifact." >> $GITHUB_STEP_SUMMARY
echo "- Tests run with \`retries: 2\` so any failure shown is a consistent failure, not a flake." >> $GITHUB_STEP_SUMMARY
- name: Upload TUI traces
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-traces
path: tests/e2e/cli/tui-traces/
retention-days: 14
if-no-files-found: warn
- name: Upload test log
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-log
path: tui-test-output.log
retention-days: 14
if-no-files-found: warn
@@ -0,0 +1,85 @@
name: Smoke Tests
on:
push:
branches: [main]
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
pull_request:
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: smoke-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
smoke-tests:
name: Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build and install CLI
run: |
npm run protos
cd cli && npm install && npm run build && npm link
echo "$(npm config get prefix)/bin" >> $GITHUB_PATH
- name: Verify CLI
run: cline --version
- name: Run smoke tests
env:
CLINE_API_KEY: ${{ secrets.CLINE_API_KEY }}
run: |
cline auth -p cline -k "$CLINE_API_KEY" -m "anthropic/claude-sonnet-4.5"
max_attempts=3
for attempt in $(seq 1 $max_attempts); do
echo "::group::Attempt $attempt of $max_attempts"
if npx tsx evals/smoke-tests/run-smoke-tests.ts --trials 1 --parallel; then
echo "::endgroup::"
echo "Smoke tests passed on attempt $attempt"
exit 0
fi
echo "::endgroup::"
if [ $attempt -lt $max_attempts ]; then
echo "::warning::Smoke tests failed on attempt $attempt, retrying..."
sleep 10
fi
done
echo "::error::Smoke tests failed after $max_attempts attempts"
exit 1
- name: Generate summary
if: always()
run: cat evals/smoke-tests/results/latest/summary.md >> $GITHUB_STEP_SUMMARY
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: smoke-test-results-${{ github.run_id }}
path: evals/smoke-tests/results/latest/
retention-days: 30
@@ -1,4 +1,4 @@
name: ext-vscode-test-e2e
name: E2E Tests
on:
push:
@@ -12,53 +12,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
detect-changes:
runs-on: ubuntu-latest
name: Detect Changes
outputs:
e2e: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.e2e == 'true' }}
steps:
- id: force
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: echo "run_all=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
if: steps.force.outputs.run_all != 'true'
- uses: dorny/paths-filter@v3
if: steps.force.outputs.run_all != 'true'
id: filter
with:
filters: |
e2e:
- 'apps/vscode/src/**'
- 'apps/vscode/webview-ui/**'
- 'apps/vscode/proto/**'
- 'apps/vscode/tests/**'
- 'apps/vscode/scripts/**'
- 'apps/vscode/standalone/**'
- 'apps/vscode/assets/**'
- 'apps/vscode/walkthrough/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/biome.jsonc'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.mocharc.json'
- 'apps/vscode/.vscode-test.mjs'
- 'apps/vscode/.vscodeignore'
- 'apps/vscode/playwright*.ts'
- '.github/workflows/ext-vscode-test-e2e.yml'
matrix_prep:
needs: detect-changes
if: needs.detect-changes.outputs.e2e == 'true'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -68,8 +23,7 @@ jobs:
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
e2e:
needs: [detect-changes, matrix_prep]
if: needs.detect-changes.outputs.e2e == 'true'
needs: matrix_prep
strategy:
fail-fast: false
matrix:
@@ -79,9 +33,6 @@ jobs:
permissions:
id-token: write
contents: read
defaults:
run:
working-directory: apps/vscode
steps:
- uses: actions/checkout@v4
- name: Setup Node.js environment
@@ -94,24 +45,24 @@ jobs:
uses: actions/cache@v4
id: root-cache
with:
path: apps/vscode/node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('apps/vscode/package-lock.json') }}
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: apps/vscode/webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('apps/vscode/webview-ui/package-lock.json') }}
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache VS Code installation
- name: Cache VS Code
uses: actions/cache@v4
id: vscode-cache
with:
path: apps/vscode/.vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('apps/vscode/.vscode-test.mjs', 'apps/vscode/package.json') }}
path: .vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
restore-keys: |
vscode-${{ runner.os }}-stable-
@@ -124,19 +75,17 @@ jobs:
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('apps/vscode/package-lock.json') }}
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
run: cd webview-ui && npm ci
- name: Install vsce
run: npm install -g @vscode/vsce
@@ -1,104 +0,0 @@
name: ext-vscode-publish-nightly
on:
workflow_dispatch:
run-name: "Publish Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
# Prevent concurrent publish runs on the same branch. The nightly publish script
# generates the extension version from a seconds-resolution timestamp, so parallel
# runs on the same ref can collide on the same version and cause publish failures
# or inconsistent tagging. Runs on different branches proceed independently.
concurrency:
group: ext-vscode-publish-nightly-${{ github.ref }}
cancel-in-progress: false
permissions: {}
jobs:
test:
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
permissions:
contents: read
pull-requests: read
uses: ./.github/workflows/ext-vscode-test.yml
publish:
needs: test
permissions:
contents: write
name: Publish Cline (Nightly) Extension
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
runs-on: ubuntu-latest
environment: PublishNightly
defaults:
run:
working-directory: apps/vscode
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
lfs: true
persist-credentials: false
- name: Show build source
working-directory: ${{ github.workspace }}
run: |
echo "Building ref: $GITHUB_REF"
echo "Building sha: $GITHUB_SHA"
git --no-pager log -1 --oneline
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
node-version: 22
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci --include=optional
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Publish Nightly Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
run: npm run publish:marketplace:nightly
- name: Tag published commit
working-directory: ${{ github.workspace }}
env:
GH_TOKEN: ${{ github.token }}
run: |
SAFE_REF=$(echo "$GITHUB_REF_NAME" | tr '/[:upper:]' '-[:lower:]' | tr -cd 'a-z0-9._-')
SHORT_SHA=$(git rev-parse --short=12 HEAD)
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
TAG="nightly-${SAFE_REF}-${TIMESTAMP}-${SHORT_SHA}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Cline Nightly published from ${GITHUB_REF_NAME} at ${GITHUB_SHA}"
# Use an explicit HTTPS remote with GH_TOKEN because checkout was run with
# persist-credentials: false, so actions/checkout did not persist a git credential helper.
git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/${TAG}"
echo "Tagged published commit: $TAG"
-365
View File
@@ -1,365 +0,0 @@
name: ext-vscode-test
on:
push:
branches:
- main
workflow_dispatch:
pull_request:
branches:
- main
workflow_call:
# Set default permissions for all jobs
permissions:
contents: read # Needed to check out code
pull-requests: read # Needed for changed-file detection on pull requests
jobs:
detect-changes:
runs-on: ubuntu-latest
name: Detect Changes
outputs:
vscode: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.vscode == 'true' }}
testing_platform: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.testing_platform == 'true' }}
steps:
- id: force
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
run: echo "run_all=true" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
if: steps.force.outputs.run_all != 'true'
- uses: dorny/paths-filter@v3
if: steps.force.outputs.run_all != 'true'
id: filter
with:
filters: |
vscode:
- 'apps/vscode/src/**'
- 'apps/vscode/webview-ui/**'
- 'apps/vscode/proto/**'
- 'apps/vscode/tests/**'
- 'apps/vscode/scripts/**'
- 'apps/vscode/standalone/**'
- 'apps/vscode/assets/**'
- 'apps/vscode/walkthrough/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/biome.jsonc'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.mocharc.json'
- 'apps/vscode/.nycrc*.json'
- 'apps/vscode/.vscode-test.mjs'
- 'apps/vscode/test-setup.js'
- '.github/workflows/ext-vscode-test.yml'
testing_platform:
- 'apps/vscode/src/**'
- 'apps/vscode/proto/**'
- 'apps/vscode/standalone/**'
- 'apps/vscode/testing-platform/**'
- 'apps/vscode/tests/specs/**'
- 'apps/vscode/package.json'
- 'apps/vscode/package-lock.json'
- 'apps/vscode/buf.yaml'
- 'apps/vscode/tsconfig*.json'
- 'apps/vscode/esbuild.mjs'
- 'apps/vscode/.vscodeignore'
- 'apps/vscode/scripts/**'
- '.github/workflows/ext-vscode-test.yml'
quality-checks:
needs: detect-changes
if: needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true'
runs-on: ubuntu-latest
name: Quality Checks
defaults:
run:
working-directory: apps/vscode
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
- name: Run Quality Checks (Parallel)
run: npm run ci:check-all
vscode-test:
needs: [detect-changes, quality-checks]
if: needs.detect-changes.outputs.vscode == 'true'
env:
VSCODE_TEST_VERSION: 1.103.0
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
name: ${{ matrix.os == 'ubuntu-latest' && 'vscode test' || format('vscode test ({0})', matrix.os) }}
defaults:
run:
shell: bash
working-directory: apps/vscode
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
- name: Set up NPM on Windows
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
- name: Cache VS Code test runtime
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: .vscode-test
key: vscode-test-runtime-${{ runner.os }}-${{ env.VSCODE_TEST_VERSION }}
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
- name: Unit Tests with coverage - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: |
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
- name: Unit Tests - Non-Linux
id: unit_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
npm run test:unit
- name: Extension Integration Tests - Linux
id: integration_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: xvfb-run -a npm run test:coverage
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
for attempt in 1 2 3; do
echo "Running extension integration tests (attempt ${attempt}/3)"
if npm run test:integration; then
exit 0
fi
if [ "$attempt" -eq 3 ]; then
echo "Extension integration tests failed after 3 attempts"
exit 1
fi
echo "Extension integration tests failed; retrying after short delay"
sleep 5
done
- name: Webview Tests with Coverage
id: webview_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
npm run test:coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
# Only upload artifacts on Linux - We only need coverage from one OS
if: runner.os == 'Linux'
with:
name: pr-coverage-reports
path: |
apps/vscode/coverage-unit/lcov.info
apps/vscode/webview-ui/coverage/lcov.info
test-platform-integration:
needs: [detect-changes, quality-checks]
if: needs.detect-changes.outputs.testing_platform == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/vscode
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
apps/vscode/package-lock.json
apps/vscode/webview-ui/package-lock.json
apps/vscode/testing-platform/package-lock.json
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui ci
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Compile Standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/testing-platform ci
- name: Running testing platform integration spec tests
timeout-minutes: 7
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
with:
name: test-platform-integration-core-coverage
path: apps/vscode/coverage/**/lcov.info
# Keep the required "test" check as a tiny aggregate gate instead of the conditional
# VS Code matrix. GitHub treats conditionally skipped jobs as successful required
# checks, so the gate below preserves the old required check name while making sure
# whichever filtered test jobs were selected actually passed.
test:
needs: [detect-changes, quality-checks, vscode-test, test-platform-integration]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
name: test
steps:
- name: Verify selected test jobs
env:
DETECT_CHANGES_RESULT: ${{ needs.detect-changes.result }}
QUALITY_CHECKS_RESULT: ${{ needs.quality-checks.result }}
VSCODE_CHANGED: ${{ needs.detect-changes.outputs.vscode }}
TESTING_PLATFORM_CHANGED: ${{ needs.detect-changes.outputs.testing_platform }}
VSCODE_TEST_RESULT: ${{ needs.vscode-test.result }}
TEST_PLATFORM_RESULT: ${{ needs.test-platform-integration.result }}
run: |
if [ "$DETECT_CHANGES_RESULT" != "success" ]; then
echo "detect-changes did not succeed: $DETECT_CHANGES_RESULT"
exit 1
fi
if [ "$VSCODE_CHANGED" != "true" ] && [ "$TESTING_PLATFORM_CHANGED" != "true" ]; then
echo "No root test paths changed; skipping root test requirements."
exit 0
fi
if [ "$QUALITY_CHECKS_RESULT" != "success" ]; then
echo "quality-checks did not succeed: $QUALITY_CHECKS_RESULT"
exit 1
fi
if [ "$VSCODE_CHANGED" = "true" ] && [ "$VSCODE_TEST_RESULT" != "success" ]; then
echo "vscode-test did not succeed: $VSCODE_TEST_RESULT"
exit 1
fi
if [ "$TESTING_PLATFORM_CHANGED" = "true" ] && [ "$TEST_PLATFORM_RESULT" != "success" ]; then
echo "test-platform-integration did not succeed: $TEST_PLATFORM_RESULT"
exit 1
fi
echo "Selected root test jobs passed."
qlty:
needs: [detect-changes, quality-checks, vscode-test, test-platform-integration]
if: ${{ !cancelled() && needs.quality-checks.result == 'success' && (needs.vscode-test.result == 'success' || needs.vscode-test.result == 'skipped') && (needs.test-platform-integration.result == 'success' || needs.test-platform-integration.result == 'skipped') && (needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true') }}
runs-on: ubuntu-latest
# Run on PRs to main, pushes to main, and manual dispatches
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download unit tests coverage reports
if: needs.detect-changes.outputs.vscode == 'true'
uses: actions/download-artifact@v4
with:
name: pr-coverage-reports
path: apps/vscode
- name: Upload core unit tests coverage to Qlty
if: needs.detect-changes.outputs.vscode == 'true'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
apps/vscode/coverage-unit/lcov.info
tag: unit:core
- name: Upload webview-ui unit tests coverage to Qlty
if: needs.detect-changes.outputs.vscode == 'true'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
apps/vscode/webview-ui/coverage/lcov.info
tag: unit:webview-ui
add-prefix: webview-ui/
- name: Download test platform integration core coverage artifact
if: needs.detect-changes.outputs.testing_platform == 'true'
uses: actions/download-artifact@v4
continue-on-error: true
id: download-integration-coverage
with:
name: test-platform-integration-core-coverage
path: apps/vscode/integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
if: needs.detect-changes.outputs.testing_platform == 'true' && steps.download-integration-coverage.outcome == 'success'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
files: apps/vscode/integration-core-coverage-reports/**/lcov.info
tag: integration:core
@@ -1,4 +1,4 @@
name: repo-label-issues
name: Auto-label Issues
on:
issues:
@@ -15,9 +15,9 @@ jobs:
script: |
const body = context.payload.issue.body || '';
const labels = context.payload.issue.labels.map(l => l.name);
// Check if JetBrains Plugin is selected
if (body.match(/###\s*Cline Surface\s*\n+JetBrains Plugin/i)) {
if (body.match(/###\s*Plugin Type\s*\n+JetBrains Plugin/i)) {
if (!labels.includes('JetBrains')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
@@ -29,7 +29,7 @@ jobs:
}
// Check if VSCode Extension is selected
if (body.match(/###\s*Cline Surface\s*\n+VSCode Extension/i)) {
if (body.match(/###\s*Plugin Type\s*\n+VSCode Extension/i)) {
if (!labels.includes('VS Code')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
@@ -41,7 +41,7 @@ jobs:
}
// Check if CLI is selected
if (body.match(/###\s*Cline Surface\s*\n+CLI/i)) {
if (body.match(/###\s*Plugin Type\s*\n+CLI/i)) {
if (!labels.includes('CLI')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
@@ -51,15 +51,3 @@ jobs:
});
}
}
// Check if beta version checkbox is checked
if (body.includes('- [X] I am using a beta version of Cline') || body.includes('- [x] I am using a beta version of Cline')) {
if (!labels.includes('beta')) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['beta']
});
}
}
+93
View File
@@ -0,0 +1,93 @@
name: Publish NPM Release
on:
workflow_call:
inputs:
confirm_publish:
description: 'Type "publish" to confirm you want to publish to NPM'
required: true
type: string
permissions:
contents: write # Required for pushing tags
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-release:
needs: test
name: Publish Cline CLI to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && inputs.confirm_publish == 'publish'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
run: npm run protos
- name: Read release version
id: version
run: |
# Read version from cli/package.json
VERSION=$(node -p "require('./cli/package.json').version")
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build standalone NPM package
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
run: node scripts/package-npm.mjs
- name: Verify build output
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with latest tag
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
cd dist-standalone
npm publish --tag latest --access public
- name: Tag release
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}-cli"
git push origin "v${{ steps.version.outputs.version }}-cli"
- name: Summary
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
echo ""
echo "📦 Install with: npm install -g cline"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
+134
View File
@@ -0,0 +1,134 @@
name: Publish NPM Nightly
on:
workflow_call:
inputs:
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-nightly:
needs: test
name: Publish Cline CLI (Nightly) to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check for recent commits
id: check_commits
run: |
if [ "${{ inputs.force_publish }}" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos
- name: Generate nightly version with timestamp
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read base version from cli/package.json (e.g., "2.0.0")
BASE_VERSION=$(node -p "require('./cli/package.json').version")
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 1.0.9-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
echo "Generated nightly version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update cli/package.json with nightly version
if: steps.check_commits.outputs.skip != 'true'
run: |
# Update version with timestamp-based nightly version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
"
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
- name: Build and package CLI
if: steps.check_commits.outputs.skip != 'true'
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
run: node scripts/package-npm.mjs
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
cd dist-standalone
npm publish --tag nightly --access public
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
echo ""
echo "📦 Install with: npm install -g cline@nightly"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
+215
View File
@@ -0,0 +1,215 @@
# Build and Pack CLI
#
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
# Requires write access to the repository (maintainers/collaborators only).
#
# Security: Split into two jobs to isolate untrusted build code from write tokens.
# The build job runs arbitrary ref code with zero permissions. The release job
# only runs trusted GitHub Actions with write scope.
#
# Usage (helper script, auto-detects current branch):
# ./scripts/build-cli-artifact.sh
# ./scripts/build-cli-artifact.sh feature/my-changes
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
#
# Usage (gh CLI directly):
# gh workflow run pack-cli.yml -f ref=main
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
#
# Install the built CLI (no auth required):
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
#
# Find releases:
# gh release list --limit 10
name: Build and Pack CLI
permissions:
contents: read
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
required: false
type: string
pr_number:
description: 'PR number to comment on with install instructions (optional)'
required: false
type: number
jobs:
# ── Build job: runs untrusted ref code with ZERO permissions ──
build:
name: Build CLI
runs-on: ubuntu-latest
permissions: {}
outputs:
commit_sha: ${{ steps.commit.outputs.sha }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get commit SHA
id: commit
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Building from commit: $COMMIT_SHA"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Build standalone package
run: node scripts/package-npm.mjs
- name: Create Tarball
id: pack
run: |
cd dist-standalone
TARBALL=$(npm pack)
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
echo "Created tarball: $TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-tarball
path: dist-standalone/*.tgz
# ── Release job: only trusted Actions code, with write permissions ──
release:
name: Release CLI
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: cli-tarball
path: dist-standalone
- name: Create GitHub Release
id: create_release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const commit = '${{ needs.build.outputs.commit_sha }}';
const tarball = '${{ needs.build.outputs.tarball }}';
// Delete existing release/tag if re-running for the same commit
const tagName = `cli-build-${commit}`;
try {
const existing = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: existing.data.id
});
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
core.info(`Deleted existing release for ${tagName}`);
} catch (e) {
// Release doesn't exist yet, that's fine
}
// Create a release
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `CLI Build (${commit})`,
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
draft: false,
prerelease: true
});
// Upload the tarball as a release asset
const tarballPath = path.join('dist-standalone', tarball);
const tarballData = fs.readFileSync(tarballPath);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: tarball,
data: tarballData
});
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
core.setOutput('release_url', release.data.html_url);
core.setOutput('download_url', downloadUrl);
- name: Comment on PR with download instructions
if: inputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const commit = '${{ needs.build.outputs.commit_sha }}';
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
const prNumber = ${{ inputs.pr_number || 0 }};
if (!prNumber) return;
const comment = `## 📦 CLI Build Ready
A CLI build has been created for commit \`${commit}\`.
### Install Directly from URL (No Authentication Required!)
\`\`\`bash
npm install -g ${downloadUrl}
\`\`\`
### Alternative: Download and Install
\`\`\`bash
curl -L ${downloadUrl} -o cline.tgz
npm install -g ./cline.tgz
\`\`\`
📦 [View Release](${releaseUrl})
`;
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Summary
run: |
echo "✅ CLI build complete!"
echo ""
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
echo ""
echo "Install from anywhere (no authentication required):"
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
@@ -0,0 +1,60 @@
name: Publish CLI (Trusted)
on:
schedule:
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
id-token: write # Required for npm trusted publishing (OIDC)
contents: write # Required because npm-main creates/pushes git tags
checks: write # Required by nested reusable test workflow
pull-requests: write # Required by nested reusable test workflow
jobs:
cli-tui-tests:
uses: ./.github/workflows/cli-tui-tests.yml
publish-main:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
uses: ./.github/workflows/npm-main.yaml
secrets: inherit
with:
confirm_publish: ${{ github.event.inputs.confirm_publish }}
publish-nightly:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target == 'nightly')
)
uses: ./.github/workflows/npm-nightly.yaml
secrets: inherit
with:
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
+76
View File
@@ -0,0 +1,76 @@
name: "Publish Nightly Release"
on:
schedule:
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
permissions:
contents: write
packages: write
checks: write
pull-requests: write
jobs:
test:
uses: ./.github/workflows/test.yml
publish:
needs: test
name: Publish Cline (Nightly) Extension
if: github.repository == 'cline/cline'
runs-on: ubuntu-latest
environment: PublishNightly
steps:
- uses: actions/checkout@v4
with:
lfs: true
- name: Check for recent commits
run: |
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, exiting"
exit 0
fi
echo "Found recent commits, proceeding with build"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
node-version: 22
- name: Install root dependencies
run: npm ci --include=optional
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
- name: Verify LFS media assets are resolved
run: |
FILE="webview-ui/src/assets/cline_kanban_demo.webm"
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
- name: Publish Extension as Pre-release
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
# OpenTelemetry production defaults (can be overridden at runtime)
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
run: npm run publish:marketplace:nightly
@@ -1,4 +1,4 @@
name: ext-vscode-publish-stable
name: "Publish Release"
on:
workflow_dispatch:
@@ -29,16 +29,13 @@ permissions:
jobs:
test:
uses: ./.github/workflows/ext-vscode-test.yml
uses: ./.github/workflows/test.yml
publish:
needs: test
name: Publish Extension
runs-on: ubuntu-latest
environment: publish
defaults:
run:
working-directory: apps/vscode
steps:
- uses: actions/checkout@v4
@@ -50,11 +47,9 @@ jobs:
- name: Resolve Release Tag
id: resolve_tag
working-directory: ${{ github.workspace }}
env:
TAG: ${{ github.event.inputs.tag }}
AUTO_CREATE: ${{ github.event.inputs.auto_create_tag_from_main }}
run: |
TAG="${{ github.event.inputs.tag }}"
AUTO_CREATE="${{ github.event.inputs.auto_create_tag_from_main }}"
TESTED_SHA="${{ github.sha }}"
WORKFLOW_REF="${{ github.ref }}"
@@ -114,13 +109,11 @@ jobs:
with:
node-version: 22
- name: Install extension dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode install --include=optional
- name: Install root dependencies
run: npm install --include=optional
- name: Install webview-ui dependencies
working-directory: ${{ github.workspace }}
run: npm --prefix apps/vscode/webview-ui install --include=optional
run: cd webview-ui && npm install --include=optional
- name: Install Publishing Tools
run: npm install -g @vscode/vsce ovsx
@@ -141,6 +134,14 @@ jobs:
fi
echo "Tag and package version match: $TAG"
- name: Verify LFS media assets are resolved
run: |
FILE="webview-ui/src/assets/cline_kanban_demo.webm"
if grep -q "git-lfs.github.com/spec/v1" "$FILE"; then
echo "Error: $FILE is still a Git LFS pointer in CI checkout"
exit 1
fi
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
@@ -155,19 +156,11 @@ jobs:
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
run: |
# Swap README.marketplace.md into README.md so both the GitHub
# release artifact (vsce package below) and the marketplace
# publish (npm run publish:marketplace below, which swaps
# internally as an idempotent no-op) ship the same README.
node scripts/marketplace-readme.mjs swap-in
trap 'node scripts/marketplace-readme.mjs restore' EXIT
# Required to generate the .vsix
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
if [ "$RELEASE_TYPE" = "pre-release" ]; then
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
npm run publish:marketplace:prerelease
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
else
@@ -177,7 +170,6 @@ jobs:
- name: Get Previous Tag
id: prev_tag
working-directory: ${{ github.workspace }}
run: |
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
@@ -185,7 +177,6 @@ jobs:
- name: Get Changelog Entry
id: changelog
working-directory: ${{ github.workspace }}
run: |
# Get content between first ## [ and second ## [
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
@@ -197,7 +188,7 @@ jobs:
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.resolve_tag.outputs.tag }}
files: "apps/vscode/*.vsix"
files: "*.vsix"
body: |
${{ steps.changelog.outputs.content }}
@@ -205,25 +196,3 @@ jobs:
prerelease: ${{ github.event.inputs.release-type == 'pre-release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post release to Slack
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline ${{ steps.resolve_tag.outputs.tag }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Cline ${{ steps.resolve_tag.outputs.tag }}*"
- type: "section"
text:
type: "mrkdwn"
text: ${{ toJSON(steps.changelog.outputs.content) }}
- type: "context"
elements:
- type: "mrkdwn"
text: "Full Changelog: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}"
-282
View File
@@ -1,282 +0,0 @@
name: sdk-publish
on:
workflow_dispatch:
inputs:
channel:
description: "Publish channel"
required: true
type: choice
options:
- nightly
- latest
default: nightly
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
confirm_publish:
description: 'Required when channel=latest. Type "publish" to confirm release publish.'
required: false
type: string
schedule:
# Run nightly at 2:00 AM UTC
- cron: "0 2 * * *"
defaults:
run:
working-directory: .
jobs:
test:
permissions:
contents: read
uses: ./.github/workflows/sdk-test.yml
publish-sdk:
needs: test
name: Publish SDK Packages
permissions:
contents: write
id-token: write
if: |
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
(
github.event_name != 'workflow_dispatch' ||
inputs.channel != 'latest' ||
(
inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine publish channel
id: channel
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_CHANNEL: ${{ inputs.channel }}
run: |
# Default to nightly for scheduled runs
if [ "$EVENT_NAME" = "schedule" ]; then
echo "channel=nightly" >> $GITHUB_OUTPUT
else
echo "channel=$INPUT_CHANNEL" >> $GITHUB_OUTPUT
fi
- name: Check for recent commits
id: check_commits
env:
CHANNEL: ${{ steps.channel.outputs.channel }}
FORCE_PUBLISH: ${{ inputs.force_publish }}
run: |
# Always publish for latest (production) releases
if [ "$CHANNEL" = "latest" ]; then
echo "Production release requested, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ "$FORCE_PUBLISH" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ "$(git rev-list --count HEAD --since="24 hours ago")" -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Verify trusted publishing context
if: steps.check_commits.outputs.skip != 'true'
run: |
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "GitHub OIDC request environment is unavailable. Ensure this job has id-token: write for npm trusted publishing."
exit 1
fi
echo "GitHub OIDC request environment is available for npm trusted publishing."
- name: Setup Bun
if: steps.check_commits.outputs.skip != 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Verify publish tooling
if: steps.check_commits.outputs.skip != 'true'
run: |
NPM_VERSION=$(npm --version)
echo "npm ${NPM_VERSION}"
IFS=. read -r major minor patch <<EOF
${NPM_VERSION}
EOF
if [ "$major" -lt 11 ] || { [ "$major" -eq 11 ] && [ "$minor" -lt 5 ]; } || { [ "$major" -eq 11 ] && [ "$minor" -eq 5 ] && [ "$patch" -lt 1 ]; }; then
echo "npm 11.5.1 or newer is required for trusted publishing"
exit 1
fi
- name: Install dependencies
if: steps.check_commits.outputs.skip != 'true'
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build SDK
if: steps.check_commits.outputs.skip != 'true'
run: bun run build:sdk
- name: Generate shared version
if: steps.check_commits.outputs.skip != 'true'
id: version
env:
CHANNEL: ${{ steps.channel.outputs.channel }}
run: |
BASE_VERSION=$(node -p "require('./sdk/packages/llms/package.json').version")
if [ "$CHANNEL" = "nightly" ]; then
TIMESTAMP=$(date +%s)
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
else
VERSION="$BASE_VERSION"
fi
echo "Base version: $BASE_VERSION"
echo "Channel: $CHANNEL"
echo "Publish version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update all package versions and lockfile
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: bun sdk/scripts/version.ts "$VERSION"
- name: Verify publishability
if: steps.check_commits.outputs.skip != 'true'
run: bun sdk/scripts/check-publish.ts
- name: Prepare package tarball directory
if: steps.check_commits.outputs.skip != 'true'
run: mkdir -p "$RUNNER_TEMP/sdk-npm-packs"
# Pack with Bun so workspace/catalog protocols are resolved in the tarball,
# then publish that tarball with npm so npm trusted publishing can use GitHub OIDC.
# Publish sequentially in dependency order: shared → llms → agents → core → sdk
- name: Publish @cline/shared
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/shared@${VERSION} with tag '${CHANNEL}'..."
cd sdk/packages/shared
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/llms
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/llms@${VERSION} with tag '${CHANNEL}'..."
cd sdk/packages/llms
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/agents
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/agents@${VERSION} with tag '${CHANNEL}'..."
cd sdk/packages/agents
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/core
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/core@${VERSION} with tag '${CHANNEL}'..."
cd sdk/packages/core
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Publish @cline/sdk
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
CHANNEL: ${{ steps.channel.outputs.channel }}
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "Publishing @cline/sdk@${VERSION} with tag '${CHANNEL}'..."
cd sdk/packages/sdk
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
- name: Create package tags for production publish
if: steps.check_commits.outputs.skip != 'true' && steps.channel.outputs.channel == 'latest'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
for PKG in shared llms agents core sdk; do
TAG="sdk/${PKG}/v${VERSION}"
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "Tag already exists locally: ${TAG}"
else
git tag -a "${TAG}" -m "@cline/${PKG}@${VERSION}"
echo "Created tag: ${TAG}"
fi
# Ensure remote has the tag; this is idempotent if tag already exists remotely.
git push origin "refs/tags/${TAG}"
done
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
CHANNEL: ${{ steps.channel.outputs.channel }}
run: |
echo "Published SDK packages with tag '${CHANNEL}':"
echo " - @cline/shared@${VERSION}"
echo " - @cline/llms@${VERSION}"
echo " - @cline/agents@${VERSION}"
echo " - @cline/core@${VERSION}"
echo " - @cline/sdk@${VERSION}"
if [ "$CHANNEL" = "latest" ]; then
echo "Created git tags:"
echo " - sdk/shared/v${VERSION}"
echo " - sdk/llms/v${VERSION}"
echo " - sdk/agents/v${VERSION}"
echo " - sdk/core/v${VERSION}"
echo " - sdk/sdk/v${VERSION}"
fi
-112
View File
@@ -1,112 +0,0 @@
name: sdk-test
on:
push:
branches:
- main
paths:
- "sdk/**"
- ".github/workflows/sdk-test.yml"
workflow_dispatch:
pull_request:
branches:
- main
paths:
- "sdk/**"
- ".github/workflows/sdk-test.yml"
workflow_call:
permissions:
contents: read
defaults:
run:
working-directory: .
jobs:
quality-checks:
runs-on: ubuntu-latest
name: Quality Checks
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Install dependencies
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Typecheck
run: |
bun run build:sdk
bun run -F @cline/cli build
bun run types
- name: Lint & Format
run: bun run lint
test:
needs: quality-checks
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
node-version: "24.x"
- os: windows-latest
node-version: "24.x"
runs-on: ${{ matrix.os }}
name: Test (${{ matrix.os }}, Node ${{ matrix.node-version }})
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.13"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: bun install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build SDK
id: build_sdk_step
run: bun run build:sdk
- name: Build CLI
id: build_cli_step
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' }}
run: bun -F @cline/cli build
- name: Run Tests
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os != 'windows-latest' }}
run: bun run test
- name: Run SDK Tests (Windows)
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'windows-latest' }}
run: bun -F './sdk/packages/**' test
- name: Smoke test SQLite under Node
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os != 'windows-latest' }}
timeout-minutes: 10
run: bun sdk/scripts/ci-node-smoke.ts
- name: Run TUI e2e tests
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
run: bun -F @cline/cli test:e2e:cli:tui
- name: Verify packages are publishable
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
run: bun sdk/scripts/check-publish.ts
@@ -1,6 +1,6 @@
# This workflow will only label and/or close 30 issues at a time in order to avoid exceeding a rate limit.
# More info: https://docs.github.com/en/actions/use-cases-and-examples/project-management/closing-inactive-issues
name: repo-stale-issues
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
+32
View File
@@ -0,0 +1,32 @@
name: Test Stale Issues Workflow
on:
workflow_dispatch:
inputs:
days-before-stale:
description: "Days before an issue becomes stale"
required: true
default: "1"
days-before-close:
description: "Days before a stale issue is closed"
required: true
default: "1"
jobs:
test-stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@28ca103
with:
days-before-issue-stale: ${{ github.event.inputs.days-before-stale }}
days-before-issue-close: ${{ github.event.inputs.days-before-close }}
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for ${{ github.event.inputs.days-before-stale }} days with no activity."
close-issue-message: "This issue was closed because it has been inactive for ${{ github.event.inputs.days-before-close }} days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
exempt-issue-labels: "pinned,security"
repo-token: ${{ secrets.GITHUB_TOKEN }}
debug-only: true
+224
View File
@@ -0,0 +1,224 @@
name: Tests
on:
push:
branches:
- main
workflow_dispatch:
pull_request:
branches:
- main
workflow_call:
# Set default permissions for all jobs
permissions:
contents: read # Needed to check out code
checks: write # Needed to report test results
pull-requests: write # Needed to add comments/annotations to PRs
jobs:
quality-checks:
runs-on: ubuntu-latest
name: Quality Checks
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Run Quality Checks (Parallel)
run: npm run ci:check-all
test:
needs: quality-checks
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
name: ${{ matrix.os == 'ubuntu-latest' && 'test' || format('test ({0})', matrix.os) }}
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Set up NPM on Windows
if: runner.os == 'Windows'
run: |
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
# Build the extension and tests (without redundant checks)
- name: Build Tests and Extension
id: build_step
run: npm run ci:build
- name: Unit Tests with coverage - Linux
id: unit_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: |
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
- name: Unit Tests - Non-Linux
id: unit_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: |
npm run test:unit
- name: Extension Integration Tests - Linux
id: integration_tests_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
run: xvfb-run -a npm run test:coverage
- name: Extension Integration Tests - Non-Linux
id: integration_tests_non_linux
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
run: npm run test:integration
- name: Webview Tests with Coverage
id: webview_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: |
cd webview-ui
npm run test:coverage
- name: CLI Tests
id: cli_tests
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
run: cd cli && npm run test:run
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
# Only upload artifacts on Linux - We only need coverage from one OS
if: runner.os == 'Linux'
with:
name: pr-coverage-reports
path: |
coverage-unit/lcov.info
webview-ui/coverage/lcov.info
test-platform-integration:
needs: quality-checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
cache-dependency-path: |
package-lock.json
webview-ui/package-lock.json
testing-platform/package-lock.json
- name: Install root dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install webview-ui dependencies
run: cd webview-ui && npm ci
- name: Download ripgrep binaries
run: npm run download-ripgrep
- name: Compile Standalone
run: npm run compile-standalone
- name: Install testing platform dependencies
run: cd testing-platform && npm ci
- name: Running testing platform integration spec tests
timeout-minutes: 7
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
- name: Save Coverage Reports
uses: actions/upload-artifact@v4
with:
name: test-platform-integration-core-coverage
path: coverage/**/lcov.info
qlty:
needs: [test, test-platform-integration]
runs-on: ubuntu-latest
# Run on PRs to main, pushes to main, and manual dispatches
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download unit tests coverage reports
uses: actions/download-artifact@v4
with:
name: pr-coverage-reports
path: .
- name: Upload core unit tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
coverage-unit/lcov.info
tag: unit:core
- name: Upload webview-ui unit tests coverage to Qlty
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
# we can merge multiple files if necessary
files: |
webview-ui/coverage/lcov.info
tag: unit:webview-ui
add-prefix: webview-ui/
- name: Download test platform integration core coverage artifact
uses: actions/download-artifact@v4
continue-on-error: true
id: download-integration-coverage
with:
name: test-platform-integration-core-coverage
path: integration-core-coverage-reports
- name: Upload core integration tests coverage to Qlty
if: steps.download-integration-coverage.outcome == 'success'
uses: qltysh/qlty-action/coverage@v2
with:
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
files: integration-core-coverage-reports/**/lcov.info
tag: integration:core
@@ -1,4 +1,4 @@
name: ext-jb-test-integration
name: Trigger Jetbrains Plugin <-> Cline Tests
on:
pull_request_target:
types: [opened, reopened]
@@ -15,11 +15,9 @@ jobs:
trigger-integration-test:
name: Run Tests
runs-on: ubuntu-latest
# Auto-run only for trusted PR authors. Anyone else needs a maintainer
# to opt their PR in by commenting /test-jetbrains.
# Run on PR open/reopen, or when someone comments /test-jetbrains on a PR
if: |
(github.event_name == 'pull_request_target' &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.pull_request.author_association)) ||
github.event_name == 'pull_request_target' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/test-jetbrains') &&
@@ -29,8 +27,8 @@ jobs:
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.CLINE_JETBRAINS_APP_ID }}
private-key: ${{ secrets.CLINE_JETBRAINS_APP_KEY }}
app-id: 1998650
private-key: ${{ secrets.CLINE_JETBRAINS_WORKFLOW_KEY }}
owner: cline
repositories: intellij-plugin
+5 -30
View File
@@ -17,8 +17,8 @@ pnpm-lock.yaml
.actrc
CLAUDE.local.md
apps/vscode/webview-ui/src/**/*.js
apps/vscode/webview-ui/src/**/*.js.map
webview-ui/src/**/*.js
webview-ui/src/**/*.js.map
# Ignore coverage directories and files
coverage
@@ -35,9 +35,9 @@ coverage-unit
.worktrees
## Generated files ##
apps/vscode/src/generated/
apps/vscode/src/shared/proto/
apps/vscode/webview-ui/src/services/grpc-client.ts
src/generated/
src/shared/proto/
webview-ui/src/services/grpc-client.ts
*.tsbuildinfo
# E2E Tests
@@ -56,28 +56,3 @@ evals/smoke-tests/results/
secrets.json
tui-traces
tests/**/cache
# Backup created by scripts/marketplace-readme.mjs while publishing.
# Should never be committed: only exists if a publish aborts mid-swap.
.README.github.bak
# Tauri generated code
apps/*/src-tauri/gen
apps/*/src-tauri/bin
apps/examples/*/src-tauri/gen
apps/examples/*/src-tauri/bin
# Tauri UI test snapshots
apps/*/src/tests/.tui-test
apps/*/src/tests/tui-traces
apps/vscode/webview-ui/src/**/*.js
apps/vscode/webview-ui/src/**/*.js.map
# SDK Session files / User data
.cline/data
.cline/tmp
*.db
*.db-shm
*.db-wal
.cline/**/managed.json
.cline/**/bundle.json
-4
View File
@@ -1,4 +0,0 @@
title = "Cline SDK secret scanning"
[extend]
useDefault = true
-52
View File
@@ -1,52 +0,0 @@
{
"strictness": 2,
"triggerOnUpdates": true,
"statusCheck": true,
"rules": [
{
"id": "sdk-tool-handler-telemetry",
"rule": "Any new tool handler added to packages/agents/src or packages/core/src that performs a user-visible action (writes files, executes commands, modifies state, calls external APIs) must include a call to captureToolUsage() from packages/core/src/services/telemetry/core-events.ts, or emit a task.tool_used event via telemetry.capture(). Pure read-only helpers and getters are exempt. When in doubt, prefer instrumentation.",
"scope": [
"sdk/packages/agents/src/**",
"sdk/packages/core/src/**"
],
"severity": "high"
},
{
"id": "sdk-session-lifecycle-telemetry",
"rule": "New session start, end, or state-transition code paths in packages/core/src must call the appropriate typed helper from packages/core/src/services/telemetry/core-events.ts (captureTaskCreated, captureTaskCompleted, captureConversationTurnEvent, captureTokenUsage, etc.). Do not inline raw telemetry.capture() calls for session lifecycle events — always use the typed helper, which guarantees a consistent payload shape.",
"scope": [
"sdk/packages/core/src/cline-core/**",
"sdk/packages/core/src/runtime/**"
],
"severity": "high"
},
{
"id": "sdk-no-raw-event-strings",
"rule": "All telemetry event name strings must be sourced from CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts. If a PR introduces a string literal in a telemetry.capture(), telemetry.captureRequired(), or recordCounter()/recordHistogram()/recordGauge() call that does not reference CORE_TELEMETRY_EVENTS, flag it. New events must be added to CORE_TELEMETRY_EVENTS first, with a typed capture helper created alongside them.",
"scope": [
"sdk/packages/core/src/**",
"sdk/packages/agents/src/**",
"apps/cli/src/**",
"apps/vscode/src/**"
],
"severity": "medium"
},
{
"id": "sdk-auth-telemetry-completeness",
"rule": "Any new OAuth or authentication provider added under packages/core/src/auth must emit all four lifecycle events using the typed helpers from core-events.ts: captureAuthStarted (at flow entry), captureAuthSucceeded + identifyAccount (on token success), captureAuthFailed (on error), and captureAuthLoggedOut (on token invalidation or explicit logout). Flag PRs that introduce a new auth flow file without all four. Cross-reference packages/core/src/auth/cline.ts and packages/core/src/auth/codex.ts as canonical examples.",
"scope": [
"sdk/packages/core/src/auth/**"
],
"severity": "high"
},
{
"id": "sdk-telemetry-doc-update",
"rule": "Any PR that adds new event constants to CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts, adds new capture* helper functions, or changes the payload shape of an existing event must update the Event Catalog section in DOC.md. Flag PRs that modify core-events.ts without a corresponding change to DOC.md.",
"scope": [
"sdk/packages/core/src/services/telemetry/core-events.ts"
],
"severity": "medium"
}
]
}
-32
View File
@@ -1,32 +0,0 @@
{
"files": [
{
"path": "sdk/packages/core/src/services/telemetry/core-events.ts",
"description": "Single source of truth for all telemetry event names (CORE_TELEMETRY_EVENTS) and their typed capture helper functions. Every PR touching telemetry must be evaluated against this catalog. New events must be defined here first."
},
{
"path": "sdk/packages/shared/src/services/telemetry.ts",
"description": "ITelemetryService interface definition. Defines the contract all telemetry implementations must satisfy (capture, captureRequired, recordCounter, recordHistogram, recordGauge, flush, dispose)."
},
{
"path": "sdk/packages/core/src/services/telemetry/TelemetryService.ts",
"description": "Reference implementation of ITelemetryService used by all hosts. Multi-adapter fan-out service that forwards events to OpenTelemetry."
},
{
"path": "sdk/packages/core/src/services/telemetry/OpenTelemetryProvider.ts",
"description": "OpenTelemetry-backed provider that wires logs/metrics/traces exporters. Contains createConfiguredTelemetryService and createConfiguredTelemetryHandle, the canonical factories every host should use."
},
{
"path": "DOC.md",
"description": "Public API and event documentation. The Event Catalog and 'Activation funnel' sections must be kept in sync with core-events.ts. Host integration rules (CLI dir ordering, hub daemon metadata forwarding) are documented here."
},
{
"path": "sdk/ARCHITECTURE.md",
"description": "Architecture reference. Telemetry design decisions, completion semantics (submit_and_exit anchoring), and hub-daemon telemetry forwarding are documented here. Use as ground truth for design intent."
},
{
"path": "sdk/AGENTS.md",
"description": "Package boundary rules. Telemetry runtime services live in @cline/core; @cline/agents must not own stateful telemetry. Use to evaluate whether a telemetry change is being made in the correct package."
}
]
}
-142
View File
@@ -1,142 +0,0 @@
# SDK Telemetry Standards
These rules supplement `config.json`. The structured rules describe **what** to enforce; this
document explains **why**, so Greptile has the context to avoid false positives.
## Telemetry Stack
The SDK uses OpenTelemetry (OTEL) as its sole telemetry transport. Events flow through:
```
core-events.ts (event catalog + typed helpers)
ITelemetryService (sdk/packages/shared) ← interface contract
TelemetryService (sdk/packages/core) ← multi-adapter fan-out
OpenTelemetryAdapter → OpenTelemetryProvider ← OTLP transport
OTLP endpoint (collector or vendor)
```
The SDK does **not** depend on the original `cline/cline` repo for telemetry. The two have
parallel-but-independent stacks; this `.greptile/` config covers only the SDK.
## The Single Source of Truth
`sdk/packages/core/src/services/telemetry/core-events.ts` is the single source of truth for all
event names. It exports:
- `CORE_TELEMETRY_EVENTS` — a frozen const object grouped by family
(`CLIENT`, `SESSION`, `USER`, `TASK`, `HOOKS`, `WORKSPACE`)
- A typed `capture*()` helper for every event family
(`captureExtensionActivated`, `captureTaskCreated`, `captureToolUsage`, etc.)
**Never use raw string literals for event names at call sites.** A new event always means:
1. Add the constant to `CORE_TELEMETRY_EVENTS`
2. Add a typed `capture*()` helper alongside it (with a typed `properties` parameter)
3. Update the Event Catalog section in `DOC.md`
4. Add a unit test in `core-events.test.ts` asserting the event is dropped when telemetry is opted out
## The Activation Funnel
The canonical funnel that downstream analytics depends on:
```
user.extension_activated
→ workspace.initialized
→ workspace.path_resolved (gated on multi-root)
→ task.created
→ task.conversation_turn (one per turn, source: "user" | "assistant")
→ task.completed (source: "submit_and_exit" | "shutdown")
```
Emission ownership:
- `user.extension_activated`: emitted **once per host process** by host-specific helpers
(`captureCliExtensionActivated` for the CLI, `captureExtensionActivated` for VS Code).
- `workspace.initialized` / `workspace.init_error`: emitted by a per-process de-duplicated
emitter in `prepareLocalRuntimeBootstrap`. Hosts must NOT re-emit these.
- `workspace.path_resolved`: emitted from default tool executors **only when**
`WorkspaceManager` exposes more than one root.
- `task.*`: emitted by core session lifecycle code in `sdk/packages/core/src/cline-core/` and
`sdk/packages/core/src/runtime/`. Hosts must not duplicate this emission.
## `task.completed` Semantics
`task.completed` marks the moment the **assistant declared the task done**, not the moment
the SDK session record was finalized. The local runtime emits it when it observes a successful
`submit_and_exit` tool call (the SDK analog of original Cline's `attempt_completion`). For
non-interactive runs that finish without invoking the explicit completion tool,
`shutdownSession` emits it as a fallback with `source: "shutdown"`.
Each session is guaranteed at most one `task.completed` emission. The `source` field
(`"submit_and_exit" | "shutdown"`) is required for analytics attribution.
## CLI Directory-Ordering Rule
The CLI accepts `--config <dir>`. The CLI **must** apply `setClineDir(...)` and
`setHomeDir(...)` from `@cline/shared/storage` **before** calling
`captureCliExtensionActivated()`. Otherwise the telemetry singleton's persisted distinct-id
and any other on-disk telemetry state lands under `~/.cline` instead of the user's chosen
config dir.
The canonical pattern is in `apps/cli/src/main.ts` (PR #357):
```ts
if (configDir) setClineDir(configDir);
setHomeDir(homedir());
captureCliExtensionActivated(); // <-- after dir overrides
```
## Hub Daemon Metadata Forwarding
Hosts that spawn a detached `@cline/core/hub/daemon-entry` process must forward telemetry
metadata into the daemon argv so the daemon can reconstruct an equivalent
`ITelemetryService`. The expected payload is base64-encoded JSON with snake_case keys:
```
{ extension_version, cline_type, platform, platform_version, os_type, os_version, is_remote_workspace }
```
The reference implementation is `apps/vscode/src/hub-daemon.ts` (PR #357). Without this
forwarding, hub-backed sessions silently drop their lifecycle telemetry.
## Auth Lifecycle Completeness
Every authentication provider in `sdk/packages/core/src/auth/` must emit all four auth lifecycle
events using the typed helpers:
| Phase | Helper | Where it fires |
|---|---|---|
| Flow entry | `captureAuthStarted(provider)` | Top of the OAuth flow function |
| Token success | `captureAuthSucceeded(provider)` + `identifyAccount(...)` | After successful token exchange |
| Token error | `captureAuthFailed(provider, errorMessage)` | In the catch block |
| Token invalidation | `captureAuthLoggedOut(provider, reason)` | On invalid_grant or explicit logout |
Cross-reference `sdk/packages/core/src/auth/cline.ts` and `sdk/packages/core/src/auth/codex.ts` as
canonical examples of all four phases.
## Single Telemetry Service Per Host
On VS Code, the telemetry handle is built **once** in `activate()`
(`apps/vscode/src/telemetry.ts`) and the same instance is passed into the sidebar, panel
command, and daemon spawn payload. Do not let individual controllers construct their own
`ITelemetryService` — that fragments distinct-id state, opt-out tracking, and flush ownership.
The CLI follows the same pattern via the `getCliTelemetryService()` singleton in
`apps/cli/src/utils/telemetry.ts`, which is memoized by the activation gate in
`telemetry.activation-gate.ts`.
## Common False-Positive Adjustments
If Greptile flags one of the following, the rule is **not** violated:
- A telemetry call that is wrapped in a host-specific helper (e.g.
`captureCliExtensionActivated` wrapping `captureExtensionActivated`) — the inner helper
is the typed call.
- `enterprise.*` events emitted from `apps/cli/src/utils/enterprise.ts` — these are
enterprise-side events not yet in `CORE_TELEMETRY_EVENTS`; they are tracked separately.
- A new test file that uses raw event name strings inside `expect(...)` assertions — tests
may reference event names as strings to assert what was emitted.
-9
View File
@@ -1,10 +1 @@
if ! command -v gitleaks >/dev/null 2>&1; then
echo "gitleaks is required for the pre-commit secret scan."
echo "Install it with: brew install gitleaks"
echo "Other install options: https://github.com/gitleaks/gitleaks#installing"
exit 1
fi
gitleaks git --pre-commit --redact --staged --verbose || exit 1
lint-staged
-9
View File
@@ -1,9 +0,0 @@
{
"shortcuts": [
{
"label": "Build & Link CLI",
"command": "bun -F @cline/cli build && bun -F @cline/cli link",
"icon": "play"
}
]
}
+1 -2
View File
@@ -3,8 +3,7 @@
"ts"
],
"spec": [
"src/**/__tests__/*.ts",
"src/test/services/**/*.test.ts"
"src/**/__tests__/*.ts"
],
"require": [
"ts-node/register",
+1 -1
View File
@@ -1 +1 @@
22
lts/*
-3
View File
@@ -1,3 +0,0 @@
bun 1.3.13
node 22
@@ -1,6 +1,5 @@
import { defineConfig } from "@vscode/test-cli"
import path from "path"
const vscodeTestVersion = process.env.VSCODE_TEST_VERSION ?? "stable"
export default defineConfig({
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
@@ -13,7 +12,7 @@ export default defineConfig({
require: ["./test-setup.js"],
},
workspaceFolder: "test-workspace",
version: vscodeTestVersion,
version: "stable",
extensionDevelopmentPath: path.resolve("./"),
launchArgs: ["--disable-extensions"],
})
+1 -2
View File
@@ -5,7 +5,6 @@
"connor4312.esbuild-problem-matchers",
"ms-vscode.extension-test-runner",
"bradlc.vscode-tailwindcss",
"biomejs.biome",
"oven.bun-vscode"
"biomejs.biome"
]
}
+31 -144
View File
@@ -10,23 +10,23 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}/apps/vscode",
"${workspaceFolder}",
"--disable-extensions"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
@@ -35,22 +35,22 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}/apps/vscode"
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "staging"
}
},
@@ -59,22 +59,22 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-workspace-trust",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"${workspaceFolder}/apps/vscode"
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "local"
}
},
@@ -84,27 +84,27 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--user-data-dir=${workspaceFolder}/apps/vscode/dist/tmp/user",
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
"--profile-temp",
"--sync=off",
"--disable-extension",
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
"--disable-extension",
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
"${workspaceFolder}/apps/vscode"
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js"
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
@@ -117,13 +117,13 @@
],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/apps/vscode/**",
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}/apps/vscode",
"cwd": "${workspaceFolder}",
"outFiles": [
"${workspaceFolder}/apps/vscode/dist/**/*.js",
"${workspaceFolder}/apps/vscode/dist-standalone/**/*.js"
"${workspaceFolder}/dist/**/*.js",
"${workspaceFolder}/dist-standalone/**/*.js"
],
"preLaunchTask": "compile-standalone",
"runtimeExecutable": "npx",
@@ -131,11 +131,11 @@
"tsx"
],
"program": "scripts/test-standalone-core-api-server.ts",
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"PROTOBUS_PORT": "26040",
"HOSTBRIDGE_PORT": "26041",
"WORKSPACE_DIR": "${workspaceFolder}/apps/vscode",
"WORKSPACE_DIR": "${workspaceFolder}",
"E2E_TEST": "true",
"CLINE_ENVIRONMENT": "local"
},
@@ -151,10 +151,10 @@
],
"sourceMaps": true,
"resolveSourceMapLocations": [
"${workspaceFolder}/apps/vscode/**",
"${workspaceFolder}/**",
"!**/node_modules/**"
],
"cwd": "${workspaceFolder}/apps/vscode",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npx",
"runtimeArgs": [
"mocha"
@@ -169,7 +169,7 @@
"--exit",
"${file}"
],
"envFile": "${workspaceFolder}/apps/vscode/.env",
"envFile": "${workspaceFolder}/.env",
"env": {
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
"NODE_ENV": "test",
@@ -188,7 +188,7 @@
"run",
"storybook"
],
"cwd": "${workspaceFolder}/apps/vscode/webview-ui",
"cwd": "${workspaceFolder}/webview-ui",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"serverReadyAction": {
@@ -199,119 +199,6 @@
"env": {
"IS_DEV": "true"
}
},
{
"name": "Launch Bun CLI (Prompt)",
"type": "bun",
"request": "launch",
"cwd": "${workspaceFolder}/sdk/apps/cli",
"runtime": "bun",
"runtimeArgs": [
"--conditions=development"
],
"program": "${workspaceFolder}/sdk/apps/cli/src/index.ts",
"args": [
"${input:cliPrompt}"
],
"env": {
"CLINE_BUILD_ENV": "development"
},
"presentation": {
"hidden": true
}
},
{
"name": "Launch RPC Server",
"type": "bun",
"request": "launch",
"cwd": "${workspaceFolder}/sdk/apps/cli",
"runtime": "bun",
"runtimeArgs": [
"--conditions=development"
],
"program": "${workspaceFolder}/sdk/apps/cli/src/index.ts",
"args": [
"rpc",
"start"
],
"env": {
"CLINE_BUILD_ENV": "development",
"CLINE_DEBUG_PORT_BASE": "9230"
},
"presentation": {
"hidden": true
}
},
{
"name": "Attach RPC Runtime (9230)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9230",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
},
{
"name": "Attach Hook Worker (9231)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9231",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
},
{
"name": "Attach Plugin Sandbox (9232)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9232",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
},
{
"name": "Attach Connector Child (9233)",
"type": "bun",
"request": "attach",
"url": "ws://127.0.0.1:9233",
"localRoot": "${workspaceFolder}/sdk",
"remoteRoot": "${workspaceFolder}/sdk",
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Launch RPC Server Debugger",
"configurations": [
"Launch RPC Server",
"Attach RPC Runtime (9230)"
]
},
{
"name": "Launch CLI Debugger",
"configurations": [
"Launch Bun CLI (Prompt)",
"Attach RPC Runtime (9230)",
"Attach Hook Worker (9231)",
"Attach Plugin Sandbox (9232)",
"Attach Connector Child (9233)"
]
}
],
"inputs": [
{
"id": "cliPrompt",
"type": "promptString",
"description": "Prompt to send to the CLI",
"default": "hey"
}
]
}
+1 -2
View File
@@ -1,6 +1,5 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.insertFinalNewline": true,
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
@@ -17,7 +16,7 @@
// Protobuf settings
"protoc": {
"options": [
"--proto_path=apps/vscode/proto"
"--proto_path=proto"
]
},
// Enable Lint and format using Biome
+22 -50
View File
@@ -5,28 +5,24 @@
"tasks": [
{
"label": "compile-standalone",
"type": "shell",
"command": "npm run compile-standalone",
"type": "npm",
"script": "compile-standalone",
"group": "build",
"problemMatcher": [],
"presentation": {
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode"
}
},
{
"label": "npm: protos",
"type": "shell",
"command": "npm run protos",
"type": "npm",
"script": "protos",
"problemMatcher": [],
"isBackground": false,
"presentation": {
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
@@ -64,8 +60,8 @@
"group": "build"
},
{
"type": "shell",
"command": "npm run build:webview",
"type": "npm",
"script": "build:webview",
"group": "build",
"problemMatcher": [],
"isBackground": true,
@@ -78,15 +74,14 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
}
},
{
"type": "shell",
"command": "npm run build:webview:test",
"type": "npm",
"script": "build:webview:test",
"group": "build",
"problemMatcher": [],
"isBackground": true,
@@ -99,7 +94,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true",
"IS_TEST": "true"
@@ -107,8 +101,8 @@
}
},
{
"type": "shell",
"command": "npm run dev:webview",
"type": "npm",
"script": "dev:webview",
"group": "build",
"problemMatcher": [
{
@@ -137,15 +131,14 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
}
},
{
"type": "shell",
"command": "npm run watch:esbuild",
"type": "npm",
"script": "watch:esbuild",
"group": "build",
"problemMatcher": {
"pattern": [
@@ -176,15 +169,14 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
}
},
{
"type": "shell",
"command": "npm run watch:esbuild:test",
"type": "npm",
"script": "watch:esbuild:test",
"group": "build",
"problemMatcher": {
"pattern": [
@@ -215,7 +207,6 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true",
"IS_TEST": "true"
@@ -223,8 +214,8 @@
}
},
{
"type": "shell",
"command": "npm run watch:tsc",
"type": "npm",
"script": "watch:tsc",
"group": "build",
"problemMatcher": "$tsc-watch",
"isBackground": true,
@@ -235,15 +226,11 @@
"presentation": {
"group": "watch",
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode"
}
},
{
"type": "shell",
"command": "npm run watch-tests",
"label": "npm: watch-tests",
"type": "npm",
"script": "watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"dependsOn": [
@@ -253,10 +240,7 @@
"reveal": "always",
"group": "watchers"
},
"group": "build",
"options": {
"cwd": "${workspaceFolder}/apps/vscode"
}
"group": "build"
},
{
"label": "tasks: watch-tests",
@@ -278,11 +262,11 @@
"dependsOn": [
"watch"
],
"command": "rm -rf ${workspaceFolder}/apps/vscode/dist/tmp/user && mkdir -p ${workspaceFolder}/apps/vscode/dist/tmp/user"
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
},
{
"type": "shell",
"command": "npm run storybook",
"type": "npm",
"script": "storybook",
"group": "build",
"problemMatcher": [],
"isBackground": false,
@@ -295,22 +279,10 @@
"reveal": "always"
},
"options": {
"cwd": "${workspaceFolder}/apps/vscode",
"env": {
"IS_DEV": "true"
}
}
},
{
"label": "build-sdk",
"type": "shell",
"command": "bun run build:sdk",
"problemMatcher": [
"$tsc"
],
"options": {
"cwd": "${workspaceFolder}"
}
}
],
"inputs": [
+2 -12
View File
@@ -2,10 +2,6 @@
.vscode/**
.vscode-test/**
.worktrees/**
# Agent tooling, never shipped in the VSIX
.agents/**
.claude/**
.codex/**
CLAUDE.local.md
out/
dist-standalone/
@@ -26,14 +22,8 @@ eslint-rules/**
.husky/**
.env
# sdk (separate monorepo with its own build/release pipeline)
sdk/**
# Source-of-truth for the marketplace README (the .vsix only ever sees the
# README.md that scripts/marketplace-readme.mjs swaps into place). The backup
# only exists if a publish aborts mid-swap; neither should ship in the .vsix.
README.marketplace.md
.README.github.bak
# cli
cli/**
# Custom
**/demo.gif
-238
View File
@@ -1,243 +1,5 @@
# Changelog
## [3.89.2]
### Fixed
- Complete the fix for the Anthropic provider on VS Code 1.123 and later by upgrading the bundled Anthropic SDK to a release compatible with the Node 24 runtime.
- Update the Vertex AI provider to a compatible Anthropic Vertex SDK release so it works with the upgraded Anthropic SDK.
## [3.89.1]
### Fixed
- Restore the Anthropic provider on VS Code 1.123 and later, where the updated Node 24 runtime broke the bundled Anthropic SDK.
- Handle the DeepSeek V4 reasoning format.
## [3.89.0]
### Added
- Add Claude Fable 5 model support.
### Fixed
- Fix MiniMax M3 thinking controls across gateways.
### Changed
- Clean up the Codex model list.
## [3.88.1]
### Added
- Add a debug section in settings for Cline testers.
### Fixed
- Include the walkthrough markdown files in the VS Code extension package so the first-run walkthrough steps load correctly.
## [3.88.0]
### Added
- Add the latest Fireworks AI serverless models and update the default Fireworks model to Kimi K2.6.
### Fixed
- Fix MCP server delete/add flows so settings writes do not cause the MCP server list to be emptied by the file watcher.
- Remove stale Fireworks AI models and correct Fireworks model metadata and cache pricing.
### Changed
- Always use the upstream Cline recommended models endpoint instead of gating it behind a feature flag.
## [3.87.0]
### Added
- Add MiniMax M3 model support.
### Fixed
- Update VS Code extension dependencies to resolve security issues in `@xmldom/xmldom`, `basic-ftp`, `axios`, `undici`, and other direct/transitive packages.
## [3.86.2]
### Fixed
- Fix `@` file mentions and workspace file search on VS Code 1.122+ by resolving the new bundled `@vscode/ripgrep-universal` per-platform binary layout before falling back to legacy ripgrep paths.
## [3.86.1]
### Fixed
- Fix `@` file mentions failing to find files in some environments (notably VS Code Remote SSH, and after certain VS Code updates) by keeping the file-search fallback alive when the workspace index or bundled ripgrep binary is unavailable.
## [3.86.0]
### Added
- Add Claude Opus 4.8 provider support, including 1M-context variants where available.
- Add Moonshot Kimi K2.6 model support.
### Fixed
- Show prompt-cache support for Qwen 3.7 Max in the Cline provider.
- Fix the VS Code nightly publish workflow startup permissions.
### Changed
- Move the VS Code extension project into `apps/vscode`.
## [3.85.0]
### Added
- Add GPT-5.5 support to SAP AI Core.
- Add DeepSeek V4 Flash and Pro models.
- Add Gemini 3.5 Flash to Gemini and Vertex providers.
- Add `/lg-task` URI webhook integration for LG dashboard flows.
### Fixed
- Fix Vertex AI global endpoint handling for Claude models.
- Route Poolside Laguna models through next-gen prompts and native tool calling.
### Changed
- Update `diff` and `protobufjs` dependencies.
## [3.84.0]
### Added
- Add SAP AI Core support for additional hosted models
### Fixed
- Disable the MCP "Restart Server" button when a server is toggled off.
### Changed
- Remove the Cline Kanban launch modal and bundled demo media from the VS Code extension startup flow.
## [3.83.0]
### Fixed
- Show a clear "Searching..." state in the @-mention file picker
- Improve @-mention file search performance
- Allow `write_to_file` to create or overwrite files with empty content.
- Fix validation failures for MCP servers that require an object.
- Enable OpenRouter prompt cache control for Qwen models.
- Update Axios and SAP Connectivity dependencies
### Changed
- Use the VS Code-specific `README.marketplace.md` when packaging and publishing the VS Code extension
- Add telemetry to @-mention search to help diagnose local, remote, and multi-root workspace search behavior.
## [3.82.0]
### Added
- Restore VS Code foreground terminal support and settings.
- Add latest OpenAI, SAP AI Core, and Z AI models.
### Fixed
- Fix hook template JSON escaping.
- Improve ripgrep file search error handling.
### Changed
- Remove hardcoded model lists from docs.
## [3.81.0]
### Added
- Add GPT-5.5 model support for OpenAI Codex subscription users.
### Fixed
- Remove hardcoded "Whats New" fallback items in webview; only remote-configured welcome banners are shown.
### Changed
- Improve cline-core memory diagnostics used by the extension runtime:
- enable near-heap-limit heap snapshots
- add periodic memory usage logging
- log discovered heap snapshots on abnormal exits for easier OOM debugging
## [3.80.0]
### Added
- Wire up remote `globalSkills` from enterprise remote config with full UI, toggle support, and system prompt integration — enterprise-managed skills now appear under a dedicated "Enterprise Skills" section and support `alwaysEnabled` enforcement
- Onboarding flow now uses dynamically fetched recommended models instead of a hardcoded list, with a fallback to the welcome view on failure
- Add dedicated "Quota Exceeded" error message in the chat error UI when Cline account spend caps are hit
### Fixed
- Fix OOM crashes during long conversations by setting `--max-old-space-size=8192` for the cline-core Node.js process (was defaulting to ~2 GB)
- Show detailed error information in the chat error row instead of a generic caught error message
- Update `axios` to 1.15.0 across all packages
### Changed
- Remove foreground terminal mode — all task command execution now defaults to background mode, removing the VS Code integrated terminal dependency and related settings UI
- Remove old hardcoded announcement banners
## [3.79.0]
### Added
- Add Claude Opus 4.7 model support
- Add Azure Blob Storage as a storage provider
- Add `globalSkills` to remote config
- Inline value reuse in user-level remote-config discovery
### Fixed
- Fix cache reflection for Cline and Vercel API handlers
- Fix stuck `command_output` ask when terminal command ends unexpectedly
- Add `use_subagents` to system prompt for GLM, Hermes, and XS models
- Fix action injection security risk
### Changed
- Remove deprecated evals tool
## [3.78.0]
### Added
- Add a dedicated "Spend Limit Reached" error UI when spend caps are hit
- Docs updates
### Fixed
- Show actual `read_file` line ranges in chat UI
## [3.77.0]
### Added
- Add "Lazy Teammate Mode" experimental toggle
- `read_file` tool now supports chunked reading for targeted file access
### Fixed
- Exclude `new_task` tool from system prompt in yolo/headless mode
- Fix Kanban demo video formatting
### Changed
- Polish `Notification` hook functionality
## [3.76.0]
### Added
+1
View File
@@ -1,2 +1,3 @@
@.clinerules/general.md
@.clinerules/network.md
@.clinerules/cli.md
+8 -8
View File
@@ -42,14 +42,15 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
```bash
code cline
```
3. Install [bun](https://bun.com)
4. Install the necessary dependencies for the extension and webview-gui:
3. Install the necessary dependencies for the extension and webview-gui:
```bash
cd apps/vscode && npm run install:all && cd ../..
cd sdk && bun run build && cd ..
npm run install:all
```
5. Generate Protocol Buffer files (required before first build):
6. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
4. Generate Protocol Buffer files (required before first build):
```bash
npm run protos
```
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
@@ -61,7 +62,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
2. Push your branch and create a PR on GitHub. Our CI will:
- Run tests and checks
3. Testing
- Run `cd apps/vscode && npm run test` to run tests locally.
- Run `npm run test` to run tests locally.
- Before submitting PR, run `npm run format:fix` to format your code
### Extension
@@ -73,7 +74,6 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
- If you dismissed the prompts, you can install them manually from the Extensions panel
2. **Local Development**
- cd into the vscode extension, `cd apps/vscode`
- Run `npm run install:all` to install dependencies
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
- Run `npm run test` to run tests locally
+84 -172
View File
@@ -1,20 +1,18 @@
<p align="center">
<img src="assets/icons/icon.png" width="80" alt="Cline" />
</p>
<div align="center"><sub>
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
<h1 align="center">Cline</h1>
# Cline
<p align="center">
The open source coding agent in your IDE and terminal.
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
</p>
<div align="center">
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://docs.cline.bot" target="_blank"><strong>Docs</strong></a>
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
@@ -26,213 +24,127 @@ The open source coding agent in your IDE and terminal.
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://cline.bot/join-us" target="_blank"><strong>Join us!</strong></a>
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
</td>
</tbody>
</table>
</div>
</div>
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
<br>
Thanks to [Claude Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
<div align="center">
<table>
<tr>
<td align="center" width="50%">
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
3. Once Cline has the information he needs, he can:
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
### CLI
Run Cline in your terminal.
Interactive chat or fully headless
for CI/CD and scripting.
```
npm i -g cline
```
<a href="./apps/cli/README.md">Learn more</a>
<br><br>
</td>
<td align="center" width="50%">
### Kanban
Run many agents in parallel from a
web-based task board. Each card gets its own
worktree, auto-commit, and dependency chains.
```
npm i -g kanban
```
<a href="https://github.com/cline/kanban">Learn more</a>
<br><br>
</td>
</tr>
<tr>
<td align="center" width="50%">
### VS Code Extension
AI coding assistant in your editor.
Create files, run commands, browse the web,
and use tools with human-in-the-loop approval.
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev">Install from VS Marketplace</a>
<br><br>
</td>
<td align="center" width="50%">
### JetBrains Plugin
The same Cline experience in IntelliJ IDEA,
PyCharm, WebStorm, GoLand, and the rest of
the JetBrains family.
<a href="https://plugins.jetbrains.com/plugin/28247-cline">Install from JetBrains Marketplace</a>
<br><br>
</td>
</tr>
</table>
</div>
<div align="center">
<table>
<tr>
<td align="center">
### SDK
Build your own AI agents and integrations powered by the same engine that runs the CLI, Kanban, VS Code extension, and JetBrains plugin. Custom tools, multi-agent teams, connectors, scheduled automations, and more.
```
npm install @cline/sdk
```
<a href="https://docs.cline.bot/cline-sdk/overview">Documentation</a>
<br><br>
</td>
</tr>
</table>
</div>
> [!TIP]
> Follow [this guide](https://docs.cline.bot/features/customization/opening-cline-in-sidebar) to open Cline on the right side of your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
---
## Index
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
| Product | Description | Location | CHANGELOG |
|---------|------------|--------------|--------------|
| **SDK** | Node.js programmatic agent API and extension exports. | [`sdk/`](https://github.com/cline/cline/tree/main/sdk) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/CHANGELOG.md) |
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`apps/cli/`](https://github.com/cline/cline/tree/main/apps/cli) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/apps/cli/CHANGELOG.md) |
| **VS Code Extension** | The Marketplace extension and extension host integration. | [`/`](https://github.com/cline/cline/tree/main) (WIP migrating) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/CHANGELOG.md) |
| **JetBrains Plugin** | JetBrains-hosted client that talks to the shared agent core. | Currently we are not open-sourcing JetBrains plugins | - |
| **Kanban** | Web-based multi-agent task board. | [`cline/kanban`](https://github.com/cline/kanban) | [CHANGELOG.md](https://github.com/cline/kanban/blob/main/CHANGELOG.md) |
| **Docs site** | Public documentation pages. | [`docs/`](https://docs.cline.bot/) | - |
### Use any API and Model
## Edits Code Across Your Project
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
Cline reads your project structure, understands the relationships between files, and makes coordinated changes across your codebase. It monitors linter and compiler errors as it works, fixing issues like missing imports, type mismatches, and syntax errors before you even see them. In VS Code and JetBrains, every edit shows up as a diff you can review, modify, or revert. All changes are tracked with checkpoints, so you can easily undo the agent's work.
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
## Runs Bash Commands
<!-- Transparent pixel to create line break after floating image -->
Cline executes commands directly in your terminal and watches the output in real time. Install packages, run build scripts, execute tests, deploy applications, manage databases. For long-running processes like dev servers, Cline continues working in the background and reacts to new output as it appears, catching compile errors, test failures, and server crashes as they happen.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Plan and Act
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
Toggle between Plan mode and Act mode. In Plan mode, Cline explores your codebase, asks clarifying questions, and lays out a strategy. Once you're aligned, switch to Act mode and Cline executes the plan. Every file edit and terminal command requires your approval, so you stay in control of what actually changes. Or toggle auto-approve and let Cline run autonomously.
### Run Commands in Terminal
## Rules and Skills
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works hell be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
## Works With Every Model
<!-- Transparent pixel to create line break after floating image -->
Cline is not locked to a single AI provider. Use whichever model fits your workflow:
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
| Provider | Models |
|----------|--------|
| Anthropic | Claude Opus, Sonnet, Haiku |
| OpenAI | GPT series model |
| Google | Gemini series model |
| OpenRouter | 200+ models from any provider |
| Vercel AI Gateway | Models through Vercel AI Gateway |
| AWS Bedrock | Claude, Llama, and more |
| Azure / GCP Vertex | All hosted models |
| Cerebras / Groq | Fast inference models |
| Ollama / LM Studio | Run local models on your machine |
| Any OpenAI-compatible API | Self-hosted or third-party endpoints |
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
## Extend With Plugins or MCP Servers
### Create and Edit Files
Extend Cline's capabilities with plugins. Using the SDK, register tools and lifecycle hooks programmatically through the plugin system for logging, auditing, policy enforcement, or adding domain-specific capabilities. Simple plugin example below.
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
```typescript
import { Agent, createTool } from "@cline/sdk"
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
const deployTool = createTool({
name: "deploy",
description: "Deploy the current branch to staging.",
inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] },
execute: async (input) => {
// your deployment logic
},
})
<!-- Transparent pixel to create line break after floating image -->
const agent = new Agent({ tools: [deployTool], /* ... */ })
```
...or use [MCP servers](https://github.com/modelcontextprotocol) to connect to databases, query APIs, manage cloud infrastructure, and interact with external systems. Use [community-built servers](https://github.com/modelcontextprotocol/servers) or ask Cline to create custom tools on the fly. In the CLI, manage servers with `cline mcp`.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Multi-Agent Teams
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
Coordinate multiple agents working together on complex tasks. A coordinator agent breaks the work into subtasks and delegates to specialist agents, each with their own tools and context. Team state persists across sessions so you can pick up where you left off.
### Use the Browser
```bash
cline --team-name auth-sprint "Plan and implement user authentication with tests"
```
With Claude Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
## Scheduled Agents
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
Run agents on cron schedules for recurring automations. Daily PR summaries, weekly dependency checks, codebase health reports. Schedules persist across restarts and run independently of any terminal session.
<!-- Transparent pixel to create line break after floating image -->
```bash
cline schedule create "PR summary" \
--cron "0 9 * * MON-FRI" \
--prompt "List all open PRs and their review status" \
--workspace /path/to/repo
```
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Connect to Slack, Telegram, Discord, and More
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
Chat with your agent from any messaging platform: Telegram, Slack, Discord, Google Chat, WhatsApp, and Linear. Each conversation thread maps to an agent session with full context. Set up access control to restrict who can interact with your agent.
### "add a tool that..."
```bash
# Connect to Telegram
cline connect telegram -k $BOT_TOKEN
# Connect to Slack through webhook
cline connect slack --bot-token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
# Connect to Slack using socket mode
cline connect slack --bot-token $SLACK_TOKEN --app-token $SLACK_APP_TOKEN
```
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
## Headless CLI for CI/CD
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
Run Cline with zero interaction for scripting and automation. Pipe input, get JSON output, chain commands, integrate into CI/CD pipelines.
<!-- Transparent pixel to create line break after floating image -->
```bash
cline "Run tests and fix any failures"
git diff origin/main | cline "Review these changes for issues"
cline --json "List all TODO comments" | jq -r 'select(.type == "agent_event" and .event.text) | .event.text'
```
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### Add Context
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### Checkpoints: Compare and Restore
As Cline works through a task, the extension takes a snapshot of your workspace at each step. You can use the 'Compare' button to see a diff between the snapshot and your current workspace, and the 'Restore' button to roll back to that point.
For example, when working with a local web server, you can use 'Restore Workspace Only' to quickly test different versions of your app, then use 'Restore Task and Workspace' when you find the version you want to continue building from. This lets you safely explore different approaches without losing progress.
<!-- Transparent pixel to create line break after floating image -->
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## Contributing
Start with the [Contributing Guide](CONTRIBUTING.md). Join our [Discord](https://discord.gg/cline) and head to the `#contributors` channel to connect with other contributors. Check our [careers page](https://cline.bot/join-us) for full-time roles.
To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.md) to learn the basics. You can also join our [Discord](https://discord.gg/cline) to chat with other contributors in the `#contributors` channel. If you're looking for full-time work, check out our open positions on our [careers page](https://cline.bot/join-us)!
## Enterprise
Get the same Cline experience with enterprise-grade controls: SSO (SAML/OIDC), global policies and configuration, observability with audit trails, private networking (VPC/private link), and self-hosted or on-prem deployments, and enterprise support. Learn more at our [enterprise page](https://cline.bot/enterprise) or [talk to us](https://cline.bot/contact-sales).
## License
+5 -3
View File
@@ -8,7 +8,9 @@ We actively patch only the most recent minor release of Cline. Older versions re
We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions.
To report a security issue, please submit your report through our [Bugcrowd Vulnerability Disclosure Program](https://bugcrowd.com/engagements/clinebot-vdp-ess). Bugcrowd will manage communication and triage on our behalf.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/cline/cline/security/advisories/new) tab.
The team will send a response indicating the next steps in handling your report. After the initial reply, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
When reporting, please include:
@@ -16,10 +18,10 @@ When reporting, please include:
- Steps to reproduce or a proof of concept
- Any logs, stack traces, or screenshots that might help us understand the problem
Please keep the details private until a resolution has been reached.
We acknowledge reports within 48 hours and aim to release a fix or mitigation within 30 days. While we work on a resolution, please keep the details private.
## Escalation
If you are unable to submit through Bugcrowd, you may send an email to security@cline.bot.
If you do not receive an acknowledgement of your report within 5 business days, you may send an email to security@cline.bot.
Thank you for helping us keep Cline users safe.

Some files were not shown because too many files have changed in this diff Show More