@agent-native/core 0.84.35 → 0.84.36

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 (187) hide show
  1. package/README.md +15 -51
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/actions.mdx +17 -24
  4. package/corpus/core/docs/content/agent-surfaces.mdx +59 -56
  5. package/corpus/core/docs/content/cloneable-saas.mdx +3 -3
  6. package/corpus/core/docs/content/faq.mdx +4 -4
  7. package/corpus/core/docs/content/getting-started.mdx +246 -204
  8. package/corpus/core/docs/content/key-concepts.mdx +9 -9
  9. package/corpus/core/docs/content/locales/ar-SA/cloneable-saas.mdx +3 -3
  10. package/corpus/core/docs/content/locales/ar-SA/faq.mdx +4 -4
  11. package/corpus/core/docs/content/locales/ar-SA/getting-started.mdx +148 -214
  12. package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -9
  13. package/corpus/core/docs/content/locales/ar-SA/template-chat.mdx +2 -2
  14. package/corpus/core/docs/content/locales/ar-SA/using-your-agent.mdx +2 -2
  15. package/corpus/core/docs/content/locales/de-DE/cloneable-saas.mdx +3 -3
  16. package/corpus/core/docs/content/locales/de-DE/faq.mdx +4 -4
  17. package/corpus/core/docs/content/locales/de-DE/getting-started.mdx +145 -211
  18. package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -9
  19. package/corpus/core/docs/content/locales/de-DE/template-chat.mdx +2 -2
  20. package/corpus/core/docs/content/locales/de-DE/using-your-agent.mdx +2 -2
  21. package/corpus/core/docs/content/locales/es-ES/cloneable-saas.mdx +3 -3
  22. package/corpus/core/docs/content/locales/es-ES/faq.mdx +4 -4
  23. package/corpus/core/docs/content/locales/es-ES/getting-started.mdx +148 -214
  24. package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -9
  25. package/corpus/core/docs/content/locales/es-ES/template-chat.mdx +2 -2
  26. package/corpus/core/docs/content/locales/es-ES/using-your-agent.mdx +2 -2
  27. package/corpus/core/docs/content/locales/fr-FR/cloneable-saas.mdx +2 -2
  28. package/corpus/core/docs/content/locales/fr-FR/faq.mdx +4 -4
  29. package/corpus/core/docs/content/locales/fr-FR/getting-started.mdx +146 -212
  30. package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -9
  31. package/corpus/core/docs/content/locales/fr-FR/template-chat.mdx +2 -2
  32. package/corpus/core/docs/content/locales/fr-FR/using-your-agent.mdx +2 -2
  33. package/corpus/core/docs/content/locales/hi-IN/cloneable-saas.mdx +3 -3
  34. package/corpus/core/docs/content/locales/hi-IN/faq.mdx +4 -4
  35. package/corpus/core/docs/content/locales/hi-IN/getting-started.mdx +149 -215
  36. package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +8 -13
  37. package/corpus/core/docs/content/locales/hi-IN/template-chat.mdx +2 -2
  38. package/corpus/core/docs/content/locales/hi-IN/using-your-agent.mdx +2 -2
  39. package/corpus/core/docs/content/locales/ja-JP/cloneable-saas.mdx +2 -2
  40. package/corpus/core/docs/content/locales/ja-JP/faq.mdx +4 -4
  41. package/corpus/core/docs/content/locales/ja-JP/getting-started.mdx +144 -210
  42. package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +8 -13
  43. package/corpus/core/docs/content/locales/ja-JP/template-chat.mdx +2 -2
  44. package/corpus/core/docs/content/locales/ja-JP/using-your-agent.mdx +2 -2
  45. package/corpus/core/docs/content/locales/ko-KR/cloneable-saas.mdx +3 -3
  46. package/corpus/core/docs/content/locales/ko-KR/faq.mdx +4 -4
  47. package/corpus/core/docs/content/locales/ko-KR/getting-started.mdx +146 -212
  48. package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +8 -13
  49. package/corpus/core/docs/content/locales/ko-KR/template-chat.mdx +2 -2
  50. package/corpus/core/docs/content/locales/ko-KR/using-your-agent.mdx +2 -2
  51. package/corpus/core/docs/content/locales/pt-BR/cloneable-saas.mdx +3 -3
  52. package/corpus/core/docs/content/locales/pt-BR/faq.mdx +4 -4
  53. package/corpus/core/docs/content/locales/pt-BR/getting-started.mdx +145 -211
  54. package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -9
  55. package/corpus/core/docs/content/locales/pt-BR/template-chat.mdx +2 -2
  56. package/corpus/core/docs/content/locales/pt-BR/using-your-agent.mdx +2 -2
  57. package/corpus/core/docs/content/locales/zh-CN/cloneable-saas.mdx +3 -3
  58. package/corpus/core/docs/content/locales/zh-CN/faq.mdx +4 -4
  59. package/corpus/core/docs/content/locales/zh-CN/getting-started.mdx +158 -209
  60. package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -9
  61. package/corpus/core/docs/content/locales/zh-CN/template-chat.mdx +2 -2
  62. package/corpus/core/docs/content/locales/zh-CN/using-your-agent.mdx +2 -2
  63. package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.mdx +3 -3
  64. package/corpus/core/docs/content/locales/zh-TW/faq.mdx +4 -4
  65. package/corpus/core/docs/content/locales/zh-TW/getting-started.mdx +158 -212
  66. package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -9
  67. package/corpus/core/docs/content/locales/zh-TW/template-chat.mdx +2 -2
  68. package/corpus/core/docs/content/locales/zh-TW/using-your-agent.mdx +2 -2
  69. package/corpus/core/docs/content/native-chat-ui.mdx +17 -32
  70. package/corpus/core/docs/content/pure-agent-apps.mdx +46 -26
  71. package/corpus/core/docs/content/template-chat.mdx +8 -8
  72. package/corpus/core/docs/content/using-your-agent.mdx +2 -2
  73. package/corpus/core/docs/content/what-is-agent-native.mdx +6 -6
  74. package/corpus/core/package.json +1 -1
  75. package/corpus/core/src/cli/skills.ts +7 -9
  76. package/corpus/core/src/client/MultiTabAssistantChat.tsx +2 -0
  77. package/corpus/core/src/client/RunStuckBanner.tsx +172 -18
  78. package/corpus/core/src/client/blocks/library/wireframe-kit.tsx +57 -9
  79. package/corpus/core/src/client/blocks/library/wireframe.tsx +3 -18
  80. package/corpus/core/src/client/chat/widgets/builtin-tool-renderers.tsx +28 -32
  81. package/corpus/core/src/db/schema.ts +5 -0
  82. package/corpus/core/src/styles/blocks.css +27 -6
  83. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -4
  84. package/corpus/templates/content/actions/list-trashed-content-databases.ts +1 -1
  85. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -9
  86. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -9
  87. package/dist/cli/skills.d.ts +1 -1
  88. package/dist/cli/skills.d.ts.map +1 -1
  89. package/dist/cli/skills.js +7 -9
  90. package/dist/cli/skills.js.map +1 -1
  91. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  92. package/dist/client/MultiTabAssistantChat.js +1 -1
  93. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  94. package/dist/client/RunStuckBanner.d.ts +13 -1
  95. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  96. package/dist/client/RunStuckBanner.js +123 -16
  97. package/dist/client/RunStuckBanner.js.map +1 -1
  98. package/dist/client/blocks/library/wireframe-kit.d.ts +6 -6
  99. package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -1
  100. package/dist/client/blocks/library/wireframe-kit.js +52 -5
  101. package/dist/client/blocks/library/wireframe-kit.js.map +1 -1
  102. package/dist/client/blocks/library/wireframe.d.ts.map +1 -1
  103. package/dist/client/blocks/library/wireframe.js +1 -11
  104. package/dist/client/blocks/library/wireframe.js.map +1 -1
  105. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
  106. package/dist/client/chat/widgets/builtin-tool-renderers.js +27 -31
  107. package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -1
  108. package/dist/collab/routes.d.ts +2 -2
  109. package/dist/db/schema.d.ts +2 -1
  110. package/dist/db/schema.d.ts.map +1 -1
  111. package/dist/db/schema.js +3 -2
  112. package/dist/db/schema.js.map +1 -1
  113. package/dist/resources/handlers.d.ts +3 -3
  114. package/dist/server/transcribe-voice.d.ts +1 -1
  115. package/dist/styles/blocks.css +27 -6
  116. package/docs/content/actions.mdx +17 -24
  117. package/docs/content/agent-surfaces.mdx +59 -56
  118. package/docs/content/cloneable-saas.mdx +3 -3
  119. package/docs/content/faq.mdx +4 -4
  120. package/docs/content/getting-started.mdx +246 -204
  121. package/docs/content/key-concepts.mdx +9 -9
  122. package/docs/content/locales/ar-SA/cloneable-saas.mdx +3 -3
  123. package/docs/content/locales/ar-SA/faq.mdx +4 -4
  124. package/docs/content/locales/ar-SA/getting-started.mdx +148 -214
  125. package/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -9
  126. package/docs/content/locales/ar-SA/template-chat.mdx +2 -2
  127. package/docs/content/locales/ar-SA/using-your-agent.mdx +2 -2
  128. package/docs/content/locales/de-DE/cloneable-saas.mdx +3 -3
  129. package/docs/content/locales/de-DE/faq.mdx +4 -4
  130. package/docs/content/locales/de-DE/getting-started.mdx +145 -211
  131. package/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -9
  132. package/docs/content/locales/de-DE/template-chat.mdx +2 -2
  133. package/docs/content/locales/de-DE/using-your-agent.mdx +2 -2
  134. package/docs/content/locales/es-ES/cloneable-saas.mdx +3 -3
  135. package/docs/content/locales/es-ES/faq.mdx +4 -4
  136. package/docs/content/locales/es-ES/getting-started.mdx +148 -214
  137. package/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -9
  138. package/docs/content/locales/es-ES/template-chat.mdx +2 -2
  139. package/docs/content/locales/es-ES/using-your-agent.mdx +2 -2
  140. package/docs/content/locales/fr-FR/cloneable-saas.mdx +2 -2
  141. package/docs/content/locales/fr-FR/faq.mdx +4 -4
  142. package/docs/content/locales/fr-FR/getting-started.mdx +146 -212
  143. package/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -9
  144. package/docs/content/locales/fr-FR/template-chat.mdx +2 -2
  145. package/docs/content/locales/fr-FR/using-your-agent.mdx +2 -2
  146. package/docs/content/locales/hi-IN/cloneable-saas.mdx +3 -3
  147. package/docs/content/locales/hi-IN/faq.mdx +4 -4
  148. package/docs/content/locales/hi-IN/getting-started.mdx +149 -215
  149. package/docs/content/locales/hi-IN/pure-agent-apps.mdx +8 -13
  150. package/docs/content/locales/hi-IN/template-chat.mdx +2 -2
  151. package/docs/content/locales/hi-IN/using-your-agent.mdx +2 -2
  152. package/docs/content/locales/ja-JP/cloneable-saas.mdx +2 -2
  153. package/docs/content/locales/ja-JP/faq.mdx +4 -4
  154. package/docs/content/locales/ja-JP/getting-started.mdx +144 -210
  155. package/docs/content/locales/ja-JP/pure-agent-apps.mdx +8 -13
  156. package/docs/content/locales/ja-JP/template-chat.mdx +2 -2
  157. package/docs/content/locales/ja-JP/using-your-agent.mdx +2 -2
  158. package/docs/content/locales/ko-KR/cloneable-saas.mdx +3 -3
  159. package/docs/content/locales/ko-KR/faq.mdx +4 -4
  160. package/docs/content/locales/ko-KR/getting-started.mdx +146 -212
  161. package/docs/content/locales/ko-KR/pure-agent-apps.mdx +8 -13
  162. package/docs/content/locales/ko-KR/template-chat.mdx +2 -2
  163. package/docs/content/locales/ko-KR/using-your-agent.mdx +2 -2
  164. package/docs/content/locales/pt-BR/cloneable-saas.mdx +3 -3
  165. package/docs/content/locales/pt-BR/faq.mdx +4 -4
  166. package/docs/content/locales/pt-BR/getting-started.mdx +145 -211
  167. package/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -9
  168. package/docs/content/locales/pt-BR/template-chat.mdx +2 -2
  169. package/docs/content/locales/pt-BR/using-your-agent.mdx +2 -2
  170. package/docs/content/locales/zh-CN/cloneable-saas.mdx +3 -3
  171. package/docs/content/locales/zh-CN/faq.mdx +4 -4
  172. package/docs/content/locales/zh-CN/getting-started.mdx +158 -209
  173. package/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -9
  174. package/docs/content/locales/zh-CN/template-chat.mdx +2 -2
  175. package/docs/content/locales/zh-CN/using-your-agent.mdx +2 -2
  176. package/docs/content/locales/zh-TW/cloneable-saas.mdx +3 -3
  177. package/docs/content/locales/zh-TW/faq.mdx +4 -4
  178. package/docs/content/locales/zh-TW/getting-started.mdx +158 -212
  179. package/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -9
  180. package/docs/content/locales/zh-TW/template-chat.mdx +2 -2
  181. package/docs/content/locales/zh-TW/using-your-agent.mdx +2 -2
  182. package/docs/content/native-chat-ui.mdx +17 -32
  183. package/docs/content/pure-agent-apps.mdx +46 -26
  184. package/docs/content/template-chat.mdx +8 -8
  185. package/docs/content/using-your-agent.mdx +2 -2
  186. package/docs/content/what-is-agent-native.mdx +6 -6
  187. package/package.json +1 -1
