experimental-ash 0.55.0 → 0.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/docs/public/advanced/cli-build-and-debugging.md +1 -1
- package/dist/docs/public/advanced/dev-tui.md +202 -0
- package/dist/docs/public/advanced/meta.json +1 -0
- package/dist/docs/public/advanced/vercel-deployment.md +3 -3
- package/dist/docs/public/getting-started.mdx +1 -1
- package/dist/docs/public/human-in-the-loop.mdx +4 -4
- package/dist/docs/public/onboarding.md +2 -2
- package/dist/src/cli/dev/{repl.d.ts → repl/repl.d.ts} +1 -1
- package/dist/src/cli/dev/{repl.js → repl/repl.js} +1 -1
- package/dist/src/cli/dev/tui/layout.d.ts +24 -0
- package/dist/src/cli/dev/tui/layout.js +3 -0
- package/dist/src/cli/dev/tui/markdown.d.ts +14 -0
- package/dist/src/cli/dev/tui/markdown.js +3 -0
- package/dist/src/cli/dev/tui/runner.d.ts +205 -0
- package/dist/src/cli/dev/tui/runner.js +1 -0
- package/dist/src/cli/dev/tui/terminal-frame-buffer.d.ts +21 -0
- package/dist/src/cli/dev/tui/terminal-frame-buffer.js +2 -0
- package/dist/src/cli/dev/tui/terminal-renderer.d.ts +111 -0
- package/dist/src/cli/dev/tui/terminal-renderer.js +12 -0
- package/dist/src/cli/dev/tui/terminal-text.d.ts +6 -0
- package/dist/src/cli/dev/tui/terminal-text.js +1 -0
- package/dist/src/cli/dev/tui/test/index.d.ts +10 -0
- package/dist/src/cli/dev/tui/test/index.js +1 -0
- package/dist/src/cli/dev/tui/test/mock-terminal.d.ts +28 -0
- package/dist/src/cli/dev/tui/test/mock-terminal.js +3 -0
- package/dist/src/cli/dev/tui/tui.d.ts +32 -0
- package/dist/src/cli/dev/tui/tui.js +1 -0
- package/dist/src/cli/dev/tui/types.d.ts +68 -0
- package/dist/src/cli/dev/tui/types.js +1 -0
- package/dist/src/cli/run.d.ts +66 -0
- package/dist/src/cli/run.js +2 -2
- package/dist/src/context/dynamic-tool-lifecycle.js +1 -1
- package/dist/src/evals/scorers/autoevals.js +1 -1
- package/dist/src/execution/node-step.js +1 -1
- package/dist/src/harness/code-mode.js +1 -1
- package/dist/src/harness/tool-loop.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
- package/dist/src/public/channels/ash.js +1 -1
- package/package.json +6 -1
- /package/dist/src/cli/dev/{input-requests.d.ts → repl/input-requests.d.ts} +0 -0
- /package/dist/src/cli/dev/{input-requests.js → repl/input-requests.js} +0 -0
- /package/dist/src/cli/dev/{input.d.ts → repl/input.d.ts} +0 -0
- /package/dist/src/cli/dev/{input.js → repl/input.js} +0 -0
- /package/dist/src/cli/dev/{terminal.d.ts → repl/terminal.d.ts} +0 -0
- /package/dist/src/cli/dev/{terminal.js → repl/terminal.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.55.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 93c1d3c: feat(ash): add new TUI for `ash dev`, replacing the previous REPL by default
|
|
8
|
+
- 6a8ec9f: Code-mode nested tool calls now return raw tool outputs inside the VM and expose those raw outputs to Ash lifecycle handlers.
|
|
9
|
+
|
|
3
10
|
## 0.55.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
|
@@ -13,7 +13,7 @@ From your app root:
|
|
|
13
13
|
- `ash info` - inspect the discovered authored surface and runtime details
|
|
14
14
|
- `ash build` - compile `.ash/` artifacts and build the host output
|
|
15
15
|
- `ash start` - serve the built `.output/` app
|
|
16
|
-
- `ash dev` - start the local runtime and open the
|
|
16
|
+
- `ash dev` - start the local runtime and open the interactive [terminal UI](./dev-tui.md)
|
|
17
17
|
- `ash eval` - run eval suites against the current app or a remote target
|
|
18
18
|
|
|
19
19
|
## Recommended Loop
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Dev TUI (`ash dev`)"
|
|
3
|
+
description: "Run and chat with your agent locally in an interactive terminal UI."
|
|
4
|
+
url: /dev-tui
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`ash dev` starts the local runtime and opens an interactive terminal UI (the TUI) where you can
|
|
8
|
+
chat with your agent, watch it stream, approve tool calls, and answer its questions — all without
|
|
9
|
+
leaving the terminal. It is the fastest way to exercise an agent while you iterate.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
From your app root:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ash dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This boots the local runtime, starts a dev server, and drops you into the TUI connected to it. Type
|
|
20
|
+
a prompt, press `Enter`, and the agent's response streams in. Press `Ctrl+C` to
|
|
21
|
+
quit; this also shuts the dev server down.
|
|
22
|
+
|
|
23
|
+
## The Layout
|
|
24
|
+
|
|
25
|
+
The TUI looks something like this (with the different sections being color-coded in reality):
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
┌ Ash ────────────────────────────────────────────────────────────────── 7,421 tokens ┐
|
|
29
|
+
│ ╭ User ───────────────────────────────────────────────────────────────────────────╮ │
|
|
30
|
+
│ │ what's the weather in San Francisco? │ │
|
|
31
|
+
│ ╰─────────────────────────────────────────────────────────────────────────────────╯ │
|
|
32
|
+
│ ╭ Log · stdout ···································································╮ │
|
|
33
|
+
│ │ weather-agent session started { sessionId: 'wrun_a1b2c3d4e5f6g7h8i9', │ │
|
|
34
|
+
│ │ channel: 'http' } │ │
|
|
35
|
+
│ ╰·················································································╯ │
|
|
36
|
+
│ ╭ Reasoning ······································································╮ │
|
|
37
|
+
│ │ The user is asking about weather, so I need to load the get-weather skill │ │
|
|
38
|
+
│ │ first. │ │
|
|
39
|
+
│ ╰·················································································╯ │
|
|
40
|
+
│ ╭ Tool · load_skill ──────────────────────────────────────────────────────── done ╮ │
|
|
41
|
+
│ ╰─────────────────────────────────────────────────────────────────────────────────╯ │
|
|
42
|
+
│ ╭ Log · stdout ···································································╮ │
|
|
43
|
+
│ │ weather lookup { city: 'San Francisco', temperatureF: 72, condition: 'Sunny' } │ │
|
|
44
|
+
│ ╰·················································································╯ │
|
|
45
|
+
│ ╭ Tool · get_weather ─────────────────────────────────────────────────────── done ╮ │
|
|
46
|
+
│ ╰─────────────────────────────────────────────────────────────────────────────────╯ │
|
|
47
|
+
│ ╭ Assistant ──────────────────────────────────────────────────────────────────────╮ │
|
|
48
|
+
│ │ Using the local weather tool: San Francisco is currently 72°F and sunny, with a │ │
|
|
49
|
+
│ │ light breeze. │ │
|
|
50
|
+
│ ╰─────────────────────────────────────────────────────────────────────────────────╯ │
|
|
51
|
+
│ ╭ Log · stdout ···································································╮ │
|
|
52
|
+
│ │ weather-agent turn completed { │ │
|
|
53
|
+
│ │ sessionId: 'wrun_a1b2c3d4e5f6g7h8i9', │ │
|
|
54
|
+
│ │ turnId: 'turn_0', │ │
|
|
55
|
+
│ │ sequence: 0 │ │
|
|
56
|
+
│ │ } │ │
|
|
57
|
+
│ ╰·················································································╯ │
|
|
58
|
+
└─────────────────────────────────────────────────────────────────────────────────────┘
|
|
59
|
+
┌ Input ──────────────────────────────────────────────────────────────────────────────┐
|
|
60
|
+
│ > █ │
|
|
61
|
+
└─────────────────────────────────────────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The TUI is made up of three regions:
|
|
65
|
+
|
|
66
|
+
- **Header** — the app name and, while a turn is running, live response stats (e.g. tokens per second,
|
|
67
|
+
or total tokens).
|
|
68
|
+
- **Transcript** — the scrollable conversation: your prompts, the agent's streamed messages,
|
|
69
|
+
reasoning, tool calls, subagent activity, connection-authorization prompts, and server/agent logs.
|
|
70
|
+
- **Input / status bar** — where you type, plus a contextual hint line showing the current state
|
|
71
|
+
(`Streaming…`, `Executing tools…`, `Done`, an approval prompt, and so on) and the keys available.
|
|
72
|
+
|
|
73
|
+
## Keyboard Shortcuts
|
|
74
|
+
|
|
75
|
+
| Key | Action |
|
|
76
|
+
| --------- | ---------------------------------------------------------- |
|
|
77
|
+
| `Enter` | Send the prompt / confirm the current answer |
|
|
78
|
+
| `↑` / `↓` | Scroll the transcript |
|
|
79
|
+
| `Esc` | Back out of (or clear) the current input without answering |
|
|
80
|
+
| `Ctrl+R` | Repaint the screen |
|
|
81
|
+
| `q` | Quit when the agent is idle (a turn has finished) |
|
|
82
|
+
| `Ctrl+C` | Quit at any time, interrupting a running turn |
|
|
83
|
+
|
|
84
|
+
## Slash Commands
|
|
85
|
+
|
|
86
|
+
Type these as a prompt:
|
|
87
|
+
|
|
88
|
+
- `/new` — start a fresh session. Clears the transcript and the durable session cursor so the next
|
|
89
|
+
turn begins a new server-side conversation.
|
|
90
|
+
- `/exit` (or `/quit`) — leave the TUI and shut the dev server down, the same as `Ctrl+C`.
|
|
91
|
+
|
|
92
|
+
## Human In The Loop
|
|
93
|
+
|
|
94
|
+
When the agent needs you, the TUI prompts inline and the status bar tells you which keys apply:
|
|
95
|
+
|
|
96
|
+
- **Tool approvals** — answer `y` / `n` to approve or deny.
|
|
97
|
+
- **Questions** — for option questions, use `↑` / `↓` to select and `Enter`
|
|
98
|
+
to confirm; for freeform answers, type and press `Enter`. `Esc` backs out.
|
|
99
|
+
|
|
100
|
+
If you press `Esc` on a pending request and then send a normal message, the request is
|
|
101
|
+
ignored. See [Human In The Loop](../human-in-the-loop.mdx) for how these requests are authored.
|
|
102
|
+
|
|
103
|
+
## Connection Authorization
|
|
104
|
+
|
|
105
|
+
When a tool needs an authorized [connection](../connections.mdx) — for example an MCP server behind
|
|
106
|
+
OAuth — the TUI renders the authorization challenge inline: the URL to visit and any user/device
|
|
107
|
+
code to enter. The status bar shows a `Waiting for connection authorization` hint until you complete
|
|
108
|
+
the flow in your browser, then the turn resumes automatically.
|
|
109
|
+
|
|
110
|
+
## Subagents
|
|
111
|
+
|
|
112
|
+
When the agent dispatches a [subagent](../subagents.mdx), its activity renders as its own section in
|
|
113
|
+
the transcript, with a live status in the section header. By default subagent sections are shown in
|
|
114
|
+
full; use `--subagents` to collapse or hide them (see below).
|
|
115
|
+
|
|
116
|
+
## Logs
|
|
117
|
+
|
|
118
|
+
`stdout` and `stderr` from the dev server and the agent are captured and shown inline in the
|
|
119
|
+
transcript, so you can correlate log output with the turn that produced it. Control how much is shown
|
|
120
|
+
with `--logs`.
|
|
121
|
+
|
|
122
|
+
## Display Options
|
|
123
|
+
|
|
124
|
+
The TUI's density is configurable. Each of these flags accepts one of `full`, `collapsed`,
|
|
125
|
+
`auto-collapsed`, or `hidden` unless noted otherwise. `auto-collapsed` shows a section in full while
|
|
126
|
+
it is active and collapses it once finished.
|
|
127
|
+
|
|
128
|
+
| Flag | Controls | Default |
|
|
129
|
+
| ----------------------------------- | ----------------------------------------------------------- | ----------------- |
|
|
130
|
+
| `--tools <mode>` | How tool calls render | `auto-collapsed` |
|
|
131
|
+
| `--reasoning <mode>` | How reasoning renders | `full` |
|
|
132
|
+
| `--subagents <mode>` | How subagent sections render | `full` |
|
|
133
|
+
| `--connection-auth <mode>` | How connection-authorization prompts render | `full` |
|
|
134
|
+
| `--assistant-response-stats <mode>` | Header statistic: `tokens` or `tokensPerSecond` | `tokensPerSecond` |
|
|
135
|
+
| `--context-size <tokens>` | Model context window size, shown as a usage percentage | _unset_ |
|
|
136
|
+
| `--logs <mode>` | Which server/agent logs to show: `all`, `stderr`, or `none` | `all` |
|
|
137
|
+
|
|
138
|
+
For example, to keep tool calls fully expanded and show absolute token counts:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
ash dev --tools full --assistant-response-stats tokens
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Set `--context-size` to your model's context window to see how full it is getting:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
ash dev --context-size 200000
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Server Options
|
|
151
|
+
|
|
152
|
+
By default `ash dev` binds the dev server to a local interface and uses `$PORT`, falling back to
|
|
153
|
+
`3000`. Override either:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
ash dev --host 0.0.0.0 --port 4000
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Connecting To A Remote Server
|
|
160
|
+
|
|
161
|
+
Instead of starting a local server, point the TUI at an already-running one — for example a Vercel
|
|
162
|
+
preview or production deployment:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
ash dev https://<your-app>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The bare URL is shorthand for `--url https://<your-app>`. When connecting to a remote server, the
|
|
169
|
+
`--host`, `--port`, and `--no-ui` options do not apply.
|
|
170
|
+
|
|
171
|
+
If the deployment uses Vercel preview protection, set `VERCEL_AUTOMATION_BYPASS_SECRET` locally
|
|
172
|
+
before launching so the TUI can authenticate against it. See
|
|
173
|
+
[Vercel Deployment](./vercel-deployment.md) for details.
|
|
174
|
+
|
|
175
|
+
## Headless Mode
|
|
176
|
+
|
|
177
|
+
Pass `--no-ui` to start the dev server and keep it running without an interactive UI — useful when
|
|
178
|
+
another process drives the HTTP routes:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
ash dev --no-ui
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`ash dev` also falls back to headless automatically when the terminal is not a TTY (for example in
|
|
185
|
+
CI or when output is piped), printing a note so the missing UI is not mistaken for a hang.
|
|
186
|
+
|
|
187
|
+
## The Classic REPL
|
|
188
|
+
|
|
189
|
+
Before the TUI, `ash dev` opened a simpler line-based REPL. It is still available behind a flag
|
|
190
|
+
during the transition and will be removed in a future release:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
ash dev --repl
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Prefer the default TUI for day-to-day work.
|
|
197
|
+
|
|
198
|
+
## What To Read Next
|
|
199
|
+
|
|
200
|
+
- [CLI, Build, And Debugging](./cli-build-and-debugging.md)
|
|
201
|
+
- [Human In The Loop](../human-in-the-loop.mdx)
|
|
202
|
+
- [Vercel Deployment](./vercel-deployment.md)
|
|
@@ -33,7 +33,7 @@ Typical Vercel env vars for an Ash app include:
|
|
|
33
33
|
- optional `ASH_SANDBOX_BACKEND`
|
|
34
34
|
|
|
35
35
|
If your deployment uses Vercel preview protection and you want to connect to it with `ash dev`, set
|
|
36
|
-
`VERCEL_AUTOMATION_BYPASS_SECRET` locally before launching the
|
|
36
|
+
`VERCEL_AUTOMATION_BYPASS_SECRET` locally before launching the dev TUI against the deployed URL.
|
|
37
37
|
|
|
38
38
|
## Sandbox Backend
|
|
39
39
|
|
|
@@ -88,9 +88,9 @@ Then attach to the returned stream URL pattern:
|
|
|
88
88
|
curl https://<your-app>/ash/v1/session/<sessionId>/stream
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
## Remote
|
|
91
|
+
## Remote Dev
|
|
92
92
|
|
|
93
|
-
You can point the Ash
|
|
93
|
+
You can point the Ash dev [TUI](./dev-tui.md) at a deployed app:
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
ash dev https://<your-app>
|
|
@@ -98,7 +98,7 @@ Useful commands:
|
|
|
98
98
|
- `ash info`: show the active routes and compiled artifacts
|
|
99
99
|
- `ash build`: compile the agent into `.ash/` and build the host output
|
|
100
100
|
- `ash start`: serve the built `.output/` app
|
|
101
|
-
- `ash dev`: start the local runtime and open the
|
|
101
|
+
- `ash dev`: start the local runtime and open the interactive [terminal UI](./advanced/dev-tui.md)
|
|
102
102
|
|
|
103
103
|
## Send A Message
|
|
104
104
|
|
|
@@ -244,11 +244,11 @@ webhook URL — no separate interactivity endpoint needed.
|
|
|
244
244
|
|
|
245
245
|
## CLI Behavior
|
|
246
246
|
|
|
247
|
-
`ash dev` surfaces pending HITL requests directly in the
|
|
247
|
+
`ash dev` surfaces pending HITL requests directly in the [terminal UI](./advanced/dev-tui.md):
|
|
248
248
|
|
|
249
|
-
- tool approvals prompt for
|
|
250
|
-
- questions
|
|
251
|
-
- `Escape`
|
|
249
|
+
- tool approvals prompt for `y` / `n`
|
|
250
|
+
- option questions are answered with `↑` / `↓` to select and `Enter` to confirm; freeform questions accept typed input
|
|
251
|
+
- `Escape` backs out without answering
|
|
252
252
|
|
|
253
253
|
If you press `Escape` and then send a normal message, the pending requests are ignored.
|
|
254
254
|
|
|
@@ -24,7 +24,7 @@ Collect these up front. A capable agent should use a structured question UI (suc
|
|
|
24
24
|
|
|
25
25
|
1. **Name** — the project / directory name (kebab-case). Also used as the Slack connector slug.
|
|
26
26
|
2. **Model** — for example `anthropic/claude-sonnet-4.6` or `openai/gpt-5-mini`. Baked into `agent/agent.ts`.
|
|
27
|
-
3. **Channels** — `web` can be scaffolded headlessly. `slack` is an interactive follow-up because Vercel Connect opens a browser OAuth flow. The local
|
|
27
|
+
3. **Channels** — `web` can be scaffolded headlessly. `slack` is an interactive follow-up because Vercel Connect opens a browser OAuth flow. The local terminal UI is always available via `ash dev`, regardless of channel choice.
|
|
28
28
|
4. **Model provider** — how the agent reaches a model:
|
|
29
29
|
- Vercel project (default) — create a project, or pass `--project <slug>` to link an existing one, and use AI Gateway via OIDC.
|
|
30
30
|
- API key override — pass `--gateway-api-key <key>` to write `AI_GATEWAY_API_KEY` to `.env.local`.
|
|
@@ -103,7 +103,7 @@ Treat setup as successful only when `ash info --json` reports `status: "ready"`
|
|
|
103
103
|
## Step 4 — Run and test locally
|
|
104
104
|
|
|
105
105
|
- Web chat: `pnpm dev`, open `http://localhost:3000`, send a message.
|
|
106
|
-
-
|
|
106
|
+
- Terminal UI: `ash dev`, chat in the terminal.
|
|
107
107
|
|
|
108
108
|
The agent replies only if it can reach a model: either `AI_GATEWAY_API_KEY` is set in `.env.local`, or the Vercel project is linked and you have run `vercel env pull --yes`.
|
|
109
109
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type HandleMessageStreamEvent } from "#protocol/message.js";
|
|
2
2
|
import type { InputRequest } from "#runtime/input/types.js";
|
|
3
3
|
import { type CliTheme } from "#cli/ui/output.js";
|
|
4
|
-
import { createDevelopmentTerminal } from "#cli/dev/terminal.js";
|
|
4
|
+
import { createDevelopmentTerminal } from "#cli/dev/repl/terminal.js";
|
|
5
5
|
export type ReplDisplayBlockKind = "content" | "meta";
|
|
6
6
|
export type LiveContentKind = "message" | "reasoning";
|
|
7
7
|
export interface FormattedContentEvent {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ASH_CONTINUE_SESSION_ROUTE_PATTERN,ASH_CREATE_SESSION_ROUTE_PATH,ASH_MESSAGE_STREAM_ROUTE_PATTERN,createAshMessageStreamRoutePath}from"#protocol/routes.js";import{createInterface,emitKeypressEvents}from"node:readline";import{createCliTheme,renderCliBanner,renderCliSection,renderCliSpeakerLine,renderCliTaggedLine}from"#cli/ui/output.js";import{openStreamIterable}from"#client/open-stream.js";import{isCurrentTurnBoundaryEvent}from"#protocol/message.js";import{toErrorMessage}from"#shared/errors.js";import{createDevelopmentRequestHeadersAsync}from"#services/dev-client/request-headers.js";import{extractCurrentTurnBoundaryEvent}from"#services/dev-client/stream.js";import{resolveDevelopmentServerResourceUrl}from"#services/dev-client/url.js";import{formatVercelAuthChallengeMessage,isVercelAuthChallenge}from"#services/dev-client/vercel-auth-error.js";import{createDevClient}from"#services/dev-client.js";import{parseDevReplInput}from"#cli/dev/input.js";import{ESCAPED_RUNTIME_INPUT_PROMPT,extractPendingRuntimeInputRequests,promptForRuntimeInputRequests}from"#cli/dev/input-requests.js";import{createDevelopmentTerminal}from"#cli/dev/terminal.js";function renderConnectionRows(r){let i=[{label:`Server`,tone:`info`,value:r.serverUrl},{label:`Create`,tone:`info`,value:`POST ${ASH_CREATE_SESSION_ROUTE_PATH}`},{label:`Continue`,tone:`info`,value:`POST ${ASH_CONTINUE_SESSION_ROUTE_PATTERN}`},{label:`Stream`,tone:`info`,value:`GET ${ASH_MESSAGE_STREAM_ROUTE_PATTERN}`}];return i.push({label:`Session`,value:`Follow-up messages reuse the active continuation token.`}),i}function renderCommandRows(){return[{label:`/help`,value:`Print the connection contract and available commands.`},{label:`/new`,value:`Clear the current durable session cursor.`},{label:`/exit`,value:`Exit the REPL.`}]}function renderIntro(e,t){return[renderCliBanner(e,{subtitle:`Interactive development REPL for the active Ash server.`,title:`Ash Dev`}),``,renderCliSection(e,{rows:renderConnectionRows(t),title:`Connection`}),``,renderCliSection(e,{rows:renderCommandRows(),title:`Commands`})].join(`
|
|
1
|
+
import{ASH_CONTINUE_SESSION_ROUTE_PATTERN,ASH_CREATE_SESSION_ROUTE_PATH,ASH_MESSAGE_STREAM_ROUTE_PATTERN,createAshMessageStreamRoutePath}from"#protocol/routes.js";import{createInterface,emitKeypressEvents}from"node:readline";import{createCliTheme,renderCliBanner,renderCliSection,renderCliSpeakerLine,renderCliTaggedLine}from"#cli/ui/output.js";import{openStreamIterable}from"#client/open-stream.js";import{isCurrentTurnBoundaryEvent}from"#protocol/message.js";import{toErrorMessage}from"#shared/errors.js";import{createDevelopmentRequestHeadersAsync}from"#services/dev-client/request-headers.js";import{extractCurrentTurnBoundaryEvent}from"#services/dev-client/stream.js";import{resolveDevelopmentServerResourceUrl}from"#services/dev-client/url.js";import{formatVercelAuthChallengeMessage,isVercelAuthChallenge}from"#services/dev-client/vercel-auth-error.js";import{createDevClient}from"#services/dev-client.js";import{parseDevReplInput}from"#cli/dev/repl/input.js";import{ESCAPED_RUNTIME_INPUT_PROMPT,extractPendingRuntimeInputRequests,promptForRuntimeInputRequests}from"#cli/dev/repl/input-requests.js";import{createDevelopmentTerminal}from"#cli/dev/repl/terminal.js";function renderConnectionRows(r){let i=[{label:`Server`,tone:`info`,value:r.serverUrl},{label:`Create`,tone:`info`,value:`POST ${ASH_CREATE_SESSION_ROUTE_PATH}`},{label:`Continue`,tone:`info`,value:`POST ${ASH_CONTINUE_SESSION_ROUTE_PATTERN}`},{label:`Stream`,tone:`info`,value:`GET ${ASH_MESSAGE_STREAM_ROUTE_PATTERN}`}];return i.push({label:`Session`,value:`Follow-up messages reuse the active continuation token.`}),i}function renderCommandRows(){return[{label:`/help`,value:`Print the connection contract and available commands.`},{label:`/new`,value:`Clear the current durable session cursor.`},{label:`/exit`,value:`Exit the REPL.`}]}function renderIntro(e,t){return[renderCliBanner(e,{subtitle:`Interactive development REPL for the active Ash server.`,title:`Ash Dev`}),``,renderCliSection(e,{rows:renderConnectionRows(t),title:`Connection`}),``,renderCliSection(e,{rows:renderCommandRows(),title:`Commands`})].join(`
|
|
2
2
|
`)}function normalizeStepMessage(e){let t=e.trim();return t.length>0?t:null}function getRenderTag(e){return e.options?.isSubagent===!0?`subagent`:e.fallback}function getRenderTone(e){return e.options?.isSubagent===!0?`subagent`:e.fallback}function prefixSourceLabel(e){return e.options?.sourceLabel===void 0?e.message:`${e.options.sourceLabel}${e.separator??` `}${e.message}`}function formatContentEvent(e,t,n){switch(t.type){case`message.appended`:return{finalized:!1,kind:`message`,line:renderCliSpeakerLine(e,{message:t.data.messageSoFar,speaker:n?.sourceLabel??`agent`,tone:getRenderTone({fallback:`accent`,options:n})})};case`message.completed`:if(t.data.message===null)return;if(t.data.finishReason===`tool-calls`){let r=normalizeStepMessage(t.data.message);return r===null?void 0:{finalized:!0,kind:`message`,line:renderCliTaggedLine(e,{message:prefixSourceLabel({message:r,options:n,separator:`: `}),tag:getRenderTag({fallback:`step`,options:n}),tone:getRenderTone({fallback:`accent`,options:n})})}}return{finalized:!0,kind:`message`,line:renderCliSpeakerLine(e,{message:t.data.message,speaker:n?.sourceLabel??`agent`,tone:getRenderTone({fallback:`accent`,options:n})})};case`reasoning.appended`:return{finalized:!1,kind:`reasoning`,line:renderCliTaggedLine(e,{message:prefixSourceLabel({message:t.data.reasoningSoFar,options:n,separator:`: `}),tag:getRenderTag({fallback:`reasoning`,options:n}),tone:getRenderTone({fallback:`info`,options:n})})};case`reasoning.completed`:return{finalized:!0,kind:`reasoning`,line:renderCliTaggedLine(e,{message:prefixSourceLabel({message:t.data.reasoning,options:n,separator:`: `}),tag:getRenderTag({fallback:`reasoning`,options:n}),tone:getRenderTone({fallback:`info`,options:n})})};default:return}}function formatEvent(e,t,n){let r=formatContentEvent(e,t,n);if(r!==void 0)return r.line;switch(t.type){case`message.received`:return;case`actions.requested`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.actions.length} action${t.data.actions.length===1?``:`s`})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`input.requested`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.requests.length} request${t.data.requests.length===1?``:`s`})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});case`action.result`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${formatActionResultLabel(t.data.result)})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`session.waiting`:case`session.completed`:return;case`authorization.required`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.name}: ${t.data.description}${formatAuthorizationChallengeSuffix(t.data.authorization)})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`warning`,options:n})});case`compaction.requested`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`compacting conversation history`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`compaction.completed`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`conversation history compacted`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`step.failed`:case`turn.failed`:case`session.failed`:return;case`subagent.called`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.name} -> ${t.data.childSessionId})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});case`subagent.started`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.subagentName})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});case`subagent.event`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.subagentName}: ${formatNestedSubagentEventLabel(t.data.event)})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`subagent.completed`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.subagentName})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});default:return}}function getEventDisplayBlockKind(e){switch(e.type){case`message.appended`:case`message.completed`:case`reasoning.appended`:case`reasoning.completed`:return`content`;default:return`meta`}}function createTurnDisplayState(){return{activeLiveContentKind:null,lastPrintedBlockKind:null}}function printDisplayLine(e){let t=e.state;return t.activeLiveContentKind!==null&&(e.terminal.commitLive(),t={activeLiveContentKind:null,lastPrintedBlockKind:`content`}),t.lastPrintedBlockKind!==null&&t.lastPrintedBlockKind!==e.kind&&e.terminal.print(``),e.terminal.print(e.line),{activeLiveContentKind:null,lastPrintedBlockKind:e.kind}}function renderTurnEvent(e){let t=formatContentEvent(e.theme,e.event,e.options);if(t!==void 0){let n=e.state;return n.activeLiveContentKind!==null&&n.activeLiveContentKind!==t.kind&&(e.terminal.commitLive(),n={activeLiveContentKind:null,lastPrintedBlockKind:`content`}),n.lastPrintedBlockKind!==null&&n.lastPrintedBlockKind!==`content`&&e.terminal.print(``),e.terminal.updateLive(t.line),t.finalized?(e.terminal.commitLive(),{activeLiveContentKind:null,lastPrintedBlockKind:`content`}):{activeLiveContentKind:t.kind,lastPrintedBlockKind:`content`}}let n=formatEvent(e.theme,e.event,e.options);return n===void 0?e.state:printDisplayLine({kind:getEventDisplayBlockKind(e.event),line:n,state:e.state,terminal:e.terminal})}function isAbortLikeError(e){return(e instanceof DOMException||e instanceof Error)&&e.name===`AbortError`}function shouldDrainSubagentStreamsOnBoundary(e){return e.length===0}var ReplSubagentStreamManager=class{#e=new Map;#t;#n;#r;#i;constructor(e){this.#t=e.displayStateRef,this.#n=e.serverUrl,this.#r=e.terminal,this.#i=e.theme}subscribe(e){if(this.#e.has(e.sessionId))return;let t=new AbortController,n=this.#a({controller:t,sessionId:e.sessionId,subagentName:e.subagentName}).finally(()=>{this.#e.delete(e.sessionId)});this.#e.set(e.sessionId,{controller:t,done:n,label:e.subagentName})}async waitForIdle(){for(;this.#e.size>0;)await Promise.all([...this.#e.values()].map(e=>e.done))}async close(){let e=[...this.#e.values()];for(let t of e)t.controller.abort();await Promise.allSettled(e.map(e=>e.done))}async#a(e){let t=resolveDevelopmentServerResourceUrl({resource:createAshMessageStreamRoutePath(e.sessionId),serverUrl:this.#n});try{for await(let n of openStreamIterable({host:this.#n,maxReconnectAttempts:3,resolveHeaders:async()=>await createDevelopmentRequestHeadersAsync({resourceUrl:t}),sessionId:e.sessionId,signal:e.controller.signal,startIndex:0}))if(this.#t.current=renderTurnEvent({event:n,options:{isSubagent:!0,sourceLabel:e.subagentName},state:this.#t.current,terminal:this.#r,theme:this.#i}),n.type===`subagent.called`&&this.subscribe({sessionId:n.data.childSessionId,subagentName:n.data.name}),isCurrentTurnBoundaryEvent(n))return}catch(t){if(isAbortLikeError(t))return;let n=toErrorMessage(t);this.#t.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(this.#i,{message:`${e.subagentName} stream failed: ${n}`,tag:`subagent`,tone:`danger`}),state:this.#t.current,terminal:this.#r})}finally{e.controller.abort()}}};function formatActionResultLabel(e){switch(e.kind){case`load-skill-result`:return e.kind;case`subagent-result`:return`${e.kind}:${e.subagentName}`;case`tool-result`:return`${e.kind}:${e.toolName}`}}function formatAuthorizationChallengeSuffix(e){if(e===void 0)return``;let t=[];return e.url!==void 0&&t.push(e.url),e.userCode!==void 0&&t.push(`code ${e.userCode}`),e.instructions!==void 0&&t.push(e.instructions),t.length===0?``:` — ${t.join(`, `)}`}function formatNestedSubagentEventLabel(e){switch(e.type){case`actions.requested`:{let t=e.data.actions,n=t.map(e=>e.kind===`tool-call`?e.toolName:e.kind).join(`,`);return`${e.type} (${t.length} action${t.length===1?``:`s`}${n.length>0?`: ${n}`:``})`}case`action.result`:return`${e.type} (${formatActionResultLabel(e.data.result)})`;case`input.requested`:return`${e.type} (${e.data.requests.length} request${e.data.requests.length===1?``:`s`})`;default:return e.type}}function formatDispatch(e,t){return t.continuationToken?renderCliTaggedLine(e,{message:`resuming session ${t.continuationToken}`,tag:`session`,tone:`info`}):renderCliTaggedLine(e,{message:`starting a new session`,tag:`session`,tone:`info`})}function formatTurnDispatch(e,t){return t.turn.inputResponses!==void 0&&t.turn.message===void 0?renderCliTaggedLine(e,{message:`responding to pending input request${t.turn.inputResponses.length===1?``:`s`}`,tag:`session`,tone:`info`}):formatDispatch(e,t.session)}function formatSessionBoundary(e,t){let n=extractCurrentTurnBoundaryEvent(t),r=extractPendingRuntimeInputRequests(t);switch(n?.type){case`session.waiting`:return[renderCliTaggedLine(e,{message:r.length>0?`waiting for input approval/answer or the next message`:`waiting for the next message`,tag:`session`,tone:`success`})];case`session.completed`:return[renderCliTaggedLine(e,{message:`session completed; the next input starts a new session`,tag:`session`,tone:`success`})];case`session.failed`:{let t=n.data.details&&typeof n.data.details.name==`string`?n.data.details.name:void 0;return[renderCliTaggedLine(e,{message:t?`session failed (${t}): ${n.data.message}`:`session failed: ${n.data.message}`,tag:`session`,tone:`danger`}),renderCliTaggedLine(e,{message:`cleared; the next input starts a new session`,tag:`session`,tone:`warning`})]}default:return[]}}async function waitForInputLine(e,t,n={}){return await new Promise(r=>{let i=e.input,cleanup=()=>{e.off(`close`,handleClose),e.off(`line`,handleLine),i.off(`keypress`,handleKeypress)},handleClose=()=>{cleanup(),r(void 0)},handleLine=e=>{cleanup(),r(e)},handleKeypress=(t,i)=>{n.allowEscape!==!0||i.name!==`escape`||(cleanup(),e.write(null,{ctrl:!0,name:`u`}),r(ESCAPED_RUNTIME_INPUT_PROMPT))};e.setPrompt(t),e.once(`close`,handleClose),e.once(`line`,handleLine),n.allowEscape&&(emitKeypressEvents(i,e),i.on(`keypress`,handleKeypress)),e.prompt()})}async function runTurn(e){let t=e.turn,n={current:createTurnDisplayState()},i=new ReplSubagentStreamManager({displayStateRef:n,serverUrl:e.serverUrl,terminal:e.terminal,theme:e.theme}),ask=async t=>{e.terminal.startPrompt(e.rl,t);let n=await waitForInputLine(e.rl,t,{allowEscape:!0});return e.terminal.stopPrompt(),n};try{for(;;){let a=e.client.getSession();n.current=printDisplayLine({kind:`meta`,line:formatTurnDispatch(e.theme,{session:a,turn:t}),state:n.current,terminal:e.terminal});let o=await e.client.send({inputResponses:t.inputResponses,message:t.message,onEvent(t){n.current=renderTurnEvent({event:t,state:n.current,terminal:e.terminal,theme:e.theme}),t.type===`subagent.called`&&i.subscribe({sessionId:t.data.childSessionId,subagentName:t.data.name})},onResponseStart(t){t.sessionId&&a.sessionId!==t.sessionId&&(n.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(e.theme,{message:t.sessionId,tag:`session`,tone:`accent`}),state:n.current,terminal:e.terminal}),n.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(e.theme,{message:resolveDevelopmentServerResourceUrl({resource:createAshMessageStreamRoutePath(t.sessionId),serverUrl:e.serverUrl}).toString(),tag:`stream`,tone:`info`}),state:n.current,terminal:e.terminal}))}}),s=extractPendingRuntimeInputRequests(o.events);if(shouldDrainSubagentStreamsOnBoundary(s)){await i.waitForIdle();for(let t of formatSessionBoundary(e.theme,o.events))n.current=printDisplayLine({kind:`meta`,line:t,state:n.current,terminal:e.terminal});return`continue`}let c=await promptForRuntimeInputRequests({ask,print(t){n.current=printDisplayLine({kind:`meta`,line:t,state:n.current,terminal:e.terminal})},requests:s,theme:e.theme});if(c.kind===`aborted`)return`exit`;if(c.kind===`deferred`)return n.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(e.theme,{message:`left pending input requests unresolved; send a new message to ignore them`,tag:`session`,tone:`warning`}),state:n.current,terminal:e.terminal}),`continue`;t={inputResponses:c.inputResponses}}}finally{await i.close()}}async function runDevelopmentRepl(e){let t=createDevelopmentTerminal(),n=createCliTheme({color:!0}),r=createInterface({input:process.stdin,output:t.output,terminal:!0});r.on(`SIGINT`,()=>{r.close()});let a=createDevClient({serverUrl:e.serverUrl});try{for(t.print(renderIntro(n,e)),t.print(``);;){t.print(``),t.startPrompt(r,`you> `);let i=await waitForInputLine(r,`you> `);if(t.stopPrompt(),i===void 0)return;let o=parseDevReplInput(i);switch(o.kind!==`empty`&&o.kind!==`exit`&&t.print(``),o.kind){case`empty`:continue;case`help`:t.print(renderIntro(n,e)),t.print(``);continue;case`exit`:return;case`new`:await a.clear(),t.print(renderCliTaggedLine(n,{message:`cleared; the next input starts a new session`,tag:`session`,tone:`warning`})),t.print(``);continue;case`message`:try{if(await runTurn({client:a,rl:r,serverUrl:e.serverUrl,terminal:t,theme:n,turn:{message:o.message}})===`exit`)return}catch(r){isVercelAuthChallenge(r)?t.printError(renderCliTaggedLine(n,{message:formatVercelAuthChallengeMessage({serverUrl:e.serverUrl}),tag:`auth`,tone:`warning`})):t.printError(renderCliTaggedLine(n,{message:toErrorMessage(r),tag:`error`,tone:`danger`}))}t.print(``)}}}finally{await a.close(),r.close(),t.dispose()}}export{createTurnDisplayState,formatContentEvent,formatEvent,renderTurnEvent,runDevelopmentRepl,shouldDrainSubagentStreamsOnBoundary};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { sliceVisible, stripAnsi, visibleLength } from "./terminal-text.js";
|
|
2
|
+
export type TUIScreenState = {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
title: string;
|
|
6
|
+
rightTitle?: string;
|
|
7
|
+
body: string;
|
|
8
|
+
input: string;
|
|
9
|
+
inputActive: boolean;
|
|
10
|
+
inputCursorVisible?: boolean;
|
|
11
|
+
scrollOffset: number;
|
|
12
|
+
status?: string;
|
|
13
|
+
};
|
|
14
|
+
export type TUIScreenLinesState = Omit<TUIScreenState, "body"> & {
|
|
15
|
+
bodyLines: string[];
|
|
16
|
+
};
|
|
17
|
+
export type TUIScreenViewportState = Omit<TUIScreenLinesState, "bodyLines" | "scrollOffset"> & {
|
|
18
|
+
visibleBodyLines: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function renderScreen(state: TUIScreenState): string;
|
|
21
|
+
export declare function renderScreenLines(state: TUIScreenLinesState): string;
|
|
22
|
+
export declare function renderScreenViewport(state: TUIScreenViewportState): string;
|
|
23
|
+
export declare function wrapText(input: string, width: number): string[];
|
|
24
|
+
export declare function clampScrollOffset(scrollOffset: number, body: string, bodyHeight: number, width: number): number;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{ansiPrefixPattern,codePointWidth,sliceVisible,stripAnsi,visibleLength}from"./terminal-text.js";import{renderMarkdown}from"./markdown.js";function renderScreen(e){let t=Math.max(20,e.width)-4,n=wrapText(renderMarkdown(e.body),t);return renderScreenLines({...e,bodyLines:n})}function renderScreenLines(e){let t=Math.max(8,e.height)-3-2,n=Math.max(0,e.bodyLines.length-t),r=Math.min(Math.max(0,e.scrollOffset),n),i=Math.max(0,e.bodyLines.length-t-r),a=e.bodyLines.slice(i,i+t);return renderScreenViewport({...e,visibleBodyLines:a})}function renderScreenViewport(e){let t=Math.max(20,e.width),n=Math.max(8,e.height)-3-2,r=e.visibleBodyLines.slice(0,n);for(;r.length<n;)r.push(``);return[topBorder(t,e.title,e.rightTitle),...r.map(e=>boxLine(e,t)),bottomBorder(t),topBorder(t,e.inputActive?`Input`:`Status`),boxLine(e.inputActive?`> ${e.input}${e.inputCursorVisible===!1?` `:`█`}`:e.status??`Streaming... ↑/↓ scroll · Ctrl+C quit`,t),bottomBorder(t)].join(`
|
|
2
|
+
`)}function wrapText(e,t){if(t<=0)return[``];let n=[];for(let r of e.split(`
|
|
3
|
+
`)){if(r.length===0){n.push(``);continue}let e=r;for(;visibleLength(e)>t;){let r=findBreakPoint(e,t);n.push(e.slice(0,r).trimEnd()),e=e.slice(r).trimStart()}n.push(e)}return n}function clampScrollOffset(e,t,n,r){let i=Math.max(1,n-2),o=wrapText(renderMarkdown(t),Math.max(1,r-4)),s=Math.max(0,o.length-i);return Math.min(Math.max(0,e),s)}function findBreakPoint(n,r){let i=0,a=0,o=-1;for(;i<n.length&&a<r;){let s=n.slice(i).match(ansiPrefixPattern);if(s){i+=s[0].length;continue}let c=n.codePointAt(i);if(c==null)break;let l=String.fromCodePoint(c),u=codePointWidth(c);if(u>0&&a+u>r)break;l===` `&&(o=i),i+=l.length,a+=u}let s=indexAfterAnsiSequences(n,i);return a===r&&n.codePointAt(s)===32?s:o>0?o:indexAtVisibleWidth(n,r)}function topBorder(e,t,r){let a=Math.max(0,e-2),o=sliceVisible(` ${t} `,a),s=r?sliceVisible(` ${r} `,Math.max(0,a-visibleLength(o))):``,c=Math.max(0,a-visibleLength(o)-visibleLength(s));return`┌${o}${`─`.repeat(c)}${s}┐`}function bottomBorder(e){return`└${`─`.repeat(e-2)}┘`}function boxLine(e,t){let r=t-4,a=sliceVisible(e,r);return`│ ${a}${` `.repeat(Math.max(0,r-visibleLength(a)))} │`}function indexAtVisibleWidth(n,r){let i=0,a=0;for(;i<n.length&&a<r;){let o=n.slice(i).match(ansiPrefixPattern);if(o){i+=o[0].length;continue}let s=n.codePointAt(i);if(s==null)break;let c=String.fromCodePoint(s),l=codePointWidth(s);if(l>0&&a+l>r)break;i+=c.length,a+=l}return i}function indexAfterAnsiSequences(t,n){let r=n;for(;r<t.length;){let n=t.slice(r).match(ansiPrefixPattern);if(!n)break;r+=n[0].length}return r}export{clampScrollOffset,renderScreen,renderScreenLines,renderScreenViewport,sliceVisible,stripAnsi,visibleLength,wrapText};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type MarkdownToken = {
|
|
2
|
+
type: "text";
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: "bold";
|
|
6
|
+
text: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "italic";
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "code";
|
|
12
|
+
text: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function renderMarkdown(input: string): string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{visibleLength}from"./terminal-text.js";const ansi={bold:`\x1B[1m`,boldOff:`\x1B[22m`,italic:`\x1B[3m`,italicOff:`\x1B[23m`};function renderMarkdown(e){let t=e.split(`
|
|
2
|
+
`),n=[];for(let e=0;e<t.length;e+=1){let r=parseTable(t,e);if(r!=null){n.push(...renderTable(r)),e=r.endIndex-1;continue}n.push(renderMarkdownLine(t[e]??``))}return n.join(`
|
|
3
|
+
`)}function renderMarkdownLine(e){if(e.startsWith(`### `))return renderInlineMarkdown(`▶ ${e.slice(4)}`);if(e.startsWith(`## `))return renderInlineMarkdown(`■ ${e.slice(3)}`);if(e.startsWith(`# `))return renderInlineMarkdown(`█ ${e.slice(2)}`);let t=e.match(/^(\s*)[-+*]\s+(.*)$/);if(t){let[,e,n=``]=t;return renderInlineMarkdown(`${e}•${n.length>0?` ${n}`:``}`)}return/^\d+\. /.test(e)?renderInlineMarkdown(e.replace(/^(\d+)\. /,`$1. `)):e.startsWith(`> `)?renderInlineMarkdown(`│ ${e.slice(2)}`):renderInlineMarkdown(e)}function renderInlineMarkdown(e){return e.replaceAll(/`([^`]+)`/g,`$1`).replaceAll(/\*\*([^*\n]+)\*\*/g,`${ansi.bold}$1${ansi.boldOff}`).replaceAll(/__([^_\n]+)__/g,`${ansi.bold}$1${ansi.boldOff}`).replaceAll(/\*([^*\n]+)\*/g,`${ansi.italic}$1${ansi.italicOff}`).replaceAll(/_([^_\n]+)_/g,`${ansi.italic}$1${ansi.italicOff}`)}function parseTable(e,t){let n=parseTableCells(e[t]??``);if(n==null||n.length<2)return;let r=parseTableCells(e[t+1]??``);if(r==null||r.length!==n.length)return;let i=parseTableAlignments(r);if(i==null)return;let a=[],o=t+2;for(;o<e.length;){let t=parseTableCells(e[o]??``);if(t==null)break;a.push(normalizeTableRow(t,n.length)),o+=1}return{alignments:i,endIndex:o,header:n,rows:a}}function parseTableCells(e){if(!e.includes(`|`))return;let t=e.trim();t.startsWith(`|`)&&(t=t.slice(1)),t.endsWith(`|`)&&!t.endsWith(`\\|`)&&(t=t.slice(0,-1));let n=[],r=``;for(let e=0;e<t.length;e+=1){let i=t[e],a=t[e+1];if(i===`\\`&&a===`|`){r+=`|`,e+=1;continue}if(i===`|`){n.push(r.trim()),r=``;continue}r+=i}return n.push(r.trim()),n}function parseTableAlignments(e){let t=[];for(let n of e){let e=n.match(/^(:)?-{3,}(:)?$/);if(e==null)return;let[,r,i]=e;t.push(r!=null&&i!=null?`center`:i==null?`left`:`right`)}return t}function normalizeTableRow(e,t){return Array.from({length:t},(t,n)=>e[n]??``)}function renderTable(n){let r=n.header.map(e=>`${ansi.bold}${renderInlineMarkdown(e)}${ansi.boldOff}`),i=n.rows.map(e=>e.map(renderInlineMarkdown)),a=[r,...i],o=n.alignments.map((t,n)=>Math.max(3,...a.map(t=>visibleLength(t[n]??``))));return[formatTableRow(r,o,n.alignments),o.map(e=>`─`.repeat(e)).join(` `),...i.map(e=>formatTableRow(e,o,n.alignments))]}function formatTableRow(e,t,n){return e.map((e,r)=>alignTableCell(e,t[r]??0,n[r]??`left`)).join(` `)}function alignTableCell(t,n,r){let i=Math.max(0,n-visibleLength(t));if(r===`right`)return`${` `.repeat(i)}${t}`;if(r===`center`){let e=Math.floor(i/2),n=i-e;return`${` `.repeat(e)}${t}${` `.repeat(n)}`}return`${t}${` `.repeat(i)}`}export{renderMarkdown};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { type ConnectionAuthorizationOutcome, Client, ClientSession } from "#client/index.js";
|
|
2
|
+
import { type UIMessage, type UIMessageChunk } from "ai";
|
|
3
|
+
import type { AssistantResponseStatsMode, TerminalPartDisplayMode, TuiDisplayOptions } from "./types.js";
|
|
4
|
+
import { type TerminalInput, type TerminalOutput } from "./terminal-renderer.js";
|
|
5
|
+
export type AgentTUIStreamResult = {
|
|
6
|
+
uiMessageStream: AsyncIterable<UIMessageChunk> | ReadableStream<UIMessageChunk>;
|
|
7
|
+
message?: UIMessage;
|
|
8
|
+
abort?: () => void;
|
|
9
|
+
};
|
|
10
|
+
export type AgentTUIStreamOptions = {
|
|
11
|
+
messages: UIMessage[];
|
|
12
|
+
};
|
|
13
|
+
export type AgentTUISessionOptions = {
|
|
14
|
+
title?: string;
|
|
15
|
+
initialPrompt?: string;
|
|
16
|
+
submittedPrompt?: string;
|
|
17
|
+
waitForExit?: boolean;
|
|
18
|
+
continueSession?: boolean;
|
|
19
|
+
tools?: TerminalPartDisplayMode;
|
|
20
|
+
reasoning?: TerminalPartDisplayMode;
|
|
21
|
+
subagents?: TerminalPartDisplayMode;
|
|
22
|
+
connectionAuth?: TerminalPartDisplayMode;
|
|
23
|
+
assistantResponseStats?: AssistantResponseStatsMode;
|
|
24
|
+
contextSize?: number;
|
|
25
|
+
};
|
|
26
|
+
export type AgentTUIToolApprovalRequest = {
|
|
27
|
+
approvalId: string;
|
|
28
|
+
toolCallId: string;
|
|
29
|
+
toolName: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
input: unknown;
|
|
32
|
+
providerExecuted?: boolean;
|
|
33
|
+
messageId: string;
|
|
34
|
+
partIndex: number;
|
|
35
|
+
};
|
|
36
|
+
export type AgentTUIToolApprovalResponse = {
|
|
37
|
+
approved: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
};
|
|
40
|
+
export type AgentTUIInputOption = {
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
style?: "primary" | "danger" | "default";
|
|
45
|
+
};
|
|
46
|
+
export type AgentTUIInputQuestion = {
|
|
47
|
+
requestId: string;
|
|
48
|
+
prompt: string;
|
|
49
|
+
display: "select" | "text";
|
|
50
|
+
options?: ReadonlyArray<AgentTUIInputOption>;
|
|
51
|
+
allowFreeform?: boolean;
|
|
52
|
+
};
|
|
53
|
+
export type AgentTUIInputQuestionResponse = {
|
|
54
|
+
optionId?: string;
|
|
55
|
+
text?: string;
|
|
56
|
+
};
|
|
57
|
+
export type AgentTUIRenderer = {
|
|
58
|
+
readPrompt?(options?: AgentTUISessionOptions): Promise<string | undefined>;
|
|
59
|
+
readToolApproval?(request: AgentTUIToolApprovalRequest, options?: AgentTUISessionOptions): Promise<AgentTUIToolApprovalResponse>;
|
|
60
|
+
readInputQuestion?(question: AgentTUIInputQuestion, options?: AgentTUISessionOptions): Promise<AgentTUIInputQuestionResponse | undefined>;
|
|
61
|
+
renderStream(result: AgentTUIStreamResult, options?: AgentTUISessionOptions): Promise<UIMessage | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Out-of-band update for one child step (reasoning + message text) of a
|
|
64
|
+
* subagent dispatch. Called by the runner as child-session stream events
|
|
65
|
+
* arrive. The renderer renders this as a body section colored by the
|
|
66
|
+
* subagent palette.
|
|
67
|
+
*/
|
|
68
|
+
upsertSubagentStep?(update: SubagentStepUpdate): void;
|
|
69
|
+
/**
|
|
70
|
+
* Out-of-band update for one child tool call of a subagent dispatch.
|
|
71
|
+
*/
|
|
72
|
+
upsertSubagentTool?(update: SubagentToolUpdate): void;
|
|
73
|
+
/**
|
|
74
|
+
* Registers a tool call id as originating from a subagent's child
|
|
75
|
+
* session. The renderer must skip rendering parent UIMessage tool parts
|
|
76
|
+
* for these ids — they are surfaced via {@link upsertSubagentTool}
|
|
77
|
+
* instead.
|
|
78
|
+
*/
|
|
79
|
+
markChildToolCallId?(callId: string): void;
|
|
80
|
+
/**
|
|
81
|
+
* Out-of-band update for one MCP connection authorization lifecycle.
|
|
82
|
+
* Called by the runner as `authorization.*` events arrive.
|
|
83
|
+
* The renderer renders this as a persistent body section per
|
|
84
|
+
* connection that transitions through `required` → `pending` →
|
|
85
|
+
* one of the terminal `ConnectionAuthorizationOutcome` states.
|
|
86
|
+
*/
|
|
87
|
+
upsertConnectionAuth?(update: ConnectionAuthUpdate): void;
|
|
88
|
+
/**
|
|
89
|
+
* Sets the number of connections currently awaiting an OAuth
|
|
90
|
+
* callback. The renderer overrides its bottom status bar with a
|
|
91
|
+
* "waiting for connection authorization" hint while this is > 0,
|
|
92
|
+
* so the user understands the agent is parked, not hung.
|
|
93
|
+
*/
|
|
94
|
+
setConnectionAuthPendingCount?(count: number): void;
|
|
95
|
+
/**
|
|
96
|
+
* Clears the rendered transcript and resets per-conversation display
|
|
97
|
+
* state, leaving the UI interactive on a fresh screen. Used by the
|
|
98
|
+
* `/new` command to start a new session with a clean slate.
|
|
99
|
+
*/
|
|
100
|
+
reset?(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Tears down interactive mode and restores the terminal, as if the user
|
|
103
|
+
* pressed Ctrl+C. Used by the `/exit` command.
|
|
104
|
+
*/
|
|
105
|
+
shutdown?(): void;
|
|
106
|
+
};
|
|
107
|
+
export type AshTUIRunnerOptions = TuiDisplayOptions & {
|
|
108
|
+
session: ClientSession;
|
|
109
|
+
/**
|
|
110
|
+
* Optional client used to attach to child sessions for live subagent
|
|
111
|
+
* stream observation. When omitted, the TUI still shows the subagent
|
|
112
|
+
* section but cannot surface the subagent's reasoning / response /
|
|
113
|
+
* intermediate events — only the parent-stream `called` and
|
|
114
|
+
* `completed` transitions.
|
|
115
|
+
*/
|
|
116
|
+
client?: Client;
|
|
117
|
+
renderer?: AgentTUIRenderer;
|
|
118
|
+
screen?: TerminalOutput;
|
|
119
|
+
userInput?: TerminalInput;
|
|
120
|
+
/**
|
|
121
|
+
* Formats an error thrown while dispatching a turn (the initial
|
|
122
|
+
* `session.send()` POST — e.g. a transport failure or a Vercel
|
|
123
|
+
* Deployment Protection challenge) into the text rendered in the
|
|
124
|
+
* inline error region. Defaults to the error's message. Callers that
|
|
125
|
+
* know about transport-specific challenges (the `ash dev` glue) inject
|
|
126
|
+
* a richer formatter here.
|
|
127
|
+
*/
|
|
128
|
+
formatTransportError?: (error: unknown) => string;
|
|
129
|
+
};
|
|
130
|
+
export declare class AshTUIRunner {
|
|
131
|
+
#private;
|
|
132
|
+
constructor(options: AshTUIRunnerOptions);
|
|
133
|
+
run(): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
type ResponseMetadata = {
|
|
136
|
+
usage?: {
|
|
137
|
+
totalTokens?: number;
|
|
138
|
+
outputTokens?: number;
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
type SubagentChildStep = {
|
|
142
|
+
reasoning: string;
|
|
143
|
+
message: string;
|
|
144
|
+
finalized: boolean;
|
|
145
|
+
};
|
|
146
|
+
type SubagentToolState = {
|
|
147
|
+
toolName: string;
|
|
148
|
+
input: unknown;
|
|
149
|
+
status: "approval-requested" | "executing" | "done" | "failed";
|
|
150
|
+
output?: unknown;
|
|
151
|
+
errorText?: string;
|
|
152
|
+
};
|
|
153
|
+
export type SubagentRun = {
|
|
154
|
+
name: string;
|
|
155
|
+
/**
|
|
156
|
+
* One entry per logical "child message" — independent of the child's
|
|
157
|
+
* `stepIndex` field, which the harness can reuse across multiple
|
|
158
|
+
* assistant messages within a turn (e.g. a message before a tool call
|
|
159
|
+
* and another message after the tool result both arrive under
|
|
160
|
+
* `stepIndex: 0`). The key is a monotonic counter so each
|
|
161
|
+
* `message.completed` opens a new box on the next inbound delta.
|
|
162
|
+
*/
|
|
163
|
+
steps: Map<number, SubagentChildStep>;
|
|
164
|
+
/**
|
|
165
|
+
* Section currently accepting reasoning/message deltas. `null` means
|
|
166
|
+
* the next delta opens a new section.
|
|
167
|
+
*/
|
|
168
|
+
currentSectionKey: number | null;
|
|
169
|
+
/** Monotonic counter for new section keys. */
|
|
170
|
+
nextSectionKey: number;
|
|
171
|
+
tools: Map<string, SubagentToolState>;
|
|
172
|
+
};
|
|
173
|
+
export type SubagentStepUpdate = {
|
|
174
|
+
callId: string;
|
|
175
|
+
subagentName: string;
|
|
176
|
+
sectionKey: number;
|
|
177
|
+
reasoning: string;
|
|
178
|
+
message: string;
|
|
179
|
+
finalized: boolean;
|
|
180
|
+
};
|
|
181
|
+
export type SubagentToolUpdate = {
|
|
182
|
+
callId: string;
|
|
183
|
+
subagentName: string;
|
|
184
|
+
childCallId: string;
|
|
185
|
+
toolName: string;
|
|
186
|
+
input: unknown;
|
|
187
|
+
status: "approval-requested" | "executing" | "done" | "failed";
|
|
188
|
+
output?: unknown;
|
|
189
|
+
errorText?: string;
|
|
190
|
+
};
|
|
191
|
+
export type ConnectionAuthChallenge = {
|
|
192
|
+
url?: string;
|
|
193
|
+
userCode?: string;
|
|
194
|
+
expiresAt?: string;
|
|
195
|
+
instructions?: string;
|
|
196
|
+
};
|
|
197
|
+
export type ConnectionAuthState = "required" | "pending" | ConnectionAuthorizationOutcome;
|
|
198
|
+
export type ConnectionAuthUpdate = {
|
|
199
|
+
name: string;
|
|
200
|
+
description: string;
|
|
201
|
+
state: ConnectionAuthState;
|
|
202
|
+
challenge?: ConnectionAuthChallenge;
|
|
203
|
+
reason?: string;
|
|
204
|
+
};
|
|
205
|
+
export type { ResponseMetadata as _ResponseMetadata };
|