Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ bunx --force dreb

### Tools and interaction

dreb ships with 12 built-in tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, `wait`, and `search` (semantic codebase search). Two more tools are always active: `skill` for loading workflows, and `tasks_update` for visible task tracking. `suggest_next` (ghost text command suggestions, Tab to accept) is active by default but excluded when `--tools` is specified.
dreb ships with 13 built-in tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, `wait`, `search` (semantic codebase search), and `ask_user` (pause and ask the user a structured multiple-choice or free-text question, rendered natively in the TUI and Dashboard). Two more tools are always active: `skill` for loading workflows, and `tasks_update` for visible task tracking. `suggest_next` (ghost text command suggestions, Tab to accept) is active by default but excluded when `--tools` is specified.

Interactive mode adds slash commands such as `/model`, `/settings`, `/resume`, `/tree`, `/fork`, `/compact`, `/dream`, `/buddy`, `/export`, `/reload`, and `/hotkeys`. The message queue lets you steer a running agent or queue follow-up work without waiting for the current turn to finish.

Expand Down
6 changes: 4 additions & 2 deletions packages/coding-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ dreb

Or use a custom provider (corporate proxy, Bedrock, etc.) — see [Custom providers & models](#providers--models).

Then just talk to dreb. All 11 built-in tools are enabled by default: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, and `wait`. Use `--tools` to restrict to a subset (e.g., `--tools read,grep,find,ls` for read-only). Three additional tools — `search`, `skill`, and `tasks_update` — are always active regardless of `--tools`. `suggest_next` is active by default but excluded when `--tools` is specified. The model uses these to fulfill your requests. Add capabilities via [skills](#skills), [prompt templates](#prompt-templates), [extensions](#extensions), or [packages](#packages).
Then just talk to dreb. All 12 built-in tools are enabled by default: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, `wait`, and `ask_user`. Use `--tools` to restrict to a subset (e.g., `--tools read,grep,find,ls` for read-only). Three additional tools — `search`, `skill`, and `tasks_update` — are always active regardless of `--tools`. `suggest_next` is active by default but excluded when `--tools` is specified. The model uses these to fulfill your requests. Add capabilities via [skills](#skills), [prompt templates](#prompt-templates), [extensions](#extensions), or [packages](#packages).

**Also available:** [`@dreb/telegram`](https://www.npmjs.com/package/@dreb/telegram) — run dreb as a Telegram bot with live tool status and visible results for user-facing tools (`npm install -g @dreb/telegram`). [`@dreb/dashboard`](https://www.npmjs.com/package/@dreb/dashboard) — run `dreb dashboard` for a browser UI with fleet overview, full chat steering, inline provider/API failures with partial output preserved, sanitized raster tool images plus sent user uploads retained as bounded transcript previews by default, subagent observability, host file browser, curated appearance themes (per-browser light/dark), and Tailscale/rotating-code pairing (`npm install -g @dreb/dashboard`; see [docs/dashboard.md](docs/dashboard.md)). Tool images cross browser-facing transport as content-addressed references; browser-local Settings offers placeholders, bounded previews, or informed-opt-in originals, with size disclosure and confirmation above 1 MiB. Full-resolution HTML export remains self-contained. Compact SSE snapshots update live fleet cards without repeatedly fetching the cross-project inventory, and session drill-in hydrates state, messages, and background agents through one ordered snapshot request. Terminal provider failures show their reason on fleet cards, while transient failures clear terminal state when automatic retry begins and remain recorded inline on the failed attempt. Its top bar and persistent session header indicators report connecting, connected, retrying, resyncing, disconnected, or auth failed; bounded SSE replay plus an explicit snapshot barrier restores session state, tasks, and image references after a reload, restart, gap, backpressure disconnect, or stalled stream, while authenticated image routes recover bytes separately from authoritative transcripts.

Expand Down Expand Up @@ -639,7 +639,9 @@ cat README.md | dreb -p "Summarize this text"
| `--tools <list>` | Comma-separated list of tools to enable (default: all) |
| `--no-tools` | Disable all built-in tools (extension tools still work) |

Available built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, `wait`, `search`
Available built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`, `web_search`, `web_fetch`, `subagent`, `wait`, `search`, `ask_user`

`ask_user` pauses the turn and asks you a structured clarifying question — optional 2-4 multiple-choice options (single- or multi-select) plus a "type your own answer" field — rendered natively in the TUI and the Dashboard, and over RPC. Skipping, Escape, tool abort, timeout, and headless/no-UI modes all resolve gracefully, so the agent never deadlocks waiting on an absent user. When there are no options the free-text field is always shown, so every question is answerable. An optional `timeoutSeconds` auto-skips the question after the given number of seconds with a live countdown on every surface. Concurrent `ask_user` calls are shown strictly one at a time (FIFO). In the TUI: `↑`/`↓` move between options and the free-text field, `Space` toggles a checkbox (multi-select), `Enter` submits, and `Esc` skips. In the Dashboard: native radios/checkboxes plus a text field, a skip button, and `Esc` to skip.

Three additional tools are always active but don't appear in `--tools`:
- `skill` — invokes [skills](#skills) programmatically
Expand Down
22 changes: 21 additions & 1 deletion packages/coding-agent/docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Extensions are TypeScript modules that extend dreb's behavior. They can subscrib
**Key capabilities:**
- **Custom tools** - Register tools the LLM can call via `dreb.registerTool()`
- **Event interception** - Block or modify tool calls, inject context, customize compaction
- **User interaction** - Prompt users via `ctx.ui` (select, confirm, input, notify)
- **User interaction** - Prompt users via `ctx.ui` (select, confirm, input, ask, notify)
- **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()` for complex interactions
- **Custom commands** - Register commands like `/mycommand` via `dreb.registerCommand()`
- **Session persistence** - Store state that survives restarts via `dreb.appendEntry()`
Expand Down Expand Up @@ -161,6 +161,26 @@ export default function (dreb: ExtensionAPI) {
ctx.ui.notify("Done!", "success");
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)

// ctx.ui.ask — a rich clarifying question with options + free text,
// rendered natively in the TUI and Dashboard (and over RPC). Resolves to
// { selected: string[], customText?: string }, or undefined when skipped,
// cancelled, or timed out. This is the same primitive that powers the
// built-in `ask_user` tool.
const answer = await ctx.ui.ask(
{
title: "Choose a database",
question: "Which persistence strategy should I use?",
options: ["SQLite", "PostgreSQL", "Keep the JSON file"],
allowFreeText: true, // default; offers a "type your own answer" field
multiSelect: false, // true → checkboxes, combined with any free text
multiline: false, // true → multi-line free-text area
},
{ signal, timeout: 60000 }, // both optional; absent user never deadlocks
);
if (!answer) {
// user skipped / cancelled / timed out — continue gracefully
}
});

// Register tools, commands, shortcuts, flags
Expand Down
46 changes: 41 additions & 5 deletions packages/coding-agent/docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,17 @@ The `model` field is a full [Model](#model) object or `null`. `scopedModels` is

#### get_dashboard_snapshot

Capture the dashboard-visible parent-session state, full parent transcript, and background-agent registry at one RPC command boundary. This is for authoritative recovery, not ordinary incremental refreshes.
Capture the dashboard-visible parent-session state, full parent transcript, background-agent registry, and blocking extension UI requests at one RPC command boundary. This is for authoritative recovery, not ordinary incremental refreshes.

```json
{"id": "snapshot-7", "type": "get_dashboard_snapshot"}
```

The `RpcDashboardSnapshot` result is a `snapshotId`, a complete `RpcSessionState` (including `tasks`), `messages`, and `backgroundAgents`. The RPC child writes a `RpcDashboardSnapshotBarrierEvent` to stdout **immediately before** the matching response line:
The `RpcDashboardSnapshot` result is a `snapshotId`, a complete `RpcSessionState` (including `tasks`), `messages`, `backgroundAgents`, and `pendingExtensionUiRequests`. The last field contains every blocking `select`, `confirm`, `input`, `editor`, or `ask` request still awaiting a host response, so a recovering Dashboard can restore the answer UI instead of leaving the runtime blocked. The RPC child writes a `RpcDashboardSnapshotBarrierEvent` to stdout **immediately before** the matching response line:

```json
{"type":"dashboard_snapshot_barrier","snapshotId":"snapshot-7"}
{"id":"snapshot-7","type":"response","command":"get_dashboard_snapshot","success":true,"data":{"snapshotId":"snapshot-7","state":{...},"messages":[...],"backgroundAgents":[...]}}
{"id":"snapshot-7","type":"response","command":"get_dashboard_snapshot","success":true,"data":{"snapshotId":"snapshot-7","state":{...},"messages":[...],"backgroundAgents":[...],"pendingExtensionUiRequests":[...]}}
```

Stdout JSONL ordering is the contract: a relay records its current event-stream sequence when the marker arrives, before resolving the response, and pairs the snapshot only with that exact marker. The dashboard returns that captured sequence as `/api/resync.barrierSeq`; consumers discard queued events through it and replay only later events. The marker itself is not broadcast as another browser event, so one recovering client does not interrupt healthy clients. Do not infer ordering from request/response timing; see [dashboard recovery](dashboard.md#live-connection-and-recovery).
Expand Down Expand Up @@ -1874,7 +1874,7 @@ Extensions can request user interaction via `ctx.ui.select()`, `ctx.ui.confirm()

There are two categories of extension UI methods:

- **Dialog methods** (`select`, `confirm`, `input`, `editor`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.
- **Dialog methods** (`select`, `confirm`, `input`, `editor`, `ask`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.
- **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): emit an `extension_ui_request` on stdout but do not expect a response. The client can display the information or ignore it.

If a dialog method includes a `timeout` field, the agent-side will auto-resolve with a default value when the timeout expires. The client does not need to track timeouts.
Expand Down Expand Up @@ -1961,6 +1961,31 @@ Open a multi-line text editor with optional prefilled content.

Expected response: `extension_ui_response` with `value` (the edited text) or `cancelled: true`.

#### ask

Ask the user a rich clarifying question with optional single- or multi-select options and an optional free-text field. This powers the built-in `ask_user` tool. `options` (2-4) is optional; `allowFreeText` (default `true`), `multiSelect`, and `multiline` are optional booleans.

```json
{
"type": "extension_ui_request",
"id": "uuid-5",
"method": "ask",
"title": "Choose a database",
"question": "Which persistence strategy should I use?",
"options": ["SQLite", "PostgreSQL", "Keep the JSON file"],
"allowFreeText": true,
"multiSelect": false,
"multiline": false,
"timeout": 60000
}
```

Expected response: `extension_ui_response` with `selected` (array of chosen option strings, possibly empty) and optional `customText` (the typed answer), or `cancelled: true` to skip. The client should treat an empty `selected` with no `customText` as a skip.

```json
{ "type": "extension_ui_response", "id": "uuid-5", "selected": ["SQLite"], "customText": "with WAL enabled" }
```

#### notify

Display a notification. Fire-and-forget, no response expected.
Expand Down Expand Up @@ -2038,7 +2063,7 @@ Set the text in the input editor. Fire-and-forget.

### Extension UI Responses (stdin)

Responses are sent for dialog methods only (`select`, `confirm`, `input`, `editor`). The `id` must match the request.
Responses are sent for dialog methods only (`select`, `confirm`, `input`, `editor`, `ask`). The `id` must match the request.

#### Value response (select, input, editor)

Expand All @@ -2060,6 +2085,17 @@ Dismiss any dialog method. The extension receives `undefined` (for select/input/
{"type": "extension_ui_response", "id": "uuid-3", "cancelled": true}
```

After any dialog settles, RPC emits a lifecycle event on stdout so hosts can
remove the matching UI even when the dialog ended locally because of timeout,
abort, or runtime shutdown:

```json
{"type": "extension_ui_response_handled", "id": "uuid-3"}
```

Hosts should treat this event as idempotent; it can arrive after the host has
already removed a successfully answered request.

## Error Handling

Failed commands return a response with `success: false`:
Expand Down
39 changes: 25 additions & 14 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,12 @@ export class AgentSession {

/** Emit extension events based on agent events */
private async _emitExtensionEvent(event: AgentEvent): Promise<void> {
if (!this._extensionRunner) return;
// The runner is created unconditionally (so built-in tools like ask_user
// always have a UI context). When no extensions are loaded there are no
// handlers to invoke — return synchronously to avoid inserting an extra
// await tick per event, which would otherwise delay the final agent_end
// emission past when prompt() resolves.
if (!this._extensionRunner || !this._extensionRunner.hasExtensions) return;

if (event.type === "agent_start") {
this._turnIndex = 0;
Expand Down Expand Up @@ -2901,10 +2906,16 @@ export class AgentSession {
? wrapRegisteredTools(allCustomTools, this._extensionRunner)
: [];

// Give base tools a per-execution extension context so built-ins like
// ask_user can reach ctx.ui. createContext() snapshots hasUI at call time,
// so print/RPC-without-host modes degrade to the no-op UI (hasUI === false).
const baseToolCtxFactory = this._extensionRunner
? () => (this._extensionRunner as ExtensionRunner).createContext()
: undefined;
const toolRegistry = new Map(
Array.from(this._baseToolDefinitions.values()).map((definition) => [
definition.name,
wrapToolDefinition(definition),
wrapToolDefinition(definition, baseToolCtxFactory),
]),
);
for (const tool of wrappedExtensionTools as AgentTool[]) {
Expand Down Expand Up @@ -3010,18 +3021,17 @@ export class AgentSession {
}
}

const hasExtensions = extensionsResult.extensions.length > 0;
const hasCustomTools = this._customTools.length > 0;
this._extensionRunner =
hasExtensions || hasCustomTools
? new ExtensionRunner(
extensionsResult.extensions,
extensionsResult.runtime,
this._cwd,
this.sessionManager,
this._modelRegistry,
)
: undefined;
// The runner also owns the cross-surface UI context used by built-in
// tools such as ask_user. Create it even when no third-party extensions
// are loaded; otherwise ordinary TUI/Dashboard sessions give base tools
// no ctx.ui and ask_user can never open its dialog.
this._extensionRunner = new ExtensionRunner(
extensionsResult.extensions,
extensionsResult.runtime,
this._cwd,
this.sessionManager,
this._modelRegistry,
);
if (this._extensionRunnerRef) {
this._extensionRunnerRef.current = this._extensionRunner;
}
Expand All @@ -3045,6 +3055,7 @@ export class AgentSession {
"subagent",
"wait",
"search",
"ask_user",
"skill",
"tasks_update",
"suggest_next",
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/src/core/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type {
AppendEntryHandler,
// App keybindings (for custom editors)
AppKeybinding,
AskRequest,
AskResult,
// Events - Tool (ToolCallEvent types)
BashToolCallEvent,
BashToolResultEvent,
Expand Down
11 changes: 11 additions & 0 deletions packages/coding-agent/src/core/extensions/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const noOpUIContext: ExtensionUIContext = {
select: async () => undefined,
confirm: async () => false,
input: async () => undefined,
ask: async () => undefined,
notify: () => {},
onTerminalInput: () => () => {},
setStatus: () => {},
Expand Down Expand Up @@ -448,6 +449,16 @@ export class ExtensionRunner {
}
}

/**
* Whether any extensions are loaded. The runner is now created
* unconditionally so built-in tools (e.g. ask_user) always have a UI
* context, but when no extensions are present there are no event handlers
* to run — callers can skip the async emit path entirely.
*/
get hasExtensions(): boolean {
return this.extensions.length > 0;
}

hasHandlers(eventType: string): boolean {
for (const ext of this.extensions) {
const handlers = ext.handlers.get(eventType);
Expand Down
38 changes: 38 additions & 0 deletions packages/coding-agent/src/core/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,37 @@ export interface ExtensionUIDialogOptions {
timeout?: number;
}

/**
* A rich question for {@link ExtensionUIContext.ask}. Presents optional
* suggested options (single- or multi-select) alongside an optional free-text
* field, rendered natively in every surface (TUI, Dashboard, RPC host).
*/
export interface AskRequest {
/** The question to ask the user. */
question: string;
/** Short bold header. Defaults to a generic prompt title when omitted. */
title?: string;
/** 2-4 suggested options. */
options?: string[];
/** Offer a "type your own answer" field. Defaults to true. */
allowFreeText?: boolean;
/** Render options as checkboxes (multiple answers) instead of radios. */
multiSelect?: boolean;
/** Use a multi-line text area for the free-text field. */
multiline?: boolean;
}

/**
* The user's answer to an {@link AskRequest}. A cancelled/skipped/timed-out
* question resolves to `undefined` instead of an {@link AskResult}.
*/
export interface AskResult {
/** Options the user selected (empty when only free text was provided). */
selected: string[];
/** Free-text answer, when the user typed one. */
customText?: string;
}

/** Placement for extension widgets. */
export type WidgetPlacement = "aboveEditor" | "belowEditor";

Expand All @@ -110,6 +141,13 @@ export interface ExtensionUIContext {
/** Show a text input dialog. */
input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;

/**
* Show a rich question with suggested options, optional free text, and
* single- or multi-select. Resolves to the user's answer, or `undefined`
* when the question is skipped, cancelled, or times out.
*/
ask(request: AskRequest, opts?: ExtensionUIDialogOptions): Promise<AskResult | undefined>;

/** Show a notification to the user. */
notify(message: string, type?: "info" | "warning" | "error"): void;

Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/core/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
"web_fetch",
"subagent",
"wait",
"ask_user",
];
const initialActiveToolNames: string[] = options.tools
? [...options.tools.map((t) => t.name).filter((n): n is ToolName => n in allTools), ...alwaysActiveBuiltins]
Expand Down
Loading
Loading