@@ -1,27 +1,29 @@
1
1
  ---
2
- title: "Pure-Agent Apps"
3
- description: "Apps where the agent is the whole product: the app-agent loop is the front door, and UI is added only when humans need it."
2
+ title: "Automation-First Apps"
3
+ description: "No-browser Agent-Native apps for scheduled jobs, queues, scripts, integration workers, and external agents."
4
4
  ---
5
5
 
6
- # Pure-Agent Apps
6
+ # Automation-First Apps
7
7
 
8
- A pure-agent app is the minimal end of agent-native: the app-agent loop is the
9
- product, not a dashboard. You send a request from the terminal, Slack, email, a
10
- scheduled job, another agent, or Chat "summarize my unread emails," "post the
11
- daily metrics to Slack" — and the agent acts and returns the result wherever it
12
- belongs. It is still a real app: actions, sessions, app state, history,
13
- settings, credentials, and share records all live in SQL.
8
+ Most Agent-Native products should start with chat and grow into inline results
9
+ and durable app pages. Use this pattern when the primary surface is not a
10
+ browser UI yet: scheduled reports, queues, integration workers, CLI loops, or
11
+ another app or agent calling this app over MCP/A2A.
14
12
 
15
- <Diagram id="doc-block-1rfgtal" title="The app-agent loop is the front door" summary={"Many entry points reach one agent loop over SQL-backed actions and state; results return to wherever the request came from. UI is added only when humans need to supervise."}>
13
+ Automation-first does **not** mean stateless. The app-agent loop still uses the
14
+ same Agent-Native contract: actions, sessions, app state, history, settings,
15
+ credentials, and share records live in SQL.
16
+
17
+ <Diagram id="doc-block-automation-first" title="Automation-first still uses the app contract" summary={"Jobs, scripts, channels, and external agents reach one Agent-Native loop over SQL-backed actions and state."}>
16
18
 
