diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index 20eea4288a..da2d4228f5 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -8,6 +8,10 @@ on: - 'apps/web-roo-code/**' workflow_dispatch: +concurrency: + group: deploy-roocode-com + cancel-in-progress: true + env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} @@ -36,8 +40,17 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js and pnpm uses: ./.github/actions/setup-node-pnpm + - name: Run lint + run: pnpm lint + working-directory: apps/web-roo-code + - name: Run type check + run: pnpm check-types + working-directory: apps/web-roo-code + - name: Run build + run: pnpm build + working-directory: apps/web-roo-code - name: Install Vercel CLI - run: npm install --global vercel@canary + run: npm install --global vercel@latest - name: Pull Vercel Environment Information run: npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts diff --git a/.github/workflows/website-preview.yml b/.github/workflows/website-preview.yml index 6966005eaf..9446bc7753 100644 --- a/.github/workflows/website-preview.yml +++ b/.github/workflows/website-preview.yml @@ -11,6 +11,10 @@ on: - "apps/web-roo-code/**" workflow_dispatch: +concurrency: + group: preview-roocode-com-${{ github.ref }} + cancel-in-progress: true + env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} @@ -39,8 +43,17 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js and pnpm uses: ./.github/actions/setup-node-pnpm + - name: Run lint + run: pnpm lint + working-directory: apps/web-roo-code + - name: Run type check + run: pnpm check-types + working-directory: apps/web-roo-code + - name: Run build + run: pnpm build + working-directory: apps/web-roo-code - name: Install Vercel CLI - run: npm install --global vercel@canary + run: npm install --global vercel@latest - name: Pull Vercel Environment Information run: npx vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts @@ -70,15 +83,20 @@ jobs: comment.body.includes(commentIdentifier) ); - if (existingComment) { - return; - } - const comment = commentIdentifier + '\n🚀 **Preview deployed!**\n\nYour changes have been deployed to Vercel:\n\n**Preview URL:** ' + deploymentUrl + '\n\nThis preview will be updated automatically when you push new commits to this PR.'; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: comment + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..ae09fd9b30 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md + +This file provides guidance to agents when working with code in this repository. + +- Settings View Pattern: When working on `SettingsView`, inputs must bind to the local `cachedState`, NOT the live `useExtensionState()`. The `cachedState` acts as a buffer for user edits, isolating them from the `ContextProxy` source-of-truth until the user explicitly clicks "Save". Wiring inputs directly to the live state causes race conditions. diff --git a/CHANGELOG.md b/CHANGELOG.md index f92674a5b0..4d932eae0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,98 @@ # Roo Code Changelog +## [3.45.0] - 2026-01-27 + +![3.45.0 Release - Smart Code Folding](/releases/3.45.0-release.png) + +- Smart Code Folding: Context condensation now intelligently preserves a lightweight map of files you worked on—function signatures, class declarations, and type definitions—so Roo can continue referencing them accurately after condensing. Files are prioritized by most recent access, with a ~50k character budget ensuring your latest work is always preserved. (Idea by @shariqriazz, PR #10942 by @hannesrudolph) + +## [3.44.2] - 2026-01-27 + +- Re-enable parallel tool calling with new_task isolation safeguards (PR #11006 by @mrubens) +- Fix worktree indexing by using relative paths in isPathInIgnoredDirectory (PR #11009 by @daniel-lxs) +- Fix local model validation error for Ollama models (PR #10893 by @roomote) +- Fix duplicate tool_call emission from Responses API providers (PR #11008 by @daniel-lxs) + +## [3.44.1] - 2026-01-27 + +- Fix LiteLLM tool ID validation errors for Bedrock proxy (PR #10990 by @daniel-lxs) +- Add temperature=0.9 and top_p=0.95 to zai-glm-4.7 model for better generation quality (PR #10945 by @sebastiand-cerebras) +- Add quality checks to marketing site deployment workflows (PR #10959 by @mp-roocode) + +## [3.44.0] - 2026-01-26 + +![3.44.0 Release - Worktrees](/releases/3.44.0-release.png) + +- Add worktree selector and creation UX (PR #10940 by @brunobergher, thanks Cline!) +- Improve subtask visibility and navigation in history and chat views (PR #10864 by @brunobergher) +- Add wildcard support for MCP alwaysAllow configuration (PR #10948 by @app/roomote) +- Fix: Prevent nested condensing from including previously-condensed content (PR #10985 by @hannesrudolph) +- Fix: VS Code LM token counting returns 0 outside requests, breaking context condensing (#10968 by @srulyt, PR #10983 by @daniel-lxs) +- Fix: Record truncation event when condensation fails but truncation succeeds (PR #10984 by @hannesrudolph) +- Replace hyphen encoding with fuzzy matching for MCP tool names (PR #10775 by @daniel-lxs) +- Remove MCP SERVERS section from system prompt for cleaner prompts (PR #10895 by @daniel-lxs) +- new_task tool creates checkpoint the same way write_to_file does (PR #10982 by @daniel-lxs) +- Update Fireworks provider with new models (#10674 by @hannesrudolph, PR #10679 by @ThanhNguyxn) +- Fix: Truncate AWS Bedrock toolUseId to 64 characters (PR #10902 by @daniel-lxs) +- Fix: Restore opaque background to settings section headers (PR #10951 by @app/roomote) +- Fix: Remove unsupported Fireworks model tool fields (PR #10937 by @app/roomote) +- Update and improve zh-TW Traditional Chinese locale and docs (PR #10953 by @PeterDaveHello) +- Chore: Remove POWER_STEERING experiment remnants (PR #10980 by @hannesrudolph) + +## [3.43.0] - 2026-01-23 + +![3.43.0 Release - Intelligent Context Condensation](/releases/3.43.0-release.png) + +- Intelligent Context Condensation v2: New context condensation system that intelligently summarizes conversation history when approaching context limits, preserving important information while reducing token usage (PR #10873 by @hannesrudolph) +- Improved context condensation with environment details, accurate token counts, and lazy evaluation for better performance (PR #10920 by @hannesrudolph) +- Move condense prompt editor to Context Management tab for better discoverability and organization (PR #10909 by @hannesrudolph) +- Update Z.AI models with new variants and pricing (#10859 by @ErdemGKSL, PR #10860 by @ErdemGKSL) +- Add pnpm install:vsix:nightly command for easier nightly build installation (PR #10912 by @hannesrudolph) +- Fix: Convert orphaned tool_results to text blocks after condensing to prevent API errors (PR #10927 by @daniel-lxs) +- Fix: Auto-migrate v1 condensing prompt and handle invalid providers on import (PR #10931 by @hannesrudolph) +- Fix: Use json-stream-stringify for pretty-printing MCP config files to prevent memory issues with large configs (#9862 by @Michaelzag, PR #9864 by @Michaelzag) +- Fix: Correct Gemini 3 pricing for Flash and Pro models (#10432 by @rossdonald, PR #10487 by @roomote) +- Fix: Skip thoughtSignature blocks during markdown export for cleaner output (#10199 by @rossdonald, PR #10932 by @rossdonald) +- Fix: Duplicate model display for OpenAI Codex provider (PR #10930 by @roomote) +- Remove diffEnabled and fuzzyMatchThreshold settings as they are no longer needed (#10648 by @hannesrudolph, PR #10298 by @hannesrudolph) +- Remove MULTI_FILE_APPLY_DIFF experiment (PR #10925 by @hannesrudolph) +- Remove POWER_STEERING experimental feature (PR #10926 by @hannesrudolph) +- Remove legacy XML tool calling code (getToolDescription) for cleaner codebase (PR #10929 by @hannesrudolph) + +## [3.42.0] - 2026-01-22 + +![3.42.0 Release - ChatGPT Usage Tracking](/releases/3.42.0-release.png) + +- Added UI to track your ChatGPT usage limits in the OpenAI Codex provider (PR #10813 by @hannesrudolph) +- Removed deprecated Claude Code provider (PR #10883 by @daniel-lxs) +- Streamlined codebase by removing legacy XML tool calling functionality (#10848 by @hannesrudolph, PR #10841 by @hannesrudolph) +- Standardize model selectors across all providers: Improved consistency of model selection UI (#10650 by @hannesrudolph, PR #10294 by @hannesrudolph) +- Enable prompt caching for Cerebras zai-glm-4.7 model (#10601 by @jahanson, PR #10670 by @app/roomote) +- Add Kimi K2 thinking model to VertexAI provider (#9268 by @diwakar-s-maurya, PR #9269 by @app/roomote) +- Warn users when too many MCP tools are enabled (PR #10772 by @app/roomote) +- Migrate context condensing prompt to customSupportPrompts (PR #10881 by @hannesrudolph) +- Unify export path logic and default to Downloads folder (PR #10882 by @hannesrudolph) +- Performance improvements for webview state synchronization (PR #10842 by @hannesrudolph) +- Fix: Handle mode selector empty state on workspace switch (#10660 by @hannesrudolph, PR #9674 by @app/roomote) +- Fix: Resolve race condition in context condensing prompt input (PR #10876 by @hannesrudolph) +- Fix: Prevent double emission of text/reasoning in OpenAI native and codex handlers (PR #10888 by @hannesrudolph) +- Fix: Prevent task abortion when resuming via IPC/bridge (PR #10892 by @cte) +- Fix: Enforce file restrictions for all editing tools (PR #10896 by @app/roomote) +- Fix: Remove custom condensing model option (PR #10901 by @hannesrudolph) +- Unify user content tags to for consistent prompt formatting (#10658 by @hannesrudolph, PR #10723 by @app/roomote) +- Clarify linked SKILL.md file handling in prompts (PR #10907 by @hannesrudolph) +- Fix: Padding on Roo Code Cloud teaser (PR #10889 by @app/roomote) + +## [3.41.3] - 2026-01-18 + +- Fix: Thinking block word-breaking to prevent horizontal scroll in the chat UI (PR #10806 by @roomote) +- Add Claude-like CLI flags and authentication fixes for the Roo Code CLI (PR #10797 by @cte) +- Improve CLI authentication by using a redirect instead of a fetch (PR #10799 by @cte) +- Fix: Roo Code Router fixes for the CLI (PR #10789 by @cte) +- Release CLI v0.0.48 with latest improvements (PR #10800 by @cte) +- Release CLI v0.0.47 (PR #10798 by @cte) +- Revert E2E tests enablement to address stability issues (PR #10794 by @cte) + ## [3.41.2] - 2026-01-16 - Add button to open markdown in VSCode preview for easier reading of formatted content (PR #10773 by @brunobergher) diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 178e9cac5a..0babc28fd8 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to the `@roo-code/cli` package will be documented in this fi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.49] - 2026-01-18 + +### Added + +- **Output Format Options**: New `--output-format` flag to control CLI output format for scripting and automation: + - `text` (default) - Human-readable interactive output + - `json` - Single JSON object with all events and final result at task completion + - `stream-json` - NDJSON (newline-delimited JSON) for real-time streaming of events + - See [`json-events.ts`](src/types/json-events.ts) for the complete event schema + - New [`JsonEventEmitter`](src/agent/json-event-emitter.ts) for structured output generation + ## [0.0.48] - 2026-01-17 ### Changed diff --git a/apps/cli/README.md b/apps/cli/README.md index d440536440..8814c68702 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -71,7 +71,7 @@ By default, the CLI prompts for approval before executing actions: ```bash export OPENROUTER_API_KEY=sk-or-v1-... -roo ~/Documents/my-project -P "What is this project?" +roo "What is this project?" -w ~/Documents/my-project ``` You can also run without a prompt and enter it interactively in TUI mode: @@ -92,7 +92,7 @@ In interactive mode: For automation and scripts, use `-y` to auto-approve all actions: ```bash -roo ~/Documents/my-project -y -P "Refactor the utils.ts file" +roo "Refactor the utils.ts file" -y -w ~/Documents/my-project ``` In non-interactive mode: @@ -149,8 +149,8 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo | Option | Description | Default | | --------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------- | -| `[workspace]` | Workspace path to operate in (positional argument) | Current directory | -| `-P, --prompt ` | The prompt/task to execute (optional in TUI mode) | None | +| `[prompt]` | Your prompt (positional argument, optional) | None | +| `-w, --workspace ` | Workspace path to operate in | Current directory | | `-e, --extension ` | Path to the extension bundle directory | Auto-detected | | `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` | | `-x, --exit-on-complete` | Exit the process when task completes (useful for testing) | `false` | @@ -249,7 +249,7 @@ pnpm lint To create a new release, execute the /cli-release slash command: ```bash -roo ~/Documents/Roo-Code -P "/cli-release" -y +roo "/cli-release" -w ~/Documents/Roo-Code -y ``` The workflow will: diff --git a/apps/cli/package.json b/apps/cli/package.json index e11afdad37..6348bbe020 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@roo-code/cli", - "version": "0.0.48", + "version": "0.0.49", "description": "Roo Code CLI - Run the Roo Code agent from the command line", "private": true, "type": "module", @@ -30,6 +30,8 @@ "@trpc/client": "^11.8.1", "@vscode/ripgrep": "^1.15.9", "commander": "^12.1.0", + "cross-spawn": "^7.0.6", + "execa": "^9.5.2", "fuzzysort": "^3.1.0", "ink": "^6.6.0", "p-wait-for": "^5.0.2", diff --git a/apps/cli/src/agent/__tests__/extension-host.test.ts b/apps/cli/src/agent/__tests__/extension-host.test.ts index c0294e7d42..2354e3ab75 100644 --- a/apps/cli/src/agent/__tests__/extension-host.test.ts +++ b/apps/cli/src/agent/__tests__/extension-host.test.ts @@ -100,16 +100,17 @@ describe("ExtensionHost", () => { ephemeral: false, debug: false, exitOnComplete: false, + integrationTest: true, // Set explicitly for testing } const host = new ExtensionHost(options) - // Options are stored but integrationTest is set to true + // Options are stored as-is const storedOptions = getPrivate(host, "options") expect(storedOptions.mode).toBe(options.mode) expect(storedOptions.workspacePath).toBe(options.workspacePath) expect(storedOptions.extensionPath).toBe(options.extensionPath) - expect(storedOptions.integrationTest).toBe(true) // Always set to true in constructor + expect(storedOptions.integrationTest).toBe(true) }) it("should be an EventEmitter instance", () => { @@ -298,16 +299,19 @@ describe("ExtensionHost", () => { }) it("should suppress console when integrationTest is false", () => { - const host = createTestHost() + // Capture the real console.log before any host is created const originalLog = console.log - // Override integrationTest to false + // Create host with integrationTest: true to prevent constructor from suppressing + const host = createTestHost({ integrationTest: true }) + + // Override integrationTest to false to test suppression const options = getPrivate(host, "options") options.integrationTest = false callPrivate(host, "setupQuietMode") - // Console should be modified + // Console should be modified (suppressed) expect(console.log).not.toBe(originalLog) // Restore for other tests @@ -332,9 +336,12 @@ describe("ExtensionHost", () => { describe("restoreConsole", () => { it("should restore original console methods when suppressed", () => { - const host = createTestHost() + // Capture the real console.log before any host is created const originalLog = console.log + // Create host with integrationTest: true to prevent constructor from suppressing + const host = createTestHost({ integrationTest: true }) + // Override integrationTest to false to actually suppress const options = getPrivate(host, "options") options.integrationTest = false diff --git a/apps/cli/src/agent/extension-host.ts b/apps/cli/src/agent/extension-host.ts index a3ceec132f..e1f55a30d1 100644 --- a/apps/cli/src/agent/extension-host.ts +++ b/apps/cli/src/agent/extension-host.ts @@ -153,7 +153,10 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac super() this.options = options - this.options.integrationTest = true + + // Set up quiet mode early, before any extension code runs. + // This suppresses console output from the extension during load. + this.setupQuietMode() // Initialize client - single source of truth for agent state (including mode). this.client = new ExtensionClient({ @@ -162,9 +165,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac }) // Initialize output manager. - this.outputManager = new OutputManager({ - disabled: options.disableOutput, - }) + this.outputManager = new OutputManager({ disabled: options.disableOutput }) // Initialize prompt manager with console mode callbacks. this.promptManager = new PromptManager({ @@ -222,8 +223,6 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac this.initialSettings.reasoningEffort = this.options.reasoningEffort } } - - this.setupQuietMode() } // ========================================================================== @@ -267,7 +266,8 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac // ========================================================================== private setupQuietMode(): void { - if (this.options.integrationTest) { + // Skip if already set up or if integrationTest mode + if (this.originalConsole || this.options.integrationTest) { return } @@ -292,18 +292,16 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac } private restoreConsole(): void { - if (this.options.integrationTest) { + if (!this.originalConsole) { return } - if (this.originalConsole) { - console.log = this.originalConsole.log - console.warn = this.originalConsole.warn - console.error = this.originalConsole.error - console.debug = this.originalConsole.debug - console.info = this.originalConsole.info - this.originalConsole = null - } + console.log = this.originalConsole.log + console.warn = this.originalConsole.warn + console.error = this.originalConsole.error + console.debug = this.originalConsole.debug + console.info = this.originalConsole.info + this.originalConsole = null if (this.originalProcessEmitWarning) { process.emitWarning = this.originalProcessEmitWarning diff --git a/apps/cli/src/agent/index.ts b/apps/cli/src/agent/index.ts index 23cbaacb4d..7298d506e9 100644 --- a/apps/cli/src/agent/index.ts +++ b/apps/cli/src/agent/index.ts @@ -1 +1,2 @@ export * from "./extension-host.js" +export * from "./json-event-emitter.js" diff --git a/apps/cli/src/agent/json-event-emitter.ts b/apps/cli/src/agent/json-event-emitter.ts new file mode 100644 index 0000000000..a1a404e555 --- /dev/null +++ b/apps/cli/src/agent/json-event-emitter.ts @@ -0,0 +1,464 @@ +/** + * JsonEventEmitter - Handles structured JSON output for the CLI + * + * This class transforms internal CLI events (ClineMessage, state changes, etc.) + * into structured JSON events and outputs them to stdout. + * + * Supports two output modes: + * - "stream-json": NDJSON format (one JSON object per line) for real-time streaming + * - "json": Single JSON object at the end with accumulated events + * + * Schema is optimized for efficiency with high message volume: + * - Minimal fields per event + * - No redundant wrappers + * - `done` flag instead of partial:false + */ + +import type { ClineMessage } from "@roo-code/types" + +import type { JsonEvent, JsonEventCost, JsonFinalOutput } from "@/types/json-events.js" + +import type { ExtensionClient } from "./extension-client.js" +import type { TaskCompletedEvent } from "./events.js" + +/** + * Options for JsonEventEmitter. + */ +export interface JsonEventEmitterOptions { + /** Output mode: "json" or "stream-json" */ + mode: "json" | "stream-json" + /** Output stream (defaults to process.stdout) */ + stdout?: NodeJS.WriteStream +} + +/** + * Parse tool information from a ClineMessage text field. + * Tool messages are JSON with a `tool` field containing the tool name. + */ +function parseToolInfo(text: string | undefined): { name: string; input: Record } | null { + if (!text) return null + try { + const parsed = JSON.parse(text) + return parsed.tool ? { name: parsed.tool, input: parsed } : null + } catch { + return null + } +} + +/** + * Parse API request cost information from api_req_started message text. + */ +function parseApiReqCost(text: string | undefined): JsonEventCost | undefined { + if (!text) return undefined + try { + const parsed = JSON.parse(text) + return parsed.cost !== undefined + ? { + totalCost: parsed.cost, + inputTokens: parsed.tokensIn, + outputTokens: parsed.tokensOut, + cacheWrites: parsed.cacheWrites, + cacheReads: parsed.cacheReads, + } + : undefined + } catch { + return undefined + } +} + +/** Internal events that should not be emitted */ +const SKIP_SAY_TYPES = new Set([ + "api_req_finished", + "api_req_retried", + "api_req_retry_delayed", + "api_req_rate_limit_wait", + "api_req_deleted", + "checkpoint_saved", + "condense_context", + "condense_context_error", + "sliding_window_truncation", +]) + +/** Key offset for reasoning content to avoid collision with text content delta tracking */ +const REASONING_KEY_OFFSET = 1_000_000_000 + +export class JsonEventEmitter { + private mode: "json" | "stream-json" + private stdout: NodeJS.WriteStream + private events: JsonEvent[] = [] + private unsubscribers: (() => void)[] = [] + private lastCost: JsonEventCost | undefined + private seenMessageIds = new Set() + // Track previous content for delta computation + private previousContent = new Map() + // Track the completion result content + private completionResultContent: string | undefined + + constructor(options: JsonEventEmitterOptions) { + this.mode = options.mode + this.stdout = options.stdout ?? process.stdout + } + + /** + * Attach to an ExtensionClient and subscribe to its events. + */ + attachToClient(client: ExtensionClient): void { + // Subscribe to message events + const unsubMessage = client.on("message", (msg) => this.handleMessage(msg, false)) + const unsubMessageUpdated = client.on("messageUpdated", (msg) => this.handleMessage(msg, true)) + const unsubTaskCompleted = client.on("taskCompleted", (event) => this.handleTaskCompleted(event)) + const unsubError = client.on("error", (error) => this.handleError(error)) + + this.unsubscribers.push(unsubMessage, unsubMessageUpdated, unsubTaskCompleted, unsubError) + + // Emit init event + this.emitEvent({ + type: "system", + subtype: "init", + content: "Task started", + }) + } + + /** + * Detach from the client and clean up subscriptions. + */ + detach(): void { + for (const unsub of this.unsubscribers) { + unsub() + } + this.unsubscribers = [] + } + + /** + * Compute the delta (new content) for a streaming message. + * Returns null if there's no new content. + */ + private computeDelta(msgId: number, fullContent: string | undefined): string | null { + if (!fullContent) return null + + const previous = this.previousContent.get(msgId) || "" + if (fullContent === previous) return null + + this.previousContent.set(msgId, fullContent) + // If content is appended, return only the new part + return fullContent.startsWith(previous) ? fullContent.slice(previous.length) : fullContent + } + + /** + * Check if this is a streaming partial message with no new content. + */ + private isEmptyStreamingDelta(content: string | null): boolean { + return this.mode === "stream-json" && content === null + } + + /** + * Get content to send for a message (delta for streaming, full for json mode). + */ + private getContentToSend(msgId: number, text: string | undefined, isPartial: boolean): string | null { + if (this.mode === "stream-json" && isPartial) { + return this.computeDelta(msgId, text) + } + return text ?? null + } + + /** + * Build a base event with optional done flag. + */ + private buildTextEvent( + type: "assistant" | "thinking" | "user", + id: number, + content: string | null, + isDone: boolean, + subtype?: string, + ): JsonEvent { + const event: JsonEvent = { type, id } + if (content !== null) { + event.content = content + } + if (subtype) { + event.subtype = subtype + } + if (isDone) { + event.done = true + } + return event + } + + /** + * Handle a ClineMessage and emit the appropriate JSON event. + */ + private handleMessage(msg: ClineMessage, _isUpdate: boolean): void { + const isDone = !msg.partial + + // In json mode, only emit complete (non-partial) messages + if (this.mode === "json" && msg.partial) { + return + } + + // Skip duplicate complete messages + if (isDone && this.seenMessageIds.has(msg.ts)) { + return + } + + if (isDone) { + this.seenMessageIds.add(msg.ts) + this.previousContent.delete(msg.ts) + } + + const contentToSend = this.getContentToSend(msg.ts, msg.text, msg.partial ?? false) + + // Skip if no new content for streaming partial messages + if (msg.partial && this.isEmptyStreamingDelta(contentToSend)) { + return + } + + if (msg.type === "say" && msg.say) { + this.handleSayMessage(msg, contentToSend, isDone) + } + + if (msg.type === "ask" && msg.ask) { + this.handleAskMessage(msg, contentToSend, isDone) + } + } + + /** + * Handle "say" type messages. + */ + private handleSayMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void { + switch (msg.say) { + case "text": + this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone)) + break + + case "reasoning": + this.handleReasoningMessage(msg, isDone) + break + + case "error": + this.emitEvent({ type: "error", id: msg.ts, content: contentToSend ?? undefined }) + break + + case "command_output": + this.emitEvent({ + type: "tool_result", + tool_result: { name: "execute_command", output: msg.text }, + }) + break + + case "user_feedback": + case "user_feedback_diff": + this.emitEvent(this.buildTextEvent("user", msg.ts, contentToSend, isDone)) + break + + case "api_req_started": { + const cost = parseApiReqCost(msg.text) + if (cost) { + this.lastCost = cost + } + break + } + + case "browser_action": + case "browser_action_result": + this.emitEvent({ + type: "tool_result", + subtype: "browser", + tool_result: { name: "browser_action", output: msg.text }, + }) + break + + case "mcp_server_response": + this.emitEvent({ + type: "tool_result", + subtype: "mcp", + tool_result: { name: "mcp_server", output: msg.text }, + }) + break + + case "completion_result": + if (msg.text && !msg.partial) { + this.completionResultContent = msg.text + } + break + + default: + if (SKIP_SAY_TYPES.has(msg.say!)) { + break + } + if (msg.text) { + this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, msg.say)) + } + break + } + } + + /** + * Handle reasoning/thinking messages with separate delta tracking. + */ + private handleReasoningMessage(msg: ClineMessage, isDone: boolean): void { + const reasoningContent = msg.reasoning || msg.text + const reasoningKey = msg.ts + REASONING_KEY_OFFSET + const reasoningDelta = this.getContentToSend(reasoningKey, reasoningContent, msg.partial ?? false) + + if (msg.partial && this.isEmptyStreamingDelta(reasoningDelta)) { + return + } + + if (!msg.partial) { + this.previousContent.delete(reasoningKey) + } + + this.emitEvent(this.buildTextEvent("thinking", msg.ts, reasoningDelta, isDone)) + } + + /** + * Handle "ask" type messages. + */ + private handleAskMessage(msg: ClineMessage, contentToSend: string | null, isDone: boolean): void { + switch (msg.ask) { + case "tool": { + const toolInfo = parseToolInfo(msg.text) + this.emitEvent({ + type: "tool_use", + id: msg.ts, + subtype: "tool", + tool_use: toolInfo ?? { name: "unknown_tool", input: { raw: msg.text } }, + }) + break + } + + case "command": + this.emitEvent({ + type: "tool_use", + id: msg.ts, + subtype: "command", + tool_use: { name: "execute_command", input: { command: msg.text } }, + }) + break + + case "browser_action_launch": + this.emitEvent({ + type: "tool_use", + id: msg.ts, + subtype: "browser", + tool_use: { name: "browser_action", input: { raw: msg.text } }, + }) + break + + case "use_mcp_server": + this.emitEvent({ + type: "tool_use", + id: msg.ts, + subtype: "mcp", + tool_use: { name: "mcp_server", input: { raw: msg.text } }, + }) + break + + case "followup": + this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, "followup")) + break + + case "command_output": + // Handled in say type + break + + case "completion_result": + if (msg.text && !msg.partial) { + this.completionResultContent = msg.text + } + break + + default: + if (msg.text) { + this.emitEvent(this.buildTextEvent("assistant", msg.ts, contentToSend, isDone, msg.ask)) + } + break + } + } + + /** + * Handle task completion and emit result event. + */ + private handleTaskCompleted(event: TaskCompletedEvent): void { + // Use tracked completion result content, falling back to event message + const resultContent = this.completionResultContent || event.message?.text + + this.emitEvent({ + type: "result", + id: event.message?.ts ?? Date.now(), + content: resultContent, + done: true, + success: event.success, + cost: this.lastCost, + }) + + // For "json" mode, output the final accumulated result + if (this.mode === "json") { + this.outputFinalResult(event.success, resultContent) + } + } + + /** + * Handle errors and emit error event. + */ + private handleError(error: Error): void { + this.emitEvent({ + type: "error", + id: Date.now(), + content: error.message, + }) + } + + /** + * Emit a JSON event. + * For stream-json mode: immediately output to stdout + * For json mode: accumulate for final output + */ + private emitEvent(event: JsonEvent): void { + this.events.push(event) + + if (this.mode === "stream-json") { + this.outputLine(event) + } + } + + /** + * Output a single JSON line (NDJSON format). + */ + private outputLine(data: unknown): void { + this.stdout.write(JSON.stringify(data) + "\n") + } + + /** + * Output the final accumulated result (for "json" mode). + */ + private outputFinalResult(success: boolean, content?: string): void { + const output: JsonFinalOutput = { + type: "result", + success, + content, + cost: this.lastCost, + events: this.events.filter((e) => e.type !== "result"), // Exclude the result event itself + } + + this.stdout.write(JSON.stringify(output, null, 2) + "\n") + } + + /** + * Get accumulated events (for testing or external use). + */ + getEvents(): JsonEvent[] { + return [...this.events] + } + + /** + * Clear accumulated events and state. + */ + clear(): void { + this.events = [] + this.lastCost = undefined + this.seenMessageIds.clear() + this.previousContent.clear() + this.completionResultContent = undefined + } +} diff --git a/apps/cli/src/commands/cli/run.ts b/apps/cli/src/commands/cli/run.ts index 86ede4c813..663ed5cf75 100644 --- a/apps/cli/src/commands/cli/run.ts +++ b/apps/cli/src/commands/cli/run.ts @@ -11,11 +11,13 @@ import { isSupportedProvider, OnboardingProviderChoice, supportedProviders, - ASCII_ROO, DEFAULT_FLAGS, REASONING_EFFORTS, SDK_BASE_URL, + OutputFormat, } from "@/types/index.js" +import { isValidOutputFormat } from "@/types/json-events.js" +import { JsonEventEmitter } from "@/agent/json-event-emitter.js" import { createClient } from "@/lib/sdk/index.js" import { loadToken, loadSettings } from "@/lib/storage/index.js" @@ -164,6 +166,23 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption process.exit(1) } + // Validate output format + const outputFormat: OutputFormat = (flagOptions.outputFormat as OutputFormat) || "text" + + if (!isValidOutputFormat(outputFormat)) { + console.error( + `[CLI] Error: Invalid output format: ${flagOptions.outputFormat}; must be one of: text, json, stream-json`, + ) + process.exit(1) + } + + // Output format only works with --print mode + if (outputFormat !== "text" && !flagOptions.print && isTuiSupported) { + console.error("[CLI] Error: --output-format requires --print mode") + console.error("[CLI] Usage: roo --print --output-format json") + process.exit(1) + } + if (!isTuiEnabled) { if (!prompt) { console.error("[CLI] Error: prompt is required in print mode") @@ -204,38 +223,53 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption process.exit(1) } } else { - console.log(ASCII_ROO) - console.log() - console.log( - `[roo] Running ${extensionHostOptions.model || "default"} (${extensionHostOptions.reasoningEffort || "default"}) on ${extensionHostOptions.provider} in ${extensionHostOptions.mode || "default"} mode in ${extensionHostOptions.workspacePath} [debug = ${extensionHostOptions.debug}]`, - ) + const useJsonOutput = outputFormat === "json" || outputFormat === "stream-json" + + extensionHostOptions.disableOutput = useJsonOutput const host = new ExtensionHost(extensionHostOptions) - process.on("SIGINT", async () => { - console.log("\n[CLI] Received SIGINT, shutting down...") - await host.dispose() - process.exit(130) - }) + const jsonEmitter = useJsonOutput + ? new JsonEventEmitter({ mode: outputFormat as "json" | "stream-json" }) + : null - process.on("SIGTERM", async () => { - console.log("\n[CLI] Received SIGTERM, shutting down...") + async function shutdown(signal: string, exitCode: number): Promise { + if (!useJsonOutput) { + console.log(`\n[CLI] Received ${signal}, shutting down...`) + } + jsonEmitter?.detach() await host.dispose() - process.exit(143) - }) + process.exit(exitCode) + } + + process.on("SIGINT", () => shutdown("SIGINT", 130)) + process.on("SIGTERM", () => shutdown("SIGTERM", 143)) try { await host.activate() + + if (jsonEmitter) { + jsonEmitter.attachToClient(host.client) + } + await host.runTask(prompt!) + jsonEmitter?.detach() await host.dispose() process.exit(0) } catch (error) { - console.error("[CLI] Error:", error instanceof Error ? error.message : String(error)) + const errorMessage = error instanceof Error ? error.message : String(error) - if (error instanceof Error) { - console.error(error.stack) + if (useJsonOutput) { + const errorEvent = { type: "error", id: Date.now(), content: errorMessage } + process.stdout.write(JSON.stringify(errorEvent) + "\n") + } else { + console.error("[CLI] Error:", errorMessage) + if (error instanceof Error) { + console.error(error.stack) + } } + jsonEmitter?.detach() await host.dispose() process.exit(1) } diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index e664422562..5b663c2bdc 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -30,6 +30,11 @@ program ) .option("--ephemeral", "Run without persisting state (uses temporary storage)", false) .option("--oneshot", "Exit upon task completion", false) + .option( + "--output-format ", + 'Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming)', + "text", + ) .action(run) const authCommand = program.command("auth").description("Manage authentication for Roo Code Cloud") diff --git a/apps/cli/src/types/index.ts b/apps/cli/src/types/index.ts index 0ed3db2350..14e5ccf6ec 100644 --- a/apps/cli/src/types/index.ts +++ b/apps/cli/src/types/index.ts @@ -1,2 +1,3 @@ export * from "./types.js" export * from "./constants.js" +export * from "./json-events.js" diff --git a/apps/cli/src/types/json-events.ts b/apps/cli/src/types/json-events.ts new file mode 100644 index 0000000000..f18f3b2768 --- /dev/null +++ b/apps/cli/src/types/json-events.ts @@ -0,0 +1,120 @@ +/** + * JSON Event Types for Structured CLI Output + * + * This module defines the types for structured JSON output from the CLI. + * The output format is NDJSON (newline-delimited JSON) for stream-json mode, + * or a single JSON object for json mode. + * + * Schema is optimized for efficiency with high message volume: + * - Minimal fields per event + * - No redundant wrappers + * - `done` flag instead of partial:false + */ + +/** + * Output format options for the CLI. + */ +export const OUTPUT_FORMATS = ["text", "json", "stream-json"] as const + +export type OutputFormat = (typeof OUTPUT_FORMATS)[number] + +export function isValidOutputFormat(format: string): format is OutputFormat { + return (OUTPUT_FORMATS as readonly string[]).includes(format) +} + +/** + * Event type discriminators for JSON output. + */ +export type JsonEventType = + | "system" // System messages (init, ready, shutdown) + | "assistant" // Assistant text messages + | "user" // User messages (echoed input) + | "tool_use" // Tool invocations (file ops, commands, browser, MCP) + | "tool_result" // Results from tool execution + | "thinking" // Reasoning/thinking content + | "error" // Errors + | "result" // Final task result + +/** + * Tool use information for tool_use events. + */ +export interface JsonEventToolUse { + /** Tool name (e.g., "read_file", "write_to_file", "execute_command") */ + name: string + /** Tool input parameters */ + input?: Record +} + +/** + * Tool result information for tool_result events. + */ +export interface JsonEventToolResult { + /** Tool name that produced this result */ + name: string + /** Tool output (for successful execution) */ + output?: string + /** Error message (for failed execution) */ + error?: string +} + +/** + * Cost and token usage information. + */ +export interface JsonEventCost { + /** Total cost in USD */ + totalCost?: number + /** Input tokens used */ + inputTokens?: number + /** Output tokens generated */ + outputTokens?: number + /** Cache write tokens */ + cacheWrites?: number + /** Cache read tokens */ + cacheReads?: number +} + +/** + * Base JSON event structure. + * Optimized for minimal payload size. + * + * For streaming deltas: + * - Each delta includes `id` for easy correlation + * - Final message has `done: true` + */ +export interface JsonEvent { + /** Event type discriminator */ + type: JsonEventType + /** Message ID - included on first delta and final message */ + id?: number + /** Content text (for text-based events) */ + content?: string + /** True when this is the final message (stream complete) */ + done?: boolean + /** Optional subtype for more specific categorization */ + subtype?: string + /** Tool use information (for tool_use events) */ + tool_use?: JsonEventToolUse + /** Tool result information (for tool_result events) */ + tool_result?: JsonEventToolResult + /** Whether the task succeeded (for result events) */ + success?: boolean + /** Cost and token usage (for result events) */ + cost?: JsonEventCost +} + +/** + * Final JSON output for "json" mode (single object at end). + * Contains the result and accumulated messages. + */ +export interface JsonFinalOutput { + /** Final result type */ + type: "result" + /** Whether the task succeeded */ + success: boolean + /** Result content/message */ + content?: string + /** Cost and token usage */ + cost?: JsonEventCost + /** All events that occurred during the task */ + events: JsonEvent[] +} diff --git a/apps/cli/src/types/types.ts b/apps/cli/src/types/types.ts index d5c71a330f..05392ccca8 100644 --- a/apps/cli/src/types/types.ts +++ b/apps/cli/src/types/types.ts @@ -1,4 +1,5 @@ import type { ProviderName, ReasoningEffortExtended } from "@roo-code/types" +import type { OutputFormat } from "./json-events.js" export const supportedProviders = [ "anthropic", @@ -32,6 +33,7 @@ export type FlagOptions = { reasoningEffort?: ReasoningEffortFlagOptions ephemeral: boolean oneshot: boolean + outputFormat?: OutputFormat } export enum OnboardingProviderChoice { diff --git a/apps/cli/src/ui/components/tools/types.ts b/apps/cli/src/ui/components/tools/types.ts index 28a1b5faa0..a16fbd60ea 100644 --- a/apps/cli/src/ui/components/tools/types.ts +++ b/apps/cli/src/ui/components/tools/types.ts @@ -16,15 +16,7 @@ export type ToolCategory = | "other" export function getToolCategory(toolName: string): ToolCategory { - const fileReadTools = [ - "readFile", - "read_file", - "fetchInstructions", - "fetch_instructions", - "listFilesTopLevel", - "listFilesRecursive", - "list_files", - ] + const fileReadTools = ["readFile", "read_file", "skill", "listFilesTopLevel", "listFilesRecursive", "list_files"] const fileWriteTools = [ "editedExistingFile", diff --git a/apps/cli/src/ui/components/tools/utils.ts b/apps/cli/src/ui/components/tools/utils.ts index 5eaee33b12..31acf2cccb 100644 --- a/apps/cli/src/ui/components/tools/utils.ts +++ b/apps/cli/src/ui/components/tools/utils.ts @@ -50,8 +50,7 @@ export function getToolDisplayName(toolName: string): string { // File read operations readFile: "Read", read_file: "Read", - fetchInstructions: "Fetch Instructions", - fetch_instructions: "Fetch Instructions", + skill: "Load Skill", listFilesTopLevel: "List Files", listFilesRecursive: "List Files (Recursive)", list_files: "List Files", @@ -107,8 +106,7 @@ export function getToolIconName(toolName: string): IconName { // File read operations readFile: "file", read_file: "file", - fetchInstructions: "file", - fetch_instructions: "file", + skill: "file", listFilesTopLevel: "folder", listFilesRecursive: "folder", list_files: "folder", diff --git a/apps/web-evals/src/app/runs/new/new-run.tsx b/apps/web-evals/src/app/runs/new/new-run.tsx index cea15c6ddd..8d44ef38e7 100644 --- a/apps/web-evals/src/app/runs/new/new-run.tsx +++ b/apps/web-evals/src/app/runs/new/new-run.tsx @@ -56,7 +56,6 @@ import { useRooCodeCloudModels } from "@/hooks/use-roo-code-cloud-models" import { Button, - Checkbox, FormControl, FormField, FormItem, @@ -111,7 +110,6 @@ export function NewRun() { const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("other") const [executionMethod, setExecutionMethod] = useState("vscode") - const [useNativeToolProtocol, setUseNativeToolProtocol] = useState(true) const [commandExecutionTimeout, setCommandExecutionTimeout] = useState(20) const [terminalShellIntegrationTimeout, setTerminalShellIntegrationTimeout] = useState(30) // seconds @@ -464,7 +462,6 @@ export function NewRun() { ...(runValues.settings || {}), apiProvider: "openrouter", openRouterModelId: selection.model, - toolProtocol: useNativeToolProtocol ? "native" : "xml", commandExecutionTimeout, terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, } @@ -474,7 +471,6 @@ export function NewRun() { ...(runValues.settings || {}), apiProvider: "roo", apiModelId: selection.model, - toolProtocol: useNativeToolProtocol ? "native" : "xml", commandExecutionTimeout, terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, } @@ -485,7 +481,6 @@ export function NewRun() { ...EVALS_SETTINGS, ...providerSettings, ...importedSettings.globalSettings, - toolProtocol: useNativeToolProtocol ? "native" : "xml", commandExecutionTimeout, terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000, } @@ -512,7 +507,6 @@ export function NewRun() { configSelections, importedSettings, router, - useNativeToolProtocol, commandExecutionTimeout, terminalShellIntegrationTimeout, ], @@ -688,26 +682,6 @@ export function NewRun() { )} -
- -
- -
-
- {settings && ( )} @@ -792,26 +766,6 @@ export function NewRun() { ))} - -
- -
- -
-
)} diff --git a/apps/web-roo-code/src/app/legal/cookies/page.tsx b/apps/web-roo-code/src/app/legal/cookies/page.tsx index c8058a34e7..895d7c2b45 100644 --- a/apps/web-roo-code/src/app/legal/cookies/page.tsx +++ b/apps/web-roo-code/src/app/legal/cookies/page.tsx @@ -100,6 +100,19 @@ export default function CookiePolicy() { 1 year ph_* + + HubSpot + + Marketing automation and visitor tracking + + + Analytics (only with your consent) + + 13 months + + hubspotutk, __hstc, __hssrc, __hssc + + @@ -122,6 +135,15 @@ export default function CookiePolicy() { PostHog Privacy Policy

+

+ + HubSpot Privacy Policy + +

Essential cookies

@@ -133,10 +155,10 @@ export default function CookiePolicy() {

Analytics cookies

- We use PostHog analytics cookies to understand how visitors interact with our website. This - helps us improve our services and user experience. Analytics cookies are placed only if you give - consent through our cookie banner. The lawful basis for processing these cookies is your - consent, which you can withdraw at any time. + We use PostHog and HubSpot analytics cookies to understand how visitors interact with our + website. This helps us improve our services, user experience, and marketing efforts. Analytics + cookies are placed only if you give consent through our cookie banner. The lawful basis for + processing these cookies is your consent, which you can withdraw at any time.

Third-party services

diff --git a/apps/web-roo-code/src/app/pricing/page.tsx b/apps/web-roo-code/src/app/pricing/page.tsx index 487c14d087..6851b47b6a 100644 --- a/apps/web-roo-code/src/app/pricing/page.tsx +++ b/apps/web-roo-code/src/app/pricing/page.tsx @@ -291,11 +291,7 @@ export default function PricingPage() {
  • To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)
  • To pay for AI model inference costs ( - + varies by model ) diff --git a/apps/web-roo-code/src/app/slack/page.tsx b/apps/web-roo-code/src/app/slack/page.tsx new file mode 100644 index 0000000000..c1fb39cb3e --- /dev/null +++ b/apps/web-roo-code/src/app/slack/page.tsx @@ -0,0 +1,401 @@ +import { + ArrowRight, + Brain, + CreditCard, + GitBranch, + GraduationCap, + Link2, + MessageSquare, + Settings, + Shield, + Slack, + Users, + Zap, +} from "lucide-react" +import type { LucideIcon } from "lucide-react" +import type { Metadata } from "next" + +import { AnimatedBackground } from "@/components/homepage" +import { SlackThreadDemo } from "@/components/slack/slack-thread-demo" +import { Button } from "@/components/ui" +import { EXTERNAL_LINKS } from "@/lib/constants" +import { SEO } from "@/lib/seo" +import { ogImageUrl } from "@/lib/og" + +const TITLE = "Roo Code for Slack" +const DESCRIPTION = + "Mention @Roomote in any channel to explain code, plan features, or ship a PR, all without leaving the conversation." +const OG_DESCRIPTION = "Your AI Team in Slack" +const PATH = "/slack" + +export const metadata: Metadata = { + title: TITLE, + description: DESCRIPTION, + alternates: { + canonical: `${SEO.url}${PATH}`, + }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [ + { + url: ogImageUrl(TITLE, OG_DESCRIPTION), + width: 1200, + height: 630, + alt: TITLE, + }, + ], + locale: SEO.locale, + type: "website", + }, + twitter: { + card: SEO.twitterCard, + title: TITLE, + description: DESCRIPTION, + images: [ogImageUrl(TITLE, OG_DESCRIPTION)], + }, + keywords: [ + ...SEO.keywords, + "slack integration", + "slack bot", + "AI in slack", + "code assistant slack", + "@Roomote", + "team collaboration", + ], +} + +// Invalidate cache when a request comes in, at most once every hour. +export const revalidate = 3600 + +type ValueProp = { + icon: LucideIcon + title: string + description: string +} + +const VALUE_PROPS: ValueProp[] = [ + { + icon: GitBranch, + title: "Discussion to PR.", + description: + "Your team discusses a feature in Slack. @Roomote turns the discussion into a plan. Then builds it. All without leaving the conversation.", + }, + { + icon: Brain, + title: "Thread-aware.", + description: + '@Roomote reads the full thread before responding. Ask "Can we add caching here?" and it knows exactly what code you mean.', + }, + { + icon: Link2, + title: "Chain agents.", + description: + "Start with a Planner to spec it out. Then call the Coder to build it. Multi-step workflows, one Slack thread.", + }, + { + icon: Users, + title: "Open to all.", + description: + "Anyone on your team can ask @Roomote to fix bugs, build features, or investigate issues. Engineering gets looped in only when needed.", + }, + { + icon: GraduationCap, + title: "Built-in learning.", + description: "Public channel mentions show everyone how to leverage agents. Learn by watching.", + }, + { + icon: Shield, + title: "Safe by design.", + description: "Agents never touch main/master directly. They produce branches and PRs. You approve.", + }, +] + +type WorkflowStep = { + step: number + title: string + description: string +} + +const WORKFLOW_STEPS: WorkflowStep[] = [ + { + step: 1, + title: "Turn the discussion into a plan", + description: "Your team discusses a feature. When it gets complex, summon the Planner agent.", + }, + { + step: 2, + title: "Refine the plan in the thread", + description: + "The team reviews the spec in the thread, suggests changes, asks questions. Mention @Roomote again to refine.", + }, + { + step: 3, + title: "Build the plan", + description: "Once the plan looks good, hand it off to the Coder agent to implement.", + }, + { + step: 4, + title: "Review and ship", + description: "The Coder creates a branch and opens a PR. The team reviews, and the feature ships.", + }, +] + +type OnboardingStep = { + icon: LucideIcon + title: string + description: string + link?: { + href: string + text: string + } +} + +const ONBOARDING_STEPS: OnboardingStep[] = [ + { + icon: CreditCard, + title: "1. Team Plan", + description: "Slack requires a Team plan.", + link: { + href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL, + text: "Start a free trial", + }, + }, + { + icon: Settings, + title: "2. Connect", + description: 'Sign in to Roo Code Cloud and go to Settings. Click "Connect" next to Slack.', + }, + { + icon: Slack, + title: "3. Authorize", + description: "Authorize the Roo Code app to access your Slack workspace.", + }, + { + icon: MessageSquare, + title: "4. Add to channels", + description: "Add @Roomote to the channels where you want it available.", + }, +] + +export default function SlackPage(): JSX.Element { + return ( + <> + {/* Hero Section */} +
    + +
    +
    +
    +
    + + Powered by Roo Code Cloud +
    +

    + @Roomote: Your AI Team in Slack +

    +

    + Mention @Roomote in any channel to explain code, plan features, or ship a PR, all + without leaving the conversation. +

    + +
    + +
    + +
    +
    +
    +
    + + {/* Value Props Section */} +
    +
    +
    +
    +
    +
    +

    + Why your team will love using Roo Code in Slack +

    +

    + AI agents that understand context, chain together for complex work, and keep your team in + control. +

    +
    +
    + {VALUE_PROPS.map((prop, index) => { + const Icon = prop.icon + return ( +
    +
    + +
    +

    {prop.title}

    +

    {prop.description}

    +
    + ) + })} +
    +
    +
    + + {/* Featured Workflow Section */} +
    +
    +
    +
    +
    + +
    +
    + + Featured Workflow +
    +

    + Thread to Shipped Feature +

    +

    + Turn Slack discussions into working code. No context lost, no meetings needed. +

    +
    + +
    +
    + {/* YouTube Video Embed */} +
    +