experimental-ash 0.54.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 +19 -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 +8 -4
- 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/docs/public/sandbox.md +12 -6
- 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/compiler/channel-instrumentation-types.js +1 -1
- package/dist/src/compiler/manifest.d.ts +3 -0
- package/dist/src/compiler/manifest.js +1 -1
- package/dist/src/compiler/workspace-resources.js +1 -1
- 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/execution/sandbox/bindings/local.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.js +1 -1
- package/dist/src/execution/sandbox/ensure.js +1 -1
- package/dist/src/execution/sandbox/lazy-backend.js +1 -1
- package/dist/src/execution/sandbox/prewarm.d.ts +2 -2
- package/dist/src/execution/sandbox/prewarm.js +1 -1
- package/dist/src/harness/code-mode.js +1 -1
- package/dist/src/harness/tool-loop.js +1 -1
- package/dist/src/harness/workflow-stream-error.d.ts +29 -0
- package/dist/src/harness/workflow-stream-error.js +1 -0
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/instrumentation.d.ts +1 -1
- package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
- package/dist/src/public/channels/ash.js +1 -1
- package/dist/src/public/channels/discord/discordChannel.d.ts +2 -1
- package/dist/src/public/channels/discord/discordChannel.js +1 -1
- package/dist/src/public/channels/discord/index.d.ts +4 -0
- package/dist/src/public/channels/index.d.ts +63 -1
- package/dist/src/public/channels/index.js +1 -1
- package/dist/src/public/channels/teams/index.d.ts +5 -0
- package/dist/src/public/channels/teams/teamsChannel.d.ts +2 -1
- package/dist/src/public/channels/teams/teamsChannel.js +1 -1
- package/dist/src/public/channels/telegram/index.d.ts +5 -0
- package/dist/src/public/channels/telegram/telegramChannel.d.ts +2 -1
- package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
- package/dist/src/public/definitions/sandbox-backend.d.ts +1 -1
- package/dist/src/public/instrumentation/index.d.ts +2 -61
- package/dist/src/public/instrumentation/index.js +1 -1
- package/dist/src/runtime/sandbox/keys.d.ts +7 -3
- package/dist/src/runtime/sandbox/keys.js +1 -1
- package/dist/src/runtime/sandbox/template-plan.d.ts +21 -0
- package/dist/src/runtime/sandbox/template-plan.js +1 -0
- package/dist/src/shared/sandbox-backend.d.ts +25 -2
- 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,24 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.55.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- dc17470: Ash now skips reusable sandbox template snapshots for empty sandboxes and keys seed-only sandbox templates by the contents of skills and workspace seed files. Unchanged seed files can reuse a Vercel Sandbox template across deploys, while sandboxes with `bootstrap()` stay deployment-scoped. The build log now labels each template as either `reused cached` or `built` (with a per-build reused/built summary) so a cache hit is distinguishable from a fresh rebuild. `SandboxBackend.prewarm` returns `{ reused }` to support this; custom backends must return that result.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 276767f: Log a structured warning when a terminal model step completes without visible assistant text, so silent parked turns are easier to diagnose.
|
|
19
|
+
- 4366463: Ship declaration-merged `ChannelMetadataMap` and `ChannelReferenceMap` entries for the built-in Slack channel so `isChannel(ctx.channel, slackChannel)` narrows `metadata` to `SlackInstrumentationMetadata` without requiring an `ash build` step. Also widen `isChannel` input type to accept `DynamicResolveContext.channel` shape (optional `kind`).
|
|
20
|
+
- dc591d0: Label durable event-stream write failures as `WORKFLOW_STREAM_WRITE_FAILED` instead of `MODEL_CALL_FAILED`. When a `getWritable()` flush to the workflow server fails (e.g. an `HTTP 504 FUNCTION_INVOCATION_TIMEOUT` on the stream-write endpoint), the harness now logs "workflow stream write failed" and emits a distinct failure code, since the model call itself may have succeeded — the failure is in the durable stream transport, not the model provider. The `step.failed`/`turn.failed` `details` now also carry the parsed `statusCode`, `url`, `vercelId`, and `vercelError` from the transport error for easier diagnosis.
|
|
21
|
+
|
|
3
22
|
## 0.54.0
|
|
4
23
|
|
|
5
24
|
### 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
|
|
|
@@ -60,7 +60,11 @@ Ash can prewarm reusable Vercel templates during hosted builds.
|
|
|
60
60
|
Important behavior:
|
|
61
61
|
|
|
62
62
|
- sandbox template prewarm runs when `VERCEL` and `VERCEL_DEPLOYMENT_ID` are both present
|
|
63
|
-
-
|
|
63
|
+
- Ash skips prewarm for sandboxes with no `bootstrap()` and no workspace seed files
|
|
64
|
+
- seed-only templates are keyed by skills and workspace file contents, so unchanged seeds can reuse
|
|
65
|
+
a template across deploys
|
|
66
|
+
- sandboxes with `bootstrap()` remain deployment-scoped
|
|
67
|
+
- the build log labels each template `reused cached` or `built`, so a reuse across deploys is visible
|
|
64
68
|
- `onSession()` still runs later at runtime
|
|
65
69
|
- if build-time prewarm fails, the build fails
|
|
66
70
|
|
|
@@ -84,9 +88,9 @@ Then attach to the returned stream URL pattern:
|
|
|
84
88
|
curl https://<your-app>/ash/v1/session/<sessionId>/stream
|
|
85
89
|
```
|
|
86
90
|
|
|
87
|
-
## Remote
|
|
91
|
+
## Remote Dev
|
|
88
92
|
|
|
89
|
-
You can point the Ash
|
|
93
|
+
You can point the Ash dev [TUI](./dev-tui.md) at a deployed app:
|
|
90
94
|
|
|
91
95
|
```bash
|
|
92
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
|
|
|
@@ -429,9 +429,9 @@ This means:
|
|
|
429
429
|
Files created during earlier turns, installed dependencies, and workspace state are all preserved.
|
|
430
430
|
- The resume is transparent to your code — no configuration is required.
|
|
431
431
|
- If a sandbox has been deleted (by cleanup policies or manual deletion), Ash creates a fresh
|
|
432
|
-
session
|
|
433
|
-
|
|
434
|
-
is lost.
|
|
432
|
+
session. Sandboxes with `bootstrap()` or workspace seed files start from the prewarmed template
|
|
433
|
+
snapshot; empty sandboxes start from the backend's fresh base runtime. Any session-specific state
|
|
434
|
+
from prior turns is lost.
|
|
435
435
|
|
|
436
436
|
See the [Vercel Sandbox documentation](https://vercel.com/docs/sandbox) for details on persistence
|
|
437
437
|
behavior and plan-specific retention limits.
|
|
@@ -461,21 +461,27 @@ tags.
|
|
|
461
461
|
|
|
462
462
|
A `SandboxBackend` is just an object with a `name` and a `create` function (and optionally
|
|
463
463
|
`prewarm`). You can write your own and pass it to `defineSandbox({ backend })`. See
|
|
464
|
-
`experimental-ash/sandbox` for the `SandboxBackend`, `SandboxBackendCreateInput`,
|
|
465
|
-
`SandboxBackendPrewarmInput` interface types.
|
|
464
|
+
`experimental-ash/sandbox` for the `SandboxBackend`, `SandboxBackendCreateInput`,
|
|
465
|
+
`SandboxBackendPrewarmInput`, and `SandboxBackendPrewarmResult` interface types. A backend's
|
|
466
|
+
`prewarm` returns `{ reused }` so the build pipeline can report whether each template was reused or
|
|
467
|
+
built.
|
|
466
468
|
|
|
467
469
|
## Vercel Behavior
|
|
468
470
|
|
|
469
471
|
On hosted Vercel builds, Ash can prewarm the authored sandbox template during `ash build` when both
|
|
470
472
|
`VERCEL` and `VERCEL_DEPLOYMENT_ID` are present.
|
|
471
473
|
|
|
472
|
-
|
|
474
|
+
Ash skips template prewarm for sandboxes that have no `bootstrap()` and no workspace seed files.
|
|
475
|
+
Seed-only sandboxes use a content-addressed template key, so matching skills and workspace files can
|
|
476
|
+
reuse an existing template across deploys. Sandboxes with `bootstrap()` remain deployment-scoped.
|
|
473
477
|
|
|
474
478
|
Important behavior:
|
|
475
479
|
|
|
476
480
|
- `onSession()` still runs later at runtime
|
|
477
481
|
- if build-time sandbox prewarm fails, the build fails
|
|
478
482
|
- runtime reuses the prebuilt template when available
|
|
483
|
+
- the build log reports each template as either `reused cached` or `built`, so a cache hit is
|
|
484
|
+
distinguishable from a fresh rebuild
|
|
479
485
|
|
|
480
486
|
## What To Read Next
|
|
481
487
|
|
|
@@ -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};
|