17
19
  ```html
18
20
  <div class="diagram-pure">
19
21
  <div class="diagram-col">
20
- <div class="diagram-pill">Terminal</div>
21
- <div class="diagram-pill">Slack · email</div>
22
22
  <div class="diagram-pill">Scheduled job</div>
23
+ <div class="diagram-pill">Queue worker</div>
24
+ <div class="diagram-pill">CLI script</div>
25
+ <div class="diagram-pill">Slack · email</div>
23
26
  <div class="diagram-pill">Another agent (A2A)</div>
24
- <div class="diagram-pill">Chat</div>
25
27
  </div>
26
28
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
27
29
  <div class="diagram-panel center" data-rough>
@@ -30,7 +32,9 @@ settings, credentials, and share records all live in SQL.
30
32
  </div>
31
33
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
32
34
  <div class="diagram-box" data-rough>
33
- Result returns<br /><small class="diagram-muted">to where it belongs</small>
35
+ Result returns<br /><small class="diagram-muted"
36
+ >to the channel that asked</small
37
+ >
34
38
  </div>
35
39
  </div>
36
40
  ```
@@ -62,20 +66,36 @@ settings, credentials, and share records all live in SQL.
62
66
  </Diagram>
63
67
 
64
68
  Reach for this shape when the work runs in the background, the output leaves the
65
- app, the domain is one-shot, or you're prototyping. The agent still needs a UI —
66
- not a dashboard, but a place for humans to supervise, configure, and steer it
67
- which is why even pure-agent apps usually mount the built-in Chat shell.
69
+ app, the domain is one-shot, or you are building a callable workflow for another
70
+ system. If users need to inspect, compare, correct, or share persistent objects,
71
+ start with [Chat](/docs/template-chat) or add a small app page around the same
72
+ actions.
73
+
74
+ ## Scaffold {#scaffold}
75
+
76
+ Use the no-browser scaffold when you know you do not want a browser UI yet:
68
77
 
69
- This is the **headless** product shape. The full decision guide, what ships in
70
- the box, the scaffold, repo access, and run sharing now live in one place:
78
+ ```bash
79
+ npx @agent-native/core@latest create my-agent --headless
80
+ cd my-agent
81
+ pnpm install
82
+ ```
83
+
84
+ Then add actions and instructions exactly as you would in a chat app:
85
+
86
+ ```bash
87
+ pnpm agent "Run the daily response-insights report and post the summary."
88
+ ```
71
89
 
