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.
Files changed (80) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/docs/public/advanced/cli-build-and-debugging.md +1 -1
  3. package/dist/docs/public/advanced/dev-tui.md +202 -0
  4. package/dist/docs/public/advanced/meta.json +1 -0
  5. package/dist/docs/public/advanced/vercel-deployment.md +8 -4
  6. package/dist/docs/public/getting-started.mdx +1 -1
  7. package/dist/docs/public/human-in-the-loop.mdx +4 -4
  8. package/dist/docs/public/onboarding.md +2 -2
  9. package/dist/docs/public/sandbox.md +12 -6
  10. package/dist/src/cli/dev/{repl.d.ts → repl/repl.d.ts} +1 -1
  11. package/dist/src/cli/dev/{repl.js → repl/repl.js} +1 -1
  12. package/dist/src/cli/dev/tui/layout.d.ts +24 -0
  13. package/dist/src/cli/dev/tui/layout.js +3 -0
  14. package/dist/src/cli/dev/tui/markdown.d.ts +14 -0
  15. package/dist/src/cli/dev/tui/markdown.js +3 -0
  16. package/dist/src/cli/dev/tui/runner.d.ts +205 -0
  17. package/dist/src/cli/dev/tui/runner.js +1 -0
  18. package/dist/src/cli/dev/tui/terminal-frame-buffer.d.ts +21 -0
  19. package/dist/src/cli/dev/tui/terminal-frame-buffer.js +2 -0
  20. package/dist/src/cli/dev/tui/terminal-renderer.d.ts +111 -0
  21. package/dist/src/cli/dev/tui/terminal-renderer.js +12 -0
  22. package/dist/src/cli/dev/tui/terminal-text.d.ts +6 -0
  23. package/dist/src/cli/dev/tui/terminal-text.js +1 -0
  24. package/dist/src/cli/dev/tui/test/index.d.ts +10 -0
  25. package/dist/src/cli/dev/tui/test/index.js +1 -0
  26. package/dist/src/cli/dev/tui/test/mock-terminal.d.ts +28 -0
  27. package/dist/src/cli/dev/tui/test/mock-terminal.js +3 -0
  28. package/dist/src/cli/dev/tui/tui.d.ts +32 -0
  29. package/dist/src/cli/dev/tui/tui.js +1 -0
  30. package/dist/src/cli/dev/tui/types.d.ts +68 -0
  31. package/dist/src/cli/dev/tui/types.js +1 -0
  32. package/dist/src/cli/run.d.ts +66 -0
  33. package/dist/src/cli/run.js +2 -2
  34. package/dist/src/compiler/channel-instrumentation-types.js +1 -1
  35. package/dist/src/compiler/manifest.d.ts +3 -0
  36. package/dist/src/compiler/manifest.js +1 -1
  37. package/dist/src/compiler/workspace-resources.js +1 -1
  38. package/dist/src/context/dynamic-tool-lifecycle.js +1 -1
  39. package/dist/src/evals/scorers/autoevals.js +1 -1
  40. package/dist/src/execution/node-step.js +1 -1
  41. package/dist/src/execution/sandbox/bindings/local.js +1 -1
  42. package/dist/src/execution/sandbox/bindings/vercel.js +1 -1
  43. package/dist/src/execution/sandbox/ensure.js +1 -1
  44. package/dist/src/execution/sandbox/lazy-backend.js +1 -1
  45. package/dist/src/execution/sandbox/prewarm.d.ts +2 -2
  46. package/dist/src/execution/sandbox/prewarm.js +1 -1
  47. package/dist/src/harness/code-mode.js +1 -1
  48. package/dist/src/harness/tool-loop.js +1 -1
  49. package/dist/src/harness/workflow-stream-error.d.ts +29 -0
  50. package/dist/src/harness/workflow-stream-error.js +1 -0
  51. package/dist/src/internal/application/package.js +1 -1
  52. package/dist/src/internal/instrumentation.d.ts +1 -1
  53. package/dist/src/packages/ash-scaffold/src/channels.js +1 -1
  54. package/dist/src/public/channels/ash.js +1 -1
  55. package/dist/src/public/channels/discord/discordChannel.d.ts +2 -1
  56. package/dist/src/public/channels/discord/discordChannel.js +1 -1
  57. package/dist/src/public/channels/discord/index.d.ts +4 -0
  58. package/dist/src/public/channels/index.d.ts +63 -1
  59. package/dist/src/public/channels/index.js +1 -1
  60. package/dist/src/public/channels/teams/index.d.ts +5 -0
  61. package/dist/src/public/channels/teams/teamsChannel.d.ts +2 -1
  62. package/dist/src/public/channels/teams/teamsChannel.js +1 -1
  63. package/dist/src/public/channels/telegram/index.d.ts +5 -0
  64. package/dist/src/public/channels/telegram/telegramChannel.d.ts +2 -1
  65. package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
  66. package/dist/src/public/definitions/sandbox-backend.d.ts +1 -1
  67. package/dist/src/public/instrumentation/index.d.ts +2 -61
  68. package/dist/src/public/instrumentation/index.js +1 -1
  69. package/dist/src/runtime/sandbox/keys.d.ts +7 -3
  70. package/dist/src/runtime/sandbox/keys.js +1 -1
  71. package/dist/src/runtime/sandbox/template-plan.d.ts +21 -0
  72. package/dist/src/runtime/sandbox/template-plan.js +1 -0
  73. package/dist/src/shared/sandbox-backend.d.ts +25 -2
  74. package/package.json +6 -1
  75. /package/dist/src/cli/dev/{input-requests.d.ts → repl/input-requests.d.ts} +0 -0
  76. /package/dist/src/cli/dev/{input-requests.js → repl/input-requests.js} +0 -0
  77. /package/dist/src/cli/dev/{input.d.ts → repl/input.d.ts} +0 -0
  78. /package/dist/src/cli/dev/{input.js → repl/input.js} +0 -0
  79. /package/dist/src/cli/dev/{terminal.d.ts → repl/terminal.d.ts} +0 -0
  80. /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 REPL
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)
@@ -11,6 +11,7 @@
11
11
  "workspace",
12
12
  "evals",
13
13
  "instrumentation",
14
+ "dev-tui",
14
15
  "cli-build-and-debugging",
15
16
  "typescript-api",
16
17
  "...",
@@ -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 REPL against the deployed URL.
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
- - sandbox prewarm only bakes reusable `bootstrap()` state into the template
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 REPL
91
+ ## Remote Dev
88
92
 
89
- You can point the Ash REPL at a deployed app:
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 REPL
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 REPL:
247
+ `ash dev` surfaces pending HITL requests directly in the [terminal UI](./advanced/dev-tui.md):
248
248
 
249
- - tool approvals prompt for yes/no
250
- - questions prompt for an option number or freeform answer
251
- - `Escape` returns to the normal `you>` prompt without answering
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 REPL is always available via `ash dev`, regardless of channel choice.
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
- - REPL: `ash dev`, chat in the terminal.
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 from the prewarmed template snapshot. The user gets a clean sandbox seeded with framework
433
- defaults, bootstrap output, and workspace files but any session-specific state from prior turns
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`, and
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
- That prewarm only runs `bootstrap()` for reusable template state.
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};