From a18f5dc3dde996a0e567164e22f50198860153b3 Mon Sep 17 00:00:00 2001
From: Arjun <6592213+arkml@users.noreply.github.com>
Date: Wed, 6 May 2026 19:03:27 +0530
Subject: [PATCH 1/2] coding with acpx
---
apps/x/apps/renderer/src/App.tsx | 57 +++-
.../src/components/terminal-output.tsx | 24 ++
.../renderer/src/lib/chat-conversation.ts | 1 +
.../apps/renderer/src/lib/terminal-output.ts | 319 ++++++++++++++++++
apps/x/packages/core/src/agents/runtime.ts | 26 +-
.../src/application/assistant/instructions.ts | 2 +
.../skills/code-with-agents/skill.ts | 90 +++++
.../src/application/assistant/skills/index.ts | 7 +
.../core/src/application/lib/builtin-tools.ts | 10 +
.../src/application/lib/command-executor.ts | 9 +-
.../core/src/application/lib/exec-tool.ts | 5 +-
apps/x/packages/shared/src/runs.ts | 8 +
12 files changed, 545 insertions(+), 13 deletions(-)
create mode 100644 apps/x/apps/renderer/src/components/terminal-output.tsx
create mode 100644 apps/x/apps/renderer/src/lib/terminal-output.ts
create mode 100644 apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts
diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index c2e35cb2..b6e6add7 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { useCallback, useEffect, useState, useRef } from 'react'
+import { useCallback, useEffect, useLayoutEffect, useState, useRef } from 'react'
import { workspace } from '@x/shared';
import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js';
import type { LanguageModelUsage, ToolUIPart } from 'ai';
@@ -41,6 +41,7 @@ import { WebSearchResult } from '@/components/ai-elements/web-search-result';
import { AppActionCard } from '@/components/ai-elements/app-action-card';
import { ComposioConnectCard } from '@/components/ai-elements/composio-connect-card';
import { PermissionRequest } from '@/components/ai-elements/permission-request';
+import { TerminalOutput } from '@/components/terminal-output';
import { AskHumanRequest } from '@/components/ai-elements/ask-human-request';
import { Suggestions } from '@/components/ai-elements/suggestions';
import { ToolPermissionRequestEvent, AskHumanRequestEvent } from '@x/shared/src/runs.js';
@@ -121,6 +122,31 @@ function SmoothStreamingMessage({ text, components }: { text: string; components
return {smoothText}
}
+function AutoScrollPre({ className, children }: { className?: string; children: React.ReactNode }) {
+ const ref = useRef(null)
+ const stickToBottom = useRef(true)
+
+ useLayoutEffect(() => {
+ const el = ref.current
+ if (el && stickToBottom.current) {
+ el.scrollTop = el.scrollHeight
+ }
+ }, [children])
+
+ const handleScroll = useCallback(() => {
+ const el = ref.current
+ if (!el) return
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 24
+ stickToBottom.current = atBottom
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+
const DEFAULT_SIDEBAR_WIDTH = 256
const wikiLinkRegex = /\[\[([^[\]]+)\]\]/g
const graphPalette = [
@@ -2085,6 +2111,10 @@ function App() {
return next
})
+ if (event.toolCallId) {
+ setToolOpenForTab(activeChatTabIdRef.current, event.toolCallId, false)
+ }
+
// Handle app-navigation tool results — trigger UI side effects
if (event.toolName === 'app-navigation') {
const result = event.result as { success?: boolean; action?: string; [key: string]: unknown } | undefined
@@ -2096,6 +2126,23 @@ function App() {
break
}
+ case 'tool-output-stream': {
+ if (!isActiveRun) return
+ setConversation(prev => prev.map(item => {
+ if (
+ isToolCall(item)
+ && item.id === event.toolCallId
+ ) {
+ if (!item.streamingOutput) {
+ setToolOpenForTab(activeChatTabIdRef.current, item.id, true)
+ }
+ return { ...item, streamingOutput: (item.streamingOutput ?? '') + event.output }
+ }
+ return item
+ }))
+ break
+ }
+
case 'tool-permission-request': {
if (!isActiveRun) return
const key = event.toolCall.toolCallId
@@ -4314,7 +4361,13 @@ function App() {
state={toToolState(item.status)}
/>
-
+ {item.streamingOutput && item.status === 'running' ? (
+
+
+
+ ) : (
+
+ )}
)
diff --git a/apps/x/apps/renderer/src/components/terminal-output.tsx b/apps/x/apps/renderer/src/components/terminal-output.tsx
new file mode 100644
index 00000000..587616c8
--- /dev/null
+++ b/apps/x/apps/renderer/src/components/terminal-output.tsx
@@ -0,0 +1,24 @@
+import React, { useMemo } from 'react'
+import { processTerminalOutput, spanStyleToCSS } from '../lib/terminal-output'
+
+export function TerminalOutput({ raw }: { raw: string }) {
+ const lines = useMemo(() => processTerminalOutput(raw), [raw])
+
+ return (
+ <>
+ {lines.map((line, lineIdx) => (
+
+ {lineIdx > 0 && '\n'}
+ {line.spans.map((span, spanIdx) => {
+ const css = spanStyleToCSS(span.style)
+ return css ? (
+ {span.text}
+ ) : (
+ {span.text}
+ )
+ })}
+
+ ))}
+ >
+ )
+}
diff --git a/apps/x/apps/renderer/src/lib/chat-conversation.ts b/apps/x/apps/renderer/src/lib/chat-conversation.ts
index 150edacb..6ae88d93 100644
--- a/apps/x/apps/renderer/src/lib/chat-conversation.ts
+++ b/apps/x/apps/renderer/src/lib/chat-conversation.ts
@@ -24,6 +24,7 @@ export interface ToolCall {
name: string
input: ToolUIPart['input']
result?: ToolUIPart['output']
+ streamingOutput?: string
status: 'pending' | 'running' | 'completed' | 'error'
timestamp: number
}
diff --git a/apps/x/apps/renderer/src/lib/terminal-output.ts b/apps/x/apps/renderer/src/lib/terminal-output.ts
new file mode 100644
index 00000000..fadb0eb7
--- /dev/null
+++ b/apps/x/apps/renderer/src/lib/terminal-output.ts
@@ -0,0 +1,319 @@
+/**
+ * Terminal output processor that handles ANSI escape sequences, carriage returns,
+ * and other terminal control characters to produce styled, terminal-like output.
+ */
+
+export interface StyledSpan {
+ text: string
+ style: SpanStyle
+}
+
+export interface SpanStyle {
+ bold?: boolean
+ dim?: boolean
+ italic?: boolean
+ underline?: boolean
+ strikethrough?: boolean
+ fg?: string
+ bg?: string
+}
+
+export interface TerminalLine {
+ spans: StyledSpan[]
+}
+
+const ANSI_COLORS_16: Record = {
+ 30: '#4e4e4e', 31: '#e06c75', 32: '#98c379', 33: '#e5c07b',
+ 34: '#61afef', 35: '#c678dd', 36: '#56b6c2', 37: '#dcdfe4',
+ 90: '#5c6370', 91: '#e06c75', 92: '#98c379', 93: '#e5c07b',
+ 94: '#61afef', 95: '#c678dd', 96: '#56b6c2', 97: '#ffffff',
+}
+
+const ANSI_BG_COLORS_16: Record = {
+ 40: '#4e4e4e', 41: '#e06c75', 42: '#98c379', 43: '#e5c07b',
+ 44: '#61afef', 45: '#c678dd', 46: '#56b6c2', 47: '#dcdfe4',
+ 100: '#5c6370', 101: '#e06c75', 102: '#98c379', 103: '#e5c07b',
+ 104: '#61afef', 105: '#c678dd', 106: '#56b6c2', 107: '#ffffff',
+}
+
+function color256(n: number): string {
+ if (n < 8) return ANSI_COLORS_16[30 + n] ?? '#dcdfe4'
+ if (n < 16) return ANSI_COLORS_16[90 + (n - 8)] ?? '#dcdfe4'
+ if (n < 232) {
+ const idx = n - 16
+ const r = Math.floor(idx / 36)
+ const g = Math.floor((idx % 36) / 6)
+ const b = idx % 6
+ const toHex = (v: number) => (v === 0 ? 0 : 55 + v * 40).toString(16).padStart(2, '0')
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
+ }
+ const level = 8 + (n - 232) * 10
+ const hex = level.toString(16).padStart(2, '0')
+ return `#${hex}${hex}${hex}`
+}
+
+function parseSGR(params: number[], style: SpanStyle): SpanStyle {
+ const s = { ...style }
+ let i = 0
+ while (i < params.length) {
+ const p = params[i]
+ if (p === 0) {
+ delete s.bold
+ delete s.dim
+ delete s.italic
+ delete s.underline
+ delete s.strikethrough
+ delete s.fg
+ delete s.bg
+ } else if (p === 1) s.bold = true
+ else if (p === 2) s.dim = true
+ else if (p === 3) s.italic = true
+ else if (p === 4) s.underline = true
+ else if (p === 9) s.strikethrough = true
+ else if (p === 22) {
+ delete s.bold
+ delete s.dim
+ } else if (p === 23) delete s.italic
+ else if (p === 24) delete s.underline
+ else if (p === 29) delete s.strikethrough
+ else if (p >= 30 && p <= 37) s.fg = ANSI_COLORS_16[p]
+ else if (p === 38) {
+ if (params[i + 1] === 5 && params[i + 2] !== undefined) {
+ s.fg = color256(params[i + 2])
+ i += 2
+ } else if (params[i + 1] === 2 && params[i + 4] !== undefined) {
+ const r = params[i + 2]
+ const g = params[i + 3]
+ const b = params[i + 4]
+ s.fg = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
+ i += 4
+ }
+ } else if (p === 39) delete s.fg
+ else if (p >= 40 && p <= 47) s.bg = ANSI_BG_COLORS_16[p]
+ else if (p === 48) {
+ if (params[i + 1] === 5 && params[i + 2] !== undefined) {
+ s.bg = color256(params[i + 2])
+ i += 2
+ } else if (params[i + 1] === 2 && params[i + 4] !== undefined) {
+ const r = params[i + 2]
+ const g = params[i + 3]
+ const b = params[i + 4]
+ s.bg = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
+ i += 4
+ }
+ } else if (p === 49) delete s.bg
+ else if (p >= 90 && p <= 97) s.fg = ANSI_COLORS_16[p]
+ else if (p >= 100 && p <= 107) s.bg = ANSI_BG_COLORS_16[p]
+ i++
+ }
+ return s
+}
+
+export function processTerminalOutput(raw: string): TerminalLine[] {
+ type Cell = { char: string; style: SpanStyle }
+ const lines: Cell[][] = [[]]
+ let cursorRow = 0
+ let cursorCol = 0
+ let currentStyle: SpanStyle = {}
+
+ function ensureRow(row: number) {
+ while (lines.length <= row) lines.push([])
+ }
+
+ function ensureCol(row: number, col: number) {
+ ensureRow(row)
+ const line = lines[row]
+ while (line.length <= col) line.push({ char: ' ', style: {} })
+ }
+
+ let i = 0
+ while (i < raw.length) {
+ const ch = raw[i]
+
+ if (ch === '\x1b' && i + 1 < raw.length) {
+ const next = raw[i + 1]
+
+ if (next === '[') {
+ i += 2
+ let paramStr = ''
+ while (i < raw.length && raw[i] >= '\x20' && raw[i] <= '\x3f') {
+ paramStr += raw[i]
+ i++
+ }
+ const finalByte = i < raw.length ? raw[i] : ''
+ i++
+
+ const params = paramStr.length > 0
+ ? paramStr.split(';').map(s => parseInt(s, 10) || 0)
+ : [0]
+
+ switch (finalByte) {
+ case 'm':
+ currentStyle = parseSGR(params, currentStyle)
+ break
+ case 'A':
+ cursorRow = Math.max(0, cursorRow - (params[0] || 1))
+ break
+ case 'B':
+ cursorRow += (params[0] || 1)
+ ensureRow(cursorRow)
+ break
+ case 'C':
+ cursorCol += (params[0] || 1)
+ break
+ case 'D':
+ cursorCol = Math.max(0, cursorCol - (params[0] || 1))
+ break
+ case 'G':
+ cursorCol = Math.max(0, (params[0] || 1) - 1)
+ break
+ case 'H':
+ case 'f':
+ cursorRow = Math.max(0, (params[0] || 1) - 1)
+ cursorCol = Math.max(0, (params[1] || 1) - 1)
+ ensureRow(cursorRow)
+ break
+ case 'J': {
+ const mode = params[0] || 0
+ if (mode === 2 || mode === 3) {
+ lines.length = 0
+ lines.push([])
+ cursorRow = 0
+ cursorCol = 0
+ } else if (mode === 0) {
+ ensureRow(cursorRow)
+ lines[cursorRow].length = cursorCol
+ for (let r = cursorRow + 1; r < lines.length; r++) lines[r] = []
+ } else if (mode === 1) {
+ for (let r = 0; r < cursorRow; r++) lines[r] = []
+ ensureCol(cursorRow, cursorCol)
+ for (let c = 0; c <= cursorCol; c++) lines[cursorRow][c] = { char: ' ', style: {} }
+ }
+ break
+ }
+ case 'K': {
+ const mode = params[0] || 0
+ ensureRow(cursorRow)
+ const line = lines[cursorRow]
+ if (mode === 0) {
+ line.length = cursorCol
+ } else if (mode === 1) {
+ ensureCol(cursorRow, cursorCol)
+ for (let c = 0; c <= cursorCol; c++) line[c] = { char: ' ', style: {} }
+ } else if (mode === 2) {
+ lines[cursorRow] = []
+ }
+ break
+ }
+ default:
+ break
+ }
+ continue
+ }
+
+ if (next === ']') {
+ i += 2
+ while (i < raw.length && raw[i] !== '\x07' && !(raw[i] === '\x1b' && raw[i + 1] === '\\')) {
+ i++
+ }
+ if (i < raw.length && raw[i] === '\x07') i++
+ else if (i < raw.length) i += 2
+ continue
+ }
+
+ i += 2
+ continue
+ }
+
+ if (ch === '\r') {
+ cursorCol = 0
+ i++
+ continue
+ }
+
+ if (ch === '\n') {
+ cursorRow++
+ cursorCol = 0
+ ensureRow(cursorRow)
+ i++
+ continue
+ }
+
+ if (ch === '\b') {
+ cursorCol = Math.max(0, cursorCol - 1)
+ i++
+ continue
+ }
+
+ if (ch === '\t') {
+ const nextTabStop = (Math.floor(cursorCol / 8) + 1) * 8
+ while (cursorCol < nextTabStop) {
+ ensureCol(cursorRow, cursorCol)
+ lines[cursorRow][cursorCol] = { char: ' ', style: { ...currentStyle } }
+ cursorCol++
+ }
+ i++
+ continue
+ }
+
+ if (ch.charCodeAt(0) < 32) {
+ i++
+ continue
+ }
+
+ ensureCol(cursorRow, cursorCol)
+ lines[cursorRow][cursorCol] = { char: ch, style: { ...currentStyle } }
+ cursorCol++
+ i++
+ }
+
+ return lines.map(cells => {
+ const spans: StyledSpan[] = []
+ if (cells.length === 0) return { spans: [{ text: '', style: {} }] }
+
+ let end = cells.length
+ while (end > 0 && cells[end - 1].char === ' ' && Object.keys(cells[end - 1].style).length === 0) {
+ end--
+ }
+
+ let currentSpan: StyledSpan | null = null
+ for (let c = 0; c < end; c++) {
+ const cell = cells[c]
+ const sameStyle = currentSpan && styleEquals(currentSpan.style, cell.style)
+ if (sameStyle && currentSpan) {
+ currentSpan.text += cell.char
+ } else {
+ if (currentSpan) spans.push(currentSpan)
+ currentSpan = { text: cell.char, style: { ...cell.style } }
+ }
+ }
+ if (currentSpan) spans.push(currentSpan)
+ if (spans.length === 0) spans.push({ text: '', style: {} })
+ return { spans }
+ })
+}
+
+function styleEquals(a: SpanStyle, b: SpanStyle): boolean {
+ return a.bold === b.bold
+ && a.dim === b.dim
+ && a.italic === b.italic
+ && a.underline === b.underline
+ && a.strikethrough === b.strikethrough
+ && a.fg === b.fg
+ && a.bg === b.bg
+}
+
+export function spanStyleToCSS(style: SpanStyle): React.CSSProperties | undefined {
+ if (Object.keys(style).length === 0) return undefined
+ const css: React.CSSProperties = {}
+ if (style.fg) css.color = style.fg
+ if (style.bg) css.backgroundColor = style.bg
+ if (style.bold) css.fontWeight = 'bold'
+ if (style.dim) css.opacity = 0.6
+ if (style.italic) css.fontStyle = 'italic'
+ if (style.underline) css.textDecoration = 'underline'
+ if (style.strikethrough) {
+ css.textDecoration = css.textDecoration ? `${css.textDecoration} line-through` : 'line-through'
+ }
+ return Object.keys(css).length > 0 ? css : undefined
+}
diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts
index a635b4e9..07703e30 100644
--- a/apps/x/packages/core/src/agents/runtime.ts
+++ b/apps/x/packages/core/src/agents/runtime.ts
@@ -163,6 +163,7 @@ export class AgentRuntime implements IAgentRuntime {
modelConfigRepo: this.modelConfigRepo,
signal,
abortRegistry: this.abortRegistry,
+ bus: this.bus,
})) {
eventCount++;
if (event.type !== "llm-stream-event") {
@@ -855,6 +856,7 @@ export async function* streamAgent({
modelConfigRepo,
signal,
abortRegistry,
+ bus,
}: {
state: AgentState,
idGenerator: IMonotonicallyIncreasingIdGenerator;
@@ -863,6 +865,7 @@ export async function* streamAgent({
modelConfigRepo: IModelConfigRepo;
signal: AbortSignal;
abortRegistry: IAbortRegistry;
+ bus: IBus;
}): AsyncGenerator, void, unknown> {
const logger = new PrefixLogger(`run-${runId}-${state.agentName}`);
@@ -972,11 +975,12 @@ export async function* streamAgent({
state: subflowState,
idGenerator,
runId,
- messageQueue,
- modelConfigRepo,
- signal,
- abortRegistry,
- })) {
+ messageQueue,
+ modelConfigRepo,
+ signal,
+ abortRegistry,
+ bus,
+ })) {
yield* processEvent({
...event,
subflow: [toolCallId, ...event.subflow],
@@ -985,9 +989,15 @@ export async function* streamAgent({
if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) {
result = subflowState.finalResponse();
}
- } else {
- result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry });
- }
+ } else {
+ result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, {
+ runId,
+ toolCallId,
+ signal,
+ abortRegistry,
+ publish: (event) => bus.publish(event),
+ });
+ }
} catch (error) {
if ((error instanceof Error && error.name === "AbortError") || signal.aborted) {
throw error;
diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts
index a455d845..86fe3f9e 100644
--- a/apps/x/packages/core/src/application/assistant/instructions.ts
+++ b/apps/x/packages/core/src/application/assistant/instructions.ts
@@ -80,6 +80,8 @@ ${thirdPartyBlock}**Meeting Prep:** When users ask you to prepare for a meeting,
**Document Collaboration:** When users ask you to work on a document, collaborate on writing, create a new document, edit/refine existing notes, or say things like "let's work on [X]", "help me write [X]", "create a doc for [X]", or "let's draft [X]", you MUST load the \`doc-collab\` skill first. This is required for any document creation or editing task. The skill provides structured guidance for creating, editing, and refining documents in the knowledge base.
+**Code with Agents:** When users ask you to write code, build a project, create a script, fix a bug, or do any software development task, load the \`code-with-agents\` skill first. It provides guidance for delegating coding work to Claude Code or Codex via acpx.
+
**App Control:** When users ask you to open notes, show the bases or graph view, filter or search notes, or manage saved views, load the \`app-navigation\` skill first. It provides structured guidance for navigating the app UI and controlling the knowledge base view.
**Tracks (Auto-Updating Note Blocks):** When users ask you to **track**, **monitor**, **watch**, or **keep an eye on** something in a note — or say things like "every morning tell me X", "show the current Y in this note", "pin live updates of Z here" — load the \`tracks\` skill first. Also load it when a user presses Cmd+K with a note open and requests auto-refreshing content at the cursor. Track blocks are YAML-fenced scheduled blocks whose output is rewritten on each run — useful for weather, news, prices, status pages, and personal dashboards.
diff --git a/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts b/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts
new file mode 100644
index 00000000..c2879228
--- /dev/null
+++ b/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts
@@ -0,0 +1,90 @@
+export const skill = String.raw`
+# Code with Agents Skill
+
+Use this skill when the user asks you to write code, build a project, create scripts, fix bugs, or do any software development task that should be delegated to a coding agent (Claude Code or Codex).
+
+## Important: delegate ALL coding work
+
+Once the user has chosen to use Claude Code or Codex, you MUST delegate ALL code-related tasks to the coding agent. This includes:
+- Writing, editing, or refactoring code
+- Reading, summarizing, or explaining code
+- Debugging and fixing bugs
+- Running tests or build commands
+- Exploring project structure
+- Any other task that involves interacting with a codebase
+
+Do NOT attempt to do any of these yourself — no reading files, no running commands, no writing code. You are the coordinator; the coding agent does the work. Your job is to translate the user's request into a clear prompt and pass it to the agent.
+
+## Prerequisites
+
+The user must have one of the following installed on their machine:
+- **Claude Code** — https://claude.ai/code
+- **Codex** — https://codex.openai.com
+
+These are external tools that you cannot install for the user.
+
+## Workflow
+
+### Step 1: Gather requirements
+
+Before running anything, confirm the following with the user:
+
+1. **Working directory** — Ask which folder the code should be written in, unless the user has already specified it. Example: "Which folder should I work in?"
+2. **Agent choice** — Ask whether to use **Claude Code** or **Codex**. Mention that the chosen agent must already be installed on their machine.
+
+### Step 2: Confirm execution plan
+
+Once you know the folder and agent, tell the user:
+
+> I'll use [Claude Code / Codex] to [description of the task] in \`[folder]\`. Permission requests from the coding agent itself (file writes, command execution, etc.) will be automatically approved once it starts. Wait for the user's confirmation before you execute anything.
+
+### Step 3: Execute with acpx
+
+Use the \`executeCommand\` tool to run the coding agent via acpx. The command format is:
+
+**For Claude Code:**
+` + "`" + `
+npx acpx@latest --approve-all --cwd claude exec ""
+` + "`" + `
+
+**For Codex:**
+` + "`" + `
+npx acpx@latest --approve-all --cwd codex exec ""
+` + "`" + `
+
+### Critical: flag order
+
+The \`--approve-all\` and \`--cwd\` flags are global flags and MUST come before the agent name (\`claude\` or \`codex\`). This is the correct order:
+
+` + "`" + `
+npx acpx@latest [global flags] exec ""
+` + "`" + `
+
+**Correct:**
+` + "`" + `
+npx acpx@latest --approve-all --cwd ~/projects/myapp claude exec "fix the bug"
+` + "`" + `
+
+**Wrong (will fail):**
+` + "`" + `
+npx acpx@latest claude --approve-all exec "fix the bug"
+` + "`" + `
+
+### Writing good prompts
+
+When constructing the prompt for the coding agent:
+- Be specific and detailed about what to build or fix
+- Include file names, function signatures, and expected behavior
+- Mention any constraints (language, framework, style)
+- If the user gave you a short request, expand it into a clear, actionable prompt for the agent
+
+### Step 4: Report results
+
+After the command finishes, look for the summary that the coding agent produced at the end of its output and pass that along to the user as-is. Do not rewrite or add to it. Only add your own explanation if the command failed or the exit code is non-zero.
+
+Do NOT use file reference blocks (e.g. \`\`\`file:path/to/file\`\`\`) when mentioning code files — they may not open correctly. Just refer to file paths as plain text.
+
+- If the exit code is 5, it means permissions were denied — this should not happen with \`--approve-all\`, but if it does, let the user know
+`;
+
+export default skill;
diff --git a/apps/x/packages/core/src/application/assistant/skills/index.ts b/apps/x/packages/core/src/application/assistant/skills/index.ts
index f4ba9b1d..fb7ec4e9 100644
--- a/apps/x/packages/core/src/application/assistant/skills/index.ts
+++ b/apps/x/packages/core/src/application/assistant/skills/index.ts
@@ -11,6 +11,7 @@ import createPresentationsSkill from "./create-presentations/skill.js";
import appNavigationSkill from "./app-navigation/skill.js";
import browserControlSkill from "./browser-control/skill.js";
+import codeWithAgentsSkill from "./code-with-agents/skill.js";
import composioIntegrationSkill from "./composio-integration/skill.js";
import tracksSkill from "./tracks/skill.js";
import notifyUserSkill from "./notify-user/skill.js";
@@ -94,6 +95,12 @@ const definitions: SkillDefinition[] = [
summary: "Navigate the app UI - open notes, switch views, filter/search the knowledge base, and manage saved views.",
content: appNavigationSkill,
},
+ {
+ id: "code-with-agents",
+ title: "Code with Agents",
+ summary: "Write code, build projects, create scripts, or fix bugs by delegating to Claude Code or Codex via acpx.",
+ content: codeWithAgentsSkill,
+ },
{
id: "tracks",
title: "Tracks",
diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts
index 7dd06dd2..c57d4dfc 100644
--- a/apps/x/packages/core/src/application/lib/builtin-tools.ts
+++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts
@@ -969,6 +969,16 @@ export const BuiltinTools: z.infer = {
const { promise, process: proc } = executeCommandAbortable(command, {
cwd: workingDir,
signal: ctx.signal,
+ onData: (chunk: string) => {
+ ctx.publish({
+ runId: ctx.runId,
+ type: "tool-output-stream",
+ toolCallId: ctx.toolCallId,
+ toolName: "executeCommand",
+ output: chunk,
+ subflow: [],
+ });
+ },
});
// Register process with abort registry for force-kill
diff --git a/apps/x/packages/core/src/application/lib/command-executor.ts b/apps/x/packages/core/src/application/lib/command-executor.ts
index 11b15d90..005bb7e8 100644
--- a/apps/x/packages/core/src/application/lib/command-executor.ts
+++ b/apps/x/packages/core/src/application/lib/command-executor.ts
@@ -143,6 +143,7 @@ export function executeCommandAbortable(
timeout?: number;
maxBuffer?: number;
signal?: AbortSignal;
+ onData?: (chunk: string) => void;
}
): { promise: Promise; process: ChildProcess } {
// Check if already aborted before spawning
@@ -177,16 +178,20 @@ export function executeCommandAbortable(
// Collect output
proc.stdout?.on('data', (chunk: Buffer) => {
+ const text = chunk.toString();
const maxBuffer = options?.maxBuffer || 1024 * 1024;
if (stdout.length < maxBuffer) {
- stdout += chunk.toString();
+ stdout += text;
}
+ options?.onData?.(text);
});
proc.stderr?.on('data', (chunk: Buffer) => {
+ const text = chunk.toString();
const maxBuffer = options?.maxBuffer || 1024 * 1024;
if (stderr.length < maxBuffer) {
- stderr += chunk.toString();
+ stderr += text;
}
+ options?.onData?.(text);
});
// Abort handler
diff --git a/apps/x/packages/core/src/application/lib/exec-tool.ts b/apps/x/packages/core/src/application/lib/exec-tool.ts
index 09983402..92e87fa6 100644
--- a/apps/x/packages/core/src/application/lib/exec-tool.ts
+++ b/apps/x/packages/core/src/application/lib/exec-tool.ts
@@ -1,4 +1,5 @@
import { ToolAttachment } from "@x/shared/dist/agent.js";
+import { RunEvent } from "@x/shared/dist/runs.js";
import { z } from "zod";
import { BuiltinTools } from "./builtin-tools.js";
import { executeTool } from "../../mcp/mcp.js";
@@ -9,8 +10,10 @@ import { IAbortRegistry } from "../../runs/abort-registry.js";
*/
export interface ToolContext {
runId: string;
+ toolCallId: string;
signal: AbortSignal;
abortRegistry: IAbortRegistry;
+ publish: (event: z.infer) => Promise;
}
async function execMcpTool(agentTool: z.infer & { type: "mcp" }, input: Record): Promise {
@@ -34,4 +37,4 @@ export async function execTool(agentTool: z.infer, input:
return builtinTool.execute(input, ctx);
}
}
-}
\ No newline at end of file
+}
diff --git a/apps/x/packages/shared/src/runs.ts b/apps/x/packages/shared/src/runs.ts
index ea93c8a3..7bf7a13d 100644
--- a/apps/x/packages/shared/src/runs.ts
+++ b/apps/x/packages/shared/src/runs.ts
@@ -63,6 +63,13 @@ export const ToolResultEvent = BaseRunEvent.extend({
result: z.any(),
});
+export const ToolOutputStreamEvent = BaseRunEvent.extend({
+ type: z.literal("tool-output-stream"),
+ toolCallId: z.string(),
+ toolName: z.string(),
+ output: z.string(),
+});
+
export const AskHumanRequestEvent = BaseRunEvent.extend({
type: z.literal("ask-human-request"),
toolCallId: z.string(),
@@ -106,6 +113,7 @@ export const RunEvent = z.union([
MessageEvent,
ToolInvocationEvent,
ToolResultEvent,
+ ToolOutputStreamEvent,
AskHumanRequestEvent,
AskHumanResponseEvent,
ToolPermissionRequestEvent,
From d515c423eef3d0759509b1f73b7a136809c8ef1f Mon Sep 17 00:00:00 2001
From: Arjun <6592213+arkml@users.noreply.github.com>
Date: Wed, 6 May 2026 19:11:00 +0530
Subject: [PATCH 2/2] show the terminal view
---
apps/x/apps/renderer/src/App.tsx | 4 +--
.../renderer/src/components/chat-sidebar.tsx | 34 ++++++++++++++++++-
2 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index b6e6add7..1f5cb153 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -2111,7 +2111,7 @@ function App() {
return next
})
- if (event.toolCallId) {
+ if (event.toolCallId && event.toolName !== 'executeCommand') {
setToolOpenForTab(activeChatTabIdRef.current, event.toolCallId, false)
}
@@ -4361,7 +4361,7 @@ function App() {
state={toToolState(item.status)}
/>
- {item.streamingOutput && item.status === 'running' ? (
+ {item.streamingOutput ? (
diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx
index 6fa295b1..a7551757 100644
--- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx
+++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx
@@ -20,6 +20,7 @@ import { Tool, ToolContent, ToolGroupComponent, ToolHeader, ToolTabbedContent }
import { WebSearchResult } from '@/components/ai-elements/web-search-result'
import { ComposioConnectCard } from '@/components/ai-elements/composio-connect-card'
import { PermissionRequest } from '@/components/ai-elements/permission-request'
+import { TerminalOutput } from '@/components/terminal-output'
import { AskHumanRequest } from '@/components/ai-elements/ask-human-request'
import { Suggestions } from '@/components/ai-elements/suggestions'
import { type PromptInputMessage, type FileMention } from '@/components/ai-elements/prompt-input'
@@ -59,6 +60,31 @@ const streamdownComponents = { pre: MarkdownPreOverride }
// into
so typed line breaks are preserved without requiring blank lines.
const userMessageRemarkPlugins = [...Object.values(defaultRemarkPlugins), remarkBreaks]
+function AutoScrollPre({ className, children }: { className?: string; children: React.ReactNode }) {
+ const ref = useRef(null)
+ const stickToBottom = useRef(true)
+
+ useEffect(() => {
+ const el = ref.current
+ if (el && stickToBottom.current) {
+ el.scrollTop = el.scrollHeight
+ }
+ }, [children])
+
+ const handleScroll = useCallback(() => {
+ const el = ref.current
+ if (!el) return
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 24
+ stickToBottom.current = atBottom
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+
/* ─── Billing error helpers ─── */
const BILLING_ERROR_PATTERNS = [
@@ -452,7 +478,13 @@ export function ChatSidebar({
>
-
+ {item.streamingOutput ? (
+
+
+
+ ) : (
+
+ )}
)