72
- [**Agent Surfaces Headless agent**](/docs/agent-surfaces#headless)
90
+ The `--headless` flag only chooses the initial scaffold. It does not choose a
91
+ different architecture. You can later move the same `actions/`, instructions,
92
+ skills, and SQL state into a chat app or full application surface.
73
93
 
74
- ## What's next
94
+ ## What to add next {#whats-next}
75
95
 
76
- - [**Agent Surfaces — Headless**](/docs/agent-surfaces#headless) — the full headless decision guide and APIs
77
- - [**Getting Started**](/docs/getting-started) — create a chat app or headless agent first
78
- - [**Dispatch**](/docs/template-dispatch) — the workspace template that's a great pure-agent starting point
79
- - [**Messaging the agent**](/docs/messaging) — how users talk to the agent across web, Slack, Telegram, email
96
+ - [**Getting Started**](/docs/getting-started) — the chat-first tutorial from action to inline result to durable page
97
+ - [**Agent Surfaces — Automation-first app**](/docs/agent-surfaces#headless) — the full decision guide and APIs
98
+ - [**Actions**](/docs/actions) — the operations your automation calls
99
+ - [**Messaging the agent**](/docs/messaging) — how users talk to the agent across web, Slack, Telegram, and email
80
100
  - [**Recurring Jobs**](/docs/recurring-jobs) — scheduled prompts the agent runs on its own
81
- - [**Actions**](/docs/actions) — the tools your pure-agent will call
101
+ - [**Dispatch**](/docs/template-dispatch) — the workspace control plane for shared secrets, scheduled jobs, and delegated work
@@ -5,9 +5,9 @@ description: "A minimal chat-first agent-native app: durable chat threads, actio
5
5
 
6
6
  # Chat Template
7
7
 
8
- Chat is the basic agent-native app starting point. It gives you a clean ChatGPT-style shell with chat at the center, a threads list on the left, standard app navigation, auth, live sync, actions, and one example action. Start here when you want a real browser app you can build on without committing to a domain template.
8
+ Chat is the basic agentic app starting point. It gives you a clean ChatGPT-style shell with chat at the center, a threads list on the left, standard app navigation, auth, live sync, actions, and one example action. Start here when you want a real browser app you can build on before committing to a domain template.
9
9
 
10
- If you want the smallest action-only runtime with no browser UI, start with [Pure-Agent Apps](/docs/pure-agent-apps). If you want a finished domain product shape, start from [Calendar](/docs/template-calendar), [Mail](/docs/template-mail), [Content](/docs/template-content), [Forms](/docs/template-forms), [Analytics](/docs/template-analytics), or another domain template.
10
+ If you want a finished domain product shape, start from [Calendar](/docs/template-calendar), [Mail](/docs/template-mail), [Content](/docs/template-content), [Forms](/docs/template-forms), [Analytics](/docs/template-analytics), or another domain template. If you truly do not need a browser UI yet, see [Automation-First Apps](/docs/pure-agent-apps).
11
11
 
12
12
  <WireframeBlock id="doc-block-1qu69ww">
13
13
  <Screen
@@ -104,7 +104,7 @@ That's the point. Chat is a thin, useful default shell for your own agent, not a
104
104
  ## When to pick it {#when-to-pick}
105
105
 
106
106
  - **You want a basic app users can talk to immediately** and then extend with actions and UI.
107
- - **You have a headless app that needs chat** as the first browser surface.
107
+ - **You want the default Agent-Native on-ramp**: chat first, then actions, native results, and app pages.
108
108
  - **You want to plug your own agent backend into a familiar chat UI** while keeping Agent-Native's actions, state, auth, and deployment shape.
109
109
  - **You are prototyping a custom internal tool** that does not match a domain template.
110
110
 
@@ -117,13 +117,13 @@ pnpm install
117
117
  pnpm dev
118
118
  ```
119
119
 
120
- Or start with no UI and add a chat surface later:
120
+ The no-browser scaffold is still available for automation-first work, but it is not the main Chat template path:
121
121
 
122
122
  ```bash
123
123
  npx @agent-native/core@latest create my-agent --headless
124
124
  ```
125
125
 
126
- From there, copy the Chat template's `/` route and sidebar thread list into your app, or scaffold a Chat app and move the actions from your headless agent into its `actions/` directory. The key invariant stays the same: actions are the shared surface for chat, UI, HTTP, MCP, A2A, and CLI.
126
+ Use it for scheduled jobs, queues, CLI loops, or external-agent workflows where chat and app pages are not useful yet. The key invariant stays the same: actions are the shared surface for chat, UI, HTTP, MCP, A2A, and CLI.
127
127
 
128
128
  ## First code to inspect {#first-code}
129
129
 
@@ -196,8 +196,8 @@ The agent should add a Drizzle schema, actions, route, navigation, and instructi
196
196
 
197
197
  ## What's next
198
198
 
199
- - [**Getting Started**](/docs) — choose between headless, chat, and domain templates
200
- - [**Agent Surfaces**](/docs/agent-surfaces) — headless, chat, embedded, and full-app patterns
199
+ - [**Getting Started**](/docs) — build the chat-first path from action to inline result to app page
200
+ - [**Agent Surfaces**](/docs/agent-surfaces) — chat, inline UI, app pages, embedded, automation, and external-agent patterns
201
201
  - [**Actions**](/docs/actions) — the action system chat and UI both call
202
202
  - [**Native Chat UI**](/docs/native-chat-ui) — chat surface primitives and runtime options
203
- - [**Pure-Agent Apps**](/docs/pure-agent-apps) — action-only apps that can grow into Chat later
203
+ - [**Automation-First Apps**](/docs/pure-agent-apps) — no-browser workflows for jobs, queues, scripts, and external agents
@@ -99,7 +99,7 @@ The agent isn't a separate app you tab over to. It ships as a handful of React c
99
99
 
100
100
  Not every app needs a full dashboard. When the agent _is_ the product, you can skip most of the custom UI: open the app, ask for what you want, and let the agent do the rest. The agent still has its management surface — history, workspace, settings — but the primary interaction is conversation rather than clicks.
101
101
 
102
- → [**Pure-Agent Apps**](/docs/pure-agent-apps) — apps where the agent is the whole product.
102
+ → [**Automation-First Apps**](/docs/pure-agent-apps) — no-browser apps for jobs, queues, scripts, and external agents.
103
103
 
104
104
  ## You co-edit with it {#you-co-edit}
105
105
 
@@ -113,5 +113,5 @@ When you and the agent are working on the same document, you don't take turns. W
113
113
  - [**Agent Mentions**](/docs/agent-mentions) — direct it with `@`-mentions
114
114
  - [**Voice Input**](/docs/voice-input) — direct it by speaking
115
115
  - [**Drop-in Agent**](/docs/drop-in-agent) — embed it in any React app
116
- - [**Pure-Agent Apps**](/docs/pure-agent-apps) — go UI-light when the agent is the product
116
+ - [**Automation-First Apps**](/docs/pure-agent-apps) — use no-browser apps for jobs, queues, scripts, and external agents
117
117
  - [**Real-Time Collaboration**](/docs/real-time-collaboration) — co-edit the same document together
@@ -5,13 +5,13 @@ description: "Why most AI apps feel half-built, what makes an app truly agent-na
5
5
 
6
6
  # What Is Agent-Native?
7
7
 
8
- Agent-native is a way of building software where the AI agent and the product surface around it are **equal partners**. That surface can be a headless agent with one custom action, a rich chat, or a full UI. The important part is that agents and humans share the same actions, database, and state.
8
+ Agent-native is a way of building software where the AI agent and the product surface around it are **equal partners**. The usual path starts with chat, adds typed actions, renders structured results inline, and grows into durable app pages around the same SQL state. The important part is that agents and humans share the same actions, database, and state.
9
9
 
10
10
  If you only remember one thing from this page, remember this: most AI apps today stop one step short of being useful, and that gap is the biggest mistake in the space right now.
11
11
 
12
12
  ## What it looks like as a user {#what-it-looks-like}
13
13
 
14
- Picture a background worker, inbox, calendar, form builder, or analytics dashboard. Sometimes there is no custom screen yet: you run one action or one headless app-agent prompt. Sometimes the first screen is chat: you ask what you want, the agent guides setup, shows a table or chart, and opens the right app view. Sometimes chat is docked on the right side of a full application. Across those shapes, you can:
14
+ Picture a background worker, inbox, calendar, form builder, or analytics dashboard. Often the first screen is chat: you ask what you want, the agent guides setup, shows a table or chart, and opens the right app view. Sometimes chat is docked on the right side of a full application. Sometimes there is no custom screen yet because the workflow is a scheduled job, queue, script, or external-agent integration. Across those shapes, you can:
15
15
 
16
16
  - **Start with the real operation.** One durable action can run from the CLI, HTTP, MCP, A2A, the app-agent loop, and later a UI.
17
17
  - **Click anything you'd normally click when there is a UI.** All the buttons, lists, dashboards, keyboard shortcuts — they all call the same operations the agent can call.
@@ -116,7 +116,7 @@ Even when the agent does all the heavy lifting, humans still need to:
116
116
 
117
117
  At minimum, "a UI for the agent" is an observability and management dashboard. At maximum, it's a full SaaS app with the agent embedded as a co-pilot. Both ends count as agent-native, and the surface can grow from one without a rewrite.
118
118
 
119
- You don't have to choose a shape up front. The agent can run headless, sit behind a rich chat, or live inside a full application around the same action surface — see [Agent Surfaces](/docs/agent-surfaces) for the concrete shapes and APIs.
119
+ You don't have to choose every surface up front. The agent can start in rich chat, render native inline UI, live inside a full application, or run from automation and external-agent channels around the same action surface — see [Agent Surfaces](/docs/agent-surfaces) for the concrete shapes and APIs.
120
120
 
121
121
  ## Why every app benefits from an agent {#why-every-app-benefits-from-an-agent}
122
122
 
@@ -124,7 +124,7 @@ The flip side is just as important. Existing SaaS products keep hitting the same
124
124
 
125
125
  Agent-native flips that. Because every action in the app is defined once and exposed as both a button and an agent tool, the agent can do everything the buttons can — and more — without a separate "AI world" to maintain. Natural language becomes a first-class input alongside clicks.
126
126
 
127
- The argument isn't "agents replace UI." It's "**agents belong inside applications, with a UI on top, as equal partners**." Even an app where the agent _is_ the product still needs a UI for humans to supervise, configure, and steer it — see [Agent Surfaces — Headless](/docs/agent-surfaces#headless).
127
+ The argument isn't "agents replace UI." It's "**agents belong inside applications, with a UI on top, as equal partners**." Even an automation-first app still needs some surface for humans to supervise, configure, and steer it — usually chat, a job history, or a small status page. See [Agent Surfaces — Automation-first app](/docs/agent-surfaces#headless).
128
128
 
129
129
  ## Agent + UI parity {#agent-ui-parity}
130
130
 
@@ -394,8 +394,8 @@ One action, many surfaces: the agent calls it as a tool, the UI calls it as a ty
394
394
 
395
395
  ## What's next {#whats-next}
396
396
 
397
- - [**Getting Started**](/docs/getting-started) — start with one action, pick a template, or install a skill
398
- - [**Agent Surfaces**](/docs/agent-surfaces) — choose headless, rich chat, embedded sidecar, or full app
397
+ - [**Getting Started**](/docs/getting-started) — start with chat, add an action, render a native result, and grow into a page
398
+ - [**Agent Surfaces**](/docs/agent-surfaces) — choose chat, native inline UI, full app pages, embedded sidecar, automation, or external agent access
399
399
  - [**Key Concepts**](/docs/key-concepts) — the architecture: SQL, actions, polling sync, context awareness, portability
400
400
  - [**Templates**](/docs/cloneable-saas) — templates as complete products you own
401
401
  - [**Workspace**](/docs/workspace) — the per-user customization layer (skills, memory, instructions, MCP) backed by SQL, not files
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.84.35",
3
+ "version": "0.84.36",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -762,15 +762,13 @@ background, text, border, ring, fill, stroke, gradient, placeholder, decoration,
762
762
  or shadow color, rewrite it to renderer tokens or remove it. Layout-only classes
763
763
  are still discouraged; inline flex/grid styles are safer and easier to review.
764
764
 
765
- **Keep Rough.js sparse.** \`.wf-card\` and \`.wf-box\` already render with
766
- theme-safe filled backgrounds (\`--wf-card\`) and soft tokenized borders that work
767
- in both light and dark mode. Do not add \`data-rough\` to broad root wrappers,
768
- dialog shells, page panels, grid cells, or nested containers unless that single
769
- container is the visual point. The renderer sketches the outer frame and
770
- standard controls by default; use \`data-rough\` only for a deliberate one-off
771
- shape. If a mockup starts looking like stacked/overlapping sketch lines, remove
772
- rough targets from parent containers and let backgrounds plus spacing separate
773
- the surfaces.
765
+ **Keep Rough.js sparse.** The renderer sketches the outer frame, standard
766
+ \`.wf-*\` primitives, controls, and inline border dividers by default. Do not add
767
+ \`data-rough\` to broad root wrappers, dialog shells, page panels, grid cells, or
768
+ nested containers unless that single container is the visual point. Use
769
+ \`data-rough\` only for a deliberate one-off shape. If a mockup starts looking
770
+ like stacked/overlapping sketch lines, remove rough targets from parent
771
+ containers and let backgrounds plus spacing separate the surfaces.
774
772
 
775
773
  **Use literal CSS lengths for spacing.** The \`--wf-*\` tokens are for colors and
776
774
  renderer-owned visual styling, not layout spacing. Do not use guessed spacing
@@ -2685,6 +2685,8 @@ export function MultiTabAssistantChat({
2685
2685
  <RunStuckBanner
2686
2686
  threadId={tabId}
2687
2687
  apiUrl={apiUrl}
2688
+ autoRetry
2689
+ autoRetryOwnerId={browserTabId}
2688
2690
  onRetry={() => {
2689
2691
  const handle = chatRefs.current.get(tabId);
2690
2692
  handle?.sendRecoveryMessage(
@@ -38,21 +38,119 @@ export interface RunStuckBannerProps {
38
38
  * observability events (Sentry, PostHog) at the call site.
39
39
  */
40
40
  onStuckStateChange?: (state: RunStuckState) => void;
41
+ /**
42
+ * Automatically abort and retry once when the server reports a run is
43
+ * alive but has stopped making real progress. Manual controls remain visible
44
+ * as the fallback if the automatic recovery cannot clear the run.
45
+ */
46
+ autoRetry?: boolean;
47
+ /**
48
+ * Stable browser-tab/window id used to coordinate automatic retries across
49
+ * multiple mounted chat views for the same thread. A local id is generated
50
+ * when omitted.
51
+ */
52
+ autoRetryOwnerId?: string;
41
53
  /** Extra class on the outer container. */
42
54
  className?: string;
43
55
  }
44
56
 
57
+ const AUTO_RETRY_CLAIM_TTL_MS = 5 * 60 * 1000;
58
+
59
+ type BusyState = { type: "none" } | { type: "cancel" | "retry"; runId: string };
60
+
61
+ type MaybeLockManager = {
62
+ request<T>(
63
+ name: string,
64
+ options: { mode?: "exclusive" | "shared"; ifAvailable?: boolean },
65
+ callback: (lock: unknown) => T | Promise<T>,
66
+ ): Promise<T>;
67
+ };
68
+
69
+ function createAutoRetryOwnerId() {
70
+ const cryptoApi =
71
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
72
+ ? crypto
73
+ : null;
74
+ return cryptoApi?.randomUUID() ?? `owner-${Math.random().toString(36)}`;
75
+ }
76
+
77
+ function autoRetryClaimKey(threadId: string, runId: string) {
78
+ return `agent-native:stuck-auto-retry:${threadId}:${runId}`;
79
+ }
80
+
81
+ function markAutoRetryClaim(key: string, ownerId: string) {
82
+ if (typeof window === "undefined") return true;
83
+ const now = Date.now();
84
+ try {
85
+ const raw = window.localStorage.getItem(key);
86
+ if (raw) {
87
+ const existing = JSON.parse(raw) as {
88
+ ownerId?: unknown;
89
+ expiresAt?: unknown;
90
+ };
91
+ const expiresAt =
92
+ typeof existing.expiresAt === "number" ? existing.expiresAt : 0;
93
+ if (expiresAt > now && existing.ownerId !== ownerId) return false;
94
+ }
95
+ window.localStorage.setItem(
96
+ key,
97
+ JSON.stringify({ ownerId, expiresAt: now + AUTO_RETRY_CLAIM_TTL_MS }),
98
+ );
99
+ const confirmed = JSON.parse(window.localStorage.getItem(key) ?? "{}") as {
100
+ ownerId?: unknown;
101
+ };
102
+ return confirmed.ownerId === ownerId;
103
+ } catch {
104
+ // Private browsing or disabled storage: fall back to in-tab retry. The
105
+ // server abort is idempotent, and manual controls remain available.
106
+ return true;
107
+ }
108
+ }
109
+
110
+ async function claimAutoRetryAttempt(
111
+ threadId: string | null | undefined,
112
+ runId: string,
113
+ ownerId: string,
114
+ ) {
115
+ if (!threadId) return true;
116
+ const key = autoRetryClaimKey(threadId, runId);
117
+ const locks =
118
+ typeof navigator !== "undefined"
119
+ ? (navigator as Navigator & { locks?: MaybeLockManager }).locks
120
+ : undefined;
121
+ if (locks?.request) {
122
+ try {
123
+ return await locks.request(
124
+ key,
125
+ { mode: "exclusive", ifAvailable: true },
126
+ (lock) => (lock ? markAutoRetryClaim(key, ownerId) : false),
127
+ );
128
+ } catch {
129
+ return markAutoRetryClaim(key, ownerId);
130
+ }
131
+ }
132
+ return markAutoRetryClaim(key, ownerId);
133
+ }
134
+
45
135
  export function RunStuckBanner({
46
136
  threadId,
47
137
  apiUrl,
48
138
  stuckThresholdMs,
49
139
  onRetry,
50
140
  onStuckStateChange,
141
+ autoRetry = false,
142
+ autoRetryOwnerId,
51
143
  className,
52
144
  }: RunStuckBannerProps) {
53
145
  const state = useRunStuckDetection({ threadId, stuckThresholdMs, apiUrl });
54
146
  const abortRun = useAbortRun(apiUrl);
55
- const [busy, setBusy] = useState<"none" | "cancel" | "retry">("none");
147
+ const [busy, setBusy] = useState<BusyState>({ type: "none" });
148
+ const autoRetriedRunIdsRef = useRef<Set<string>>(new Set());
149
+ const generatedOwnerIdRef = useRef<string | null>(null);
150
+ if (!generatedOwnerIdRef.current) {
151
+ generatedOwnerIdRef.current = createAutoRetryOwnerId();
152
+ }
153
+ const ownerId = autoRetryOwnerId ?? generatedOwnerIdRef.current;
56
154
 
57
155
  const lastReportedRef = useRef<{
58
156
  isStuck: boolean;
@@ -77,39 +175,92 @@ export function RunStuckBanner({
77
175
  }
78
176
  }, [state, onStuckStateChange, threadId]);
79
177
 
80
- // Reset the busy spinner once the underlying run has flipped away from
81
- // running no need to keep showing a stale "Retrying…" indicator.
178
+ // Reset the busy spinner once the underlying run is no longer the one we
179
+ // acted on. A recovery continuation can start quickly enough that polling
180
+ // never observes an idle state between run ids.
82
181
  useEffect(() => {
83
- if (state.status !== "running") {
84
- setBusy("none");
182
+ setBusy((current) => {
183
+ if (current.type === "none") return current;
184
+ if (state.status !== "running") return { type: "none" };
185
+ if (state.runId && state.runId !== current.runId) return { type: "none" };
186
+ return current;
187
+ });
188
+ }, [state.runId, state.status]);
189
+
190
+ useEffect(() => {
191
+ if (
192
+ !autoRetry ||
193
+ !state.isStuck ||
194
+ !state.runId ||
195
+ busy.type !== "none" ||
196
+ autoRetriedRunIdsRef.current.has(state.runId)
197
+ ) {
198
+ return;
85
199
  }
86
- }, [state.status]);
200
+
201
+ const runId = state.runId;
202
+ void claimAutoRetryAttempt(threadId, runId, ownerId).then((claimed) => {
203
+ autoRetriedRunIdsRef.current.add(runId);
204
+ if (!claimed) return;
205
+ setBusy({ type: "retry", runId });
206
+ trackEvent("agent_chat_stuck_auto_retry", {
207
+ runId,
208
+ threadId: threadId ?? null,
209
+ stuckSinceMs: state.stuckSinceMs ?? null,
210
+ });
211
+ void abortRun(runId, "auto_stuck_retry").then((aborted) => {
212
+ if (aborted) {
213
+ onRetry?.(aborted);
214
+ return;
215
+ }
216
+ setBusy((current) =>
217
+ current.type !== "none" && current.runId === runId
218
+ ? { type: "none" }
219
+ : current,
220
+ );
221
+ });
222
+ });
223
+ }, [
224
+ abortRun,
225
+ autoRetry,
226
+ busy,
227
+ onRetry,
228
+ ownerId,
229
+ state.isStuck,
230
+ state.runId,
231
+ state.stuckSinceMs,
232
+ threadId,
233
+ ]);
87
234
 
88
235
  if (!state.isStuck || !state.runId) return null;
89
236
 
90
237
  const handleCancel = async () => {
91
- if (!state.runId || busy !== "none") return;
92
- setBusy("cancel");
238
+ if (!state.runId || busy.type !== "none") return;
239
+ const runId = state.runId;
240
+ setBusy({ type: "cancel", runId });
93
241
  trackEvent("agent_chat_stuck_cancel", {
94
- runId: state.runId,
242
+ runId,
95
243
  threadId: threadId ?? null,
96
244
  stuckSinceMs: state.stuckSinceMs ?? null,
97
245
  });
98
- await abortRun(state.runId, "user_stuck_cancel");
246
+ await abortRun(runId, "user_stuck_cancel");
99
247
  };
100
248
 
101
249
  const handleRetry = async () => {
102
- if (!state.runId || busy !== "none") return;
103
- setBusy("retry");
250
+ if (!state.runId || busy.type !== "none") return;
251
+ const runId = state.runId;
252
+ setBusy({ type: "retry", runId });
104
253
  trackEvent("agent_chat_stuck_retry", {
105
- runId: state.runId,
254
+ runId,
106
255
  threadId: threadId ?? null,
107
256
  stuckSinceMs: state.stuckSinceMs ?? null,
108
257
  });
109
- const aborted = await abortRun(state.runId, "user_stuck_retry");
258
+ const aborted = await abortRun(runId, "user_stuck_retry");
110
259
  if (aborted) onRetry?.(aborted);
111
260
  };
112
261
 
262
+ const busyType = busy.type;
263
+
113
264
  const stuckSeconds =
114
265
  state.stuckSinceMs != null ? Math.floor(state.stuckSinceMs / 1000) : null;
115
266
 
@@ -134,16 +285,19 @@ export function RunStuckBanner({
134
285
  No progress
135
286
  {stuckSeconds != null ? ` for ${stuckSeconds}s` : ""}. The agent may
136
287
  have hit a server timeout or lost its connection.
288
+ {autoRetry && busyType === "retry"
289
+ ? " Retrying automatically now."
290
+ : ""}
137
291
  </span>
138
292
  </div>
139
293
  <div className="flex flex-wrap items-center gap-2">
140
294
  <button
141
295
  type="button"
142
296
  onClick={handleRetry}
143
- disabled={busy !== "none"}
297
+ disabled={busyType !== "none"}
144
298
  className="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md bg-foreground px-2.5 text-[11px] font-medium text-background transition-colors hover:bg-foreground/90 disabled:cursor-not-allowed disabled:opacity-60"
145
299
  >
146
- {busy === "retry" ? (
300
+ {busyType === "retry" ? (
147
301
  <IconLoader2
148
302
  size={12}
149
303
  className="animate-spin"
@@ -155,10 +309,10 @@ export function RunStuckBanner({
155
309
  <button
156
310
  type="button"
157
311
  onClick={handleCancel}
158
- disabled={busy !== "none"}
312
+ disabled={busyType !== "none"}
159
313
  className="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md border border-border bg-background px-2.5 text-[11px] font-medium text-foreground transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-60"
160
314
  >
161
- {busy === "cancel" ? (
315
+ {busyType === "cancel" ? (
162
316
  <IconLoader2
163
317
  size={12}
164
318
  className="animate-spin"
@@ -26,10 +26,10 @@ import type {
26
26
  * DECOUPLING: the only behavioral change from the plan copy is theme detection —
27
27
  * core blocks read `document.documentElement.classList.contains("dark")` (the
28
28
  * MermaidBlock precedent) instead of importing `next-themes`. Everything else
29
- * (the `.plan-wf` / `[data-rough]` class contract the rough overlay measures,
30
- * the `--wf-*` / `--ink` / `--paper` token names every primitive reads) is
31
- * preserved exactly, so the kit looks identical in plan and renders correctly in
32
- * any app once the matching tokens exist in `core/styles/blocks.css`.
29
+ * (the `.plan-wf` / `.wf-*` / `[data-rough]` class contract the rough overlay
30
+ * measures, the `--wf-*` / `--ink` / `--paper` token names every primitive
31
+ * reads) is preserved exactly, so the kit looks identical in plan and renders
32
+ * correctly in any app once the matching tokens exist in `core/styles/blocks.css`.
33
33
  */
34
34
 
35
35
  /* ========================================================================== */
@@ -211,7 +211,7 @@ export function Screen({
211
211
  // clipping. The frame shell (`ArtboardFrame`) owns the height policy; a
212
212
  // caller can still override via `style.height` for a fixed canvas.
213
213
  minHeight: "100%",
214
- background: V.paper,
214
+ background: "transparent",
215
215
  color: V.ink,
216
216
  fontFamily: V.hand,
217
217
  fontSize: V.fs,
@@ -1425,9 +1425,9 @@ const gen = rough.generator();
1425
1425
 
1426
1426
  type RoughPath = { d: string; stroke: string; strokeWidth: number };
1427
1427
 
1428
- /** The default selector used for HTML mockups: controls plus explicit opt-ins. */
1428
+ /** The default selector used for HTML mockups: standard wireframe primitives plus explicit opt-ins. */
1429
1429
  export const HTML_ROUGH_SELECTOR =
1430
- "[data-rough],button,input,textarea,select,hr";
1430
+ "[data-rough],button,input,textarea,select,hr,.wf-btn,.wf-card,.wf-box,.wf-pill,.wf-chip,.wf-icon-fallback,[style*='border:'],[style*='border-top:'],[style*='border-right:'],[style*='border-bottom:'],[style*='border-left:']";
1431
1431
 
1432
1432
  /** Stable per-element seed so a frame doesn't re-wobble on every measure. */
1433
1433
  function seedFrom(...parts: Array<string | number>): number {
@@ -1528,6 +1528,50 @@ function elementStroke(node: Element, fallback: string): string {
1528
1528
  return fallback;
1529
1529
  }
1530
1530
 
1531
+ function visibleBorderSides(
1532
+ node: Element,
1533
+ ): Array<"top" | "right" | "bottom" | "left"> {
1534
+ const cs = getComputedStyle(node);
1535
+ const sides: Array<"top" | "right" | "bottom" | "left"> = [];
1536
+ for (const [side, widthProp, colorProp] of [
1537
+ ["top", "borderTopWidth", "borderTopColor"],
1538
+ ["right", "borderRightWidth", "borderRightColor"],
1539
+ ["bottom", "borderBottomWidth", "borderBottomColor"],
1540
+ ["left", "borderLeftWidth", "borderLeftColor"],
1541
+ ] as const) {
1542
+ const width = parseFloat(cs[widthProp]);
1543
+ const color = cs[colorProp];
1544
+ if (
1545
+ width > 0 &&
1546
+ color &&
1547
+ color !== "rgba(0, 0, 0, 0)" &&
1548
+ color !== "transparent"
1549
+ ) {
1550
+ sides.push(side);
1551
+ }
1552
+ }
1553
+ return sides;
1554
+ }
1555
+
1556
+ function inferRoughKind(node: HTMLElement): string {
1557
+ const explicit = node.getAttribute("data-rough");
1558
+ if (explicit) return explicit;
1559
+ if (node.tagName === "HR") return "line:middle";
1560
+ const sides = visibleBorderSides(node);
1561
+ if (sides.length !== 1) return "rect";
1562
+ switch (sides[0]) {
1563
+ case "top":
1564
+ return "line:top";
1565
+ case "right":
1566
+ return "line:right";
1567
+ case "bottom":
1568
+ return "line:bottom";
1569
+ case "left":
1570
+ return "line:left";
1571
+ }
1572
+ return "rect";
1573
+ }
1574
+
1531
1575
  function build(
1532
1576
  scope: HTMLElement,
1533
1577
  opts: {
@@ -1602,7 +1646,7 @@ function build(
1602
1646
  const w = r.width / zoom;
1603
1647
  const h = r.height / zoom;
1604
1648
  if (w < 2 || h < 2) return;
1605
- const kind = node.getAttribute("data-rough") || "rect";
1649
+ const kind = inferRoughKind(node);
1606
1650
  const rawStroke = elementStroke(node, sketch);
1607
1651
  const stroke =
1608
1652
  sameColor(rawStroke, ink) || (line !== "" && sameColor(rawStroke, line))
@@ -1621,11 +1665,15 @@ function build(
1621
1665
  let drawable: unknown;
1622
1666
  if (kind === "ellipse") {
1623
1667
  drawable = gen.ellipse(x + w / 2, y + h / 2, w, h, o);
1668
+ } else if (kind === "line:left") {
1669
+ drawable = gen.line(x, y, x, y + h, o);
1624
1670
  } else if (kind === "line:right") {
1625
1671
  drawable = gen.line(x + w, y, x + w, y + h, o);
1626
1672
  } else if (kind === "line:bottom") {
1627
1673
  drawable = gen.line(x, y + h, x + w, y + h, o);
1628
- } else if (kind === "line:top" || node.tagName === "HR") {
1674
+ } else if (kind === "line:top") {
1675
+ drawable = gen.line(x, y, x + w, y, o);
1676
+ } else if (kind === "line:middle") {
1629
1677
  drawable = gen.line(x, y + h / 2, x + w, y + h / 2, o);
1630
1678
  } else {
1631
1679
  const cr = parseFloat(getComputedStyle(node).borderTopLeftRadius) || 0;