dopus 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,13 +27,13 @@ pnpm dopus init
27
27
 
28
28
  `init` asks for a provider, an API key and a prompt preset, then:
29
29
 
30
- | Writes | What it is |
31
- | --------------------------------------------------- | ----------------------------------------------------------------------------- |
32
- | `.env.local` | Your API key and a generated `DOPUS_APPROVAL_SECRET`. Never `.env`. Mode 600. |
33
- | `agent.config.ts` | Model, tools and prompt layers. Edit this. |
34
- | `agent/prompts/preset.ts`, `agent/prompts/app.ts` | Your prompt layers. Yours to edit. |
35
- | `app/api/agent/route.ts` | `POST /api/agent`. It speaks the AI SDK `useChat` protocol. |
36
- | `components/agent/agent-chat.tsx` (+ `.module.css`) | Chat panel and floating widget. |
30
+ | Writes | What it is |
31
+ | ------------------------------------------------- | ----------------------------------------------------------------------------- |
32
+ | `.env.local` | Your API key and a generated `DOPUS_APPROVAL_SECRET`. Never `.env`. Mode 600. |
33
+ | `agent.config.ts` | Model, tools and prompt layers. Edit this. |
34
+ | `agent/prompts/preset.ts`, `agent/prompts/app.ts` | Your prompt layers. Yours to edit. |
35
+ | `app/api/agent/route.ts` | `POST /api/agent`. It speaks the AI SDK `useChat` protocol. |
36
+ | `components/agent/agent-chat.tsx` (+ `.css`) | Chat panel and floating widget. Plain CSS with `dopus-` classes. |
37
37
 
38
38
  It also installs whatever is missing of `ai`, `zod`, `@ai-sdk/react` and the provider package. It checks that `.env.local` is git-ignored.
39
39
 
@@ -115,7 +115,7 @@ Only two kinds of error message reach the model:
115
115
  - a `ToolError` you throw on purpose, such as `throw new ToolError('Order o-1 has already shipped')`;
116
116
  - Next.js `redirect()`, `notFound()`, `forbidden()` and `unauthorized()`, which are translated into plain messages such as "the user is not signed in".
117
117
 
118
- Any other error goes to `onError` and is replaced with a generic message, so database errors and stack details stay on the server. The browser only ever sees a generic error text.
118
+ Any other error goes to `onError` and is replaced with a generic message, so database errors and stack details stay on the server. The browser sees the same text as the model: the `ToolError` message, or the generic one.
119
119
 
120
120
  ## Permissions and security
121
121
 
@@ -156,9 +156,9 @@ Anthropic gets an explicit cache breakpoint after layer 3. OpenAI and Gemini cac
156
156
 
157
157
  Both accept `api`, `suggestions`, `labels` (every UI string, for translation) and `className`.
158
158
 
159
- The UI shows each tool call as it runs. It renders an approve/decline card for confirmed tools and continues automatically once the user answers. It also has stop and retry buttons, and it does not send on Enter while an IME (Korean, Japanese, Chinese) is still composing.
159
+ The UI shows each tool call as it runs. It renders an approve/decline card for confirmed tools and continues automatically once the user answers. When a tool has run, it calls `router.refresh()` so server components show the changed data. Closing the widget keeps the conversation. It also has stop and retry buttons, and it does not send on Enter while an IME (Korean, Japanese, Chinese) is still composing.
160
160
 
161
- Styling is a plain CSS module themed through `--dopus-*` variables, with dark mode included. It needs no Tailwind and no component library. The file is yours, so restyle it or swap in your own components.
161
+ Styling is one plain CSS file (`agent-chat.css`) whose classes all start with `dopus-`, themed through `--dopus-*` variables, with dark mode included. It is deliberately not a CSS Module: create-next-app's Tailwind setup routes every `*.css` file, modules included, through its own Turbopack loader, which left module classes unstyled. It needs no Tailwind and no component library. The file is yours, so restyle it or swap in your own components.
162
162
 
163
163
  ## Configuration
164
164
 
@@ -274,6 +274,7 @@ Planned next:
274
274
  pnpm install
275
275
  pnpm build && pnpm lint && pnpm typecheck && pnpm test
276
276
  pnpm example:build && pnpm example:e2e # next start + scripted model
277
+ pnpm example:live # real model from examples/next-app/.env.local (costs cents)
277
278
  pnpm smoke # packed tarball → fresh app → dopus init → next build
278
279
  pnpm aws:emu # OpenNext build → local Lambda streaming invoke
279
280
  pnpm cf:e2e # dopus infra cloudflare → wrangler dev (workerd) → e2e
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as PROVIDERS, t as VERSION } from "./version-DOEBWDfi.js";
2
+ import { n as PROVIDERS, t as VERSION } from "./version-DKjmKSap.js";
3
3
  import { execFileSync, spawn } from "node:child_process";
4
4
  import { parseArgs } from "node:util";
5
5
  import * as p from "@clack/prompts";
@@ -342,8 +342,8 @@ async function runInit(options, io) {
342
342
  []
343
343
  ],
344
344
  [
345
- at("components/agent/agent-chat.module.css"),
346
- () => readTemplate("ui/agent-chat.module.css"),
345
+ at("components/agent/agent-chat.css"),
346
+ () => readTemplate("ui/agent-chat.css"),
347
347
  []
348
348
  ]
349
349
  ];
@@ -964,6 +964,7 @@ async function infra(args) {
964
964
  async function main() {
965
965
  const [command, ...rest] = process.argv.slice(2);
966
966
  if (command === "--version" || command === "-v") return console.log(VERSION);
967
+ if (rest.includes("--help") || rest.includes("-h")) return console.log(HELP);
967
968
  if (command === "init") return init(rest);
968
969
  if (command === "infra") return infra(rest);
969
970
  console.log(HELP);
package/dist/index.d.ts CHANGED
@@ -103,7 +103,7 @@ interface AgentConfig<TOOLS extends DopusToolSet = DopusToolSet> {
103
103
  approvalSecret?: string;
104
104
  /** Extra `streamText` settings (temperature, providerOptions, ...). */
105
105
  callOptions?: Pick<StreamTextOptions, 'temperature' | 'maxOutputTokens' | 'providerOptions' | 'maxRetries' | 'headers' | 'onEnd' | 'onStepEnd'>;
106
- /** Called for errors; the client only ever sees a generic message. */
106
+ /** Called for unexpected errors (not `ToolError`); the client only sees a generic message for those. */
107
107
  onError?: (error: unknown) => void;
108
108
  /**
109
109
  * While the stream is idle (e.g. a slow tool), send an SSE comment every this many ms so proxies
@@ -150,5 +150,5 @@ interface ProviderInfo {
150
150
  }
151
151
  /** Verified against @ai-sdk/openai@4.0.111, @ai-sdk/anthropic@4.0.99, @ai-sdk/google@4.0.114. */
152
152
  export declare const PROVIDERS: Record<ProviderId, ProviderInfo>;
153
- export declare const VERSION = "0.2.0";
153
+ export declare const VERSION = "0.2.2";
154
154
  export type { AgentConfig, AgentUser, ClientContext, ConfirmOption, DopusTool, DopusToolSet, PromptLayers, ProviderId, ProviderInfo, RuntimeContext, ToolContext, ToolOptions };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as PROVIDERS, t as VERSION } from "./version-DOEBWDfi.js";
1
+ import { n as PROVIDERS, t as VERSION } from "./version-DKjmKSap.js";
2
2
  import { convertToModelMessages, createUIMessageStreamResponse, isStepCount, safeValidateUIMessages, streamText, toUIMessageStream, tool } from "ai";
3
3
  // src/next-errors.ts
4
4
  /**
@@ -268,6 +268,7 @@ function createAgentHandler(config) {
268
268
  tools,
269
269
  originalMessages: messages,
270
270
  onError: (error) => {
271
+ if (error instanceof ToolError) return error.message;
271
272
  report(error);
272
273
  return "Something went wrong. Please try again.";
273
274
  }
@@ -27,5 +27,5 @@ const PROVIDERS = {
27
27
  }
28
28
  };
29
29
  // src/version.ts
30
- const VERSION = "0.2.0";
30
+ const VERSION = "0.2.2";
31
31
  export { PROVIDERS as n, VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dopus",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Drop an AI agent into your existing Next.js app — your server functions become its tools.",
5
5
  "license": "MIT",
6
6
  "author": "akodm",
@@ -68,6 +68,7 @@
68
68
  "@types/node": "^24.0.0",
69
69
  "@types/react": "19.3.0",
70
70
  "ai": "7.0.166",
71
+ "next": "16.4.6",
71
72
  "react": "19.3.1",
72
73
  "tsdown": "0.23.7",
73
74
  "typescript": "6.0.3",
@@ -6,12 +6,14 @@
6
6
  * Keep it stable: it is part of the cached prompt prefix, so per-request values belong in `runtime`
7
7
  * in agent.config.ts instead.
8
8
  */
9
- export default `Service: <name> — <what it does, in one sentence>
9
+ const appPrompt = `Service: <name> — <what it does, in one sentence>
10
10
  Users: <who uses it and what they usually come to do>
11
11
  Key concepts: <the nouns your users use, e.g. "workspace", "order", "plan", and what they mean>
12
12
  Rules: <anything the agent must know, e.g. refund policy, business hours>
13
13
  Human help: <how users reach a person, e.g. support@example.com or the Help page>`
14
14
 
15
+ export default appPrompt
16
+
15
17
  /** Optional notes per language (tone, terminology). The one matching the request locale is used. */
16
18
  export const locales: Record<string, string> = {
17
19
  // ko: '존댓말을 사용하고, "주문"과 "발주"를 구분해서 쓰세요.',
@@ -2,7 +2,7 @@
2
2
  * Layer 2 — Preset: customer support. Copied by `dopus init`; this file is yours to edit.
3
3
  * It sits between dopus' core rules and your app context (agent/prompts/app.ts).
4
4
  */
5
- export default `Role: you are the support assistant for this service's customers.
5
+ const presetPrompt = `Role: you are the support assistant for this service's customers.
6
6
 
7
7
  How to work:
8
8
  - Look up the customer's own data (account, orders, tickets) with the tools before answering questions about it.
@@ -11,3 +11,5 @@ How to work:
11
11
  - Do not promise refunds, compensation, delivery dates or exceptions unless a tool result or the app context states that policy.
12
12
  - Never ask for passwords, full card numbers or full ID numbers.
13
13
  - When you cannot resolve the issue, when the customer is upset, or when they ask for a person, explain how to reach a human (see the app context) and summarise the case so they do not have to repeat it.`
14
+
15
+ export default presetPrompt
@@ -2,7 +2,7 @@
2
2
  * Layer 2 — Preset: general assistant. Copied by `dopus init`; this file is yours to edit.
3
3
  * It sits between dopus' core rules and your app context (agent/prompts/app.ts).
4
4
  */
5
- export default `Role: you are the in-app assistant. You answer questions about the app and do tasks for the user with the available tools.
5
+ const presetPrompt = `Role: you are the in-app assistant. You answer questions about the app and do tasks for the user with the available tools.
6
6
 
7
7
  How to work:
8
8
  - If a tool can do what the user asks, do it rather than explaining how to do it by hand.
@@ -10,3 +10,5 @@ How to work:
10
10
  - After a tool runs, report the outcome in plain words: what changed, or what you found.
11
11
  - When a request is ambiguous, offer the two or three most likely interpretations and ask which one.
12
12
  - If something is outside what the tools allow, point to where in the app the user can do it.`
13
+
14
+ export default presetPrompt
@@ -2,7 +2,7 @@
2
2
  * Layer 2 — Preset: internal operations (admin / back office). Copied by `dopus init`; this file is yours to edit.
3
3
  * It sits between dopus' core rules and your app context (agent/prompts/app.ts).
4
4
  */
5
- export default `Role: you are an assistant for staff working in an internal admin tool.
5
+ const presetPrompt = `Role: you are an assistant for staff working in an internal admin tool.
6
6
 
7
7
  How to work:
8
8
  - Be precise and terse. Use lists or tables for more than two records and always include identifiers.
@@ -11,3 +11,5 @@ How to work:
11
11
  - For bulk changes, confirm the scope (filters and count) before starting.
12
12
  - After changes, report counts of what succeeded and list anything that failed with the reason.
13
13
  - Show personal data only when the task needs it.`
14
+
15
+ export default presetPrompt
@@ -1,5 +1,9 @@
1
- /* Styles for agent-chat.tsx. Copied by `dopus init`; yours to edit. Theme with the --dopus-* variables. */
2
- .chat {
1
+ /*
2
+ * Styles for agent-chat.tsx. Copied by `dopus init`; yours to edit. Theme with the --dopus-* variables.
3
+ * Plain global CSS (not a CSS Module) so it works in every setup, including Tailwind's Turbopack loader;
4
+ * every class starts with `dopus-`, so it does not clash with your own.
5
+ */
6
+ .dopus-chat {
3
7
  --dopus-bg: #ffffff;
4
8
  --dopus-fg: #18181b;
5
9
  --dopus-muted: #71717a;
@@ -27,7 +31,7 @@
27
31
  }
28
32
 
29
33
  @media (prefers-color-scheme: dark) {
30
- .chat {
34
+ .dopus-chat {
31
35
  --dopus-bg: #18181b;
32
36
  --dopus-fg: #f4f4f5;
33
37
  --dopus-muted: #a1a1aa;
@@ -39,13 +43,13 @@
39
43
  }
40
44
  }
41
45
 
42
- .header {
46
+ .dopus-header {
43
47
  padding: 12px 16px;
44
48
  font-weight: 600;
45
49
  border-bottom: 1px solid var(--dopus-border);
46
50
  }
47
51
 
48
- .messages {
52
+ .dopus-messages {
49
53
  flex: 1;
50
54
  overflow-y: auto;
51
55
  padding: 16px;
@@ -54,31 +58,31 @@
54
58
  gap: 12px;
55
59
  }
56
60
 
57
- .message {
61
+ .dopus-message {
58
62
  display: flex;
59
63
  flex-direction: column;
60
64
  gap: 6px;
61
65
  max-width: 85%;
62
66
  }
63
67
 
64
- .message[data-role="user"] {
68
+ .dopus-message[data-role="user"] {
65
69
  align-self: flex-end;
66
70
  align-items: flex-end;
67
71
  }
68
72
 
69
- .text {
73
+ .dopus-text {
70
74
  white-space: pre-wrap;
71
75
  overflow-wrap: anywhere;
72
76
  }
73
77
 
74
- .message[data-role="user"] .text {
78
+ .dopus-message[data-role="user"] .dopus-text {
75
79
  background: var(--dopus-accent);
76
80
  color: var(--dopus-accent-fg);
77
81
  padding: 8px 12px;
78
82
  border-radius: var(--dopus-radius);
79
83
  }
80
84
 
81
- .tool {
85
+ .dopus-tool {
82
86
  font-size: 13px;
83
87
  color: var(--dopus-muted);
84
88
  background: var(--dopus-surface);
@@ -86,15 +90,15 @@
86
90
  padding: 6px 10px;
87
91
  }
88
92
 
89
- .tool summary {
93
+ .dopus-tool summary {
90
94
  cursor: pointer;
91
95
  }
92
96
 
93
- .tool[data-state="error"] {
97
+ .dopus-tool[data-state="error"] {
94
98
  color: var(--dopus-danger);
95
99
  }
96
100
 
97
- .approval {
101
+ .dopus-approval {
98
102
  margin: 0;
99
103
  min-inline-size: 0;
100
104
  border: 1px solid var(--dopus-border);
@@ -105,19 +109,19 @@
105
109
  gap: 8px;
106
110
  }
107
111
 
108
- .approvalTitle {
112
+ .dopus-approval-title {
109
113
  float: left;
110
114
  width: 100%;
111
115
  padding: 0;
112
116
  font-weight: 600;
113
117
  }
114
118
 
115
- .approvalActions {
119
+ .dopus-approval-actions {
116
120
  display: flex;
117
121
  gap: 8px;
118
122
  }
119
123
 
120
- .json {
124
+ .dopus-json {
121
125
  margin: 0;
122
126
  font:
123
127
  12px / 1.4 ui-monospace,
@@ -130,21 +134,21 @@
130
134
  overflow: auto;
131
135
  }
132
136
 
133
- .thinking {
137
+ .dopus-thinking {
134
138
  color: var(--dopus-muted);
135
139
  }
136
140
 
137
- .error {
141
+ .dopus-error {
138
142
  color: var(--dopus-danger);
139
143
  }
140
144
 
141
- .suggestions {
145
+ .dopus-suggestions {
142
146
  display: flex;
143
147
  flex-wrap: wrap;
144
148
  gap: 8px;
145
149
  }
146
150
 
147
- .suggestion {
151
+ .dopus-suggestion {
148
152
  border: 1px solid var(--dopus-border);
149
153
  background: transparent;
150
154
  color: inherit;
@@ -154,14 +158,14 @@
154
158
  font: inherit;
155
159
  }
156
160
 
157
- .form {
161
+ .dopus-form {
158
162
  display: flex;
159
163
  gap: 8px;
160
164
  padding: 12px;
161
165
  border-top: 1px solid var(--dopus-border);
162
166
  }
163
167
 
164
- .input {
168
+ .dopus-input {
165
169
  flex: 1;
166
170
  resize: none;
167
171
  border: 1px solid var(--dopus-border);
@@ -173,32 +177,32 @@
173
177
  max-height: 160px;
174
178
  }
175
179
 
176
- .primary,
177
- .secondary {
180
+ .dopus-primary,
181
+ .dopus-secondary {
178
182
  border-radius: 8px;
179
183
  padding: 8px 14px;
180
184
  font: inherit;
181
185
  cursor: pointer;
182
186
  }
183
187
 
184
- .primary {
188
+ .dopus-primary {
185
189
  background: var(--dopus-accent);
186
190
  color: var(--dopus-accent-fg);
187
191
  border: 1px solid var(--dopus-accent);
188
192
  }
189
193
 
190
- .primary:disabled {
194
+ .dopus-primary:disabled {
191
195
  opacity: 0.4;
192
196
  cursor: default;
193
197
  }
194
198
 
195
- .secondary {
199
+ .dopus-secondary {
196
200
  background: transparent;
197
201
  color: inherit;
198
202
  border: 1px solid var(--dopus-border);
199
203
  }
200
204
 
201
- .link {
205
+ .dopus-link {
202
206
  background: none;
203
207
  border: 0;
204
208
  padding: 0;
@@ -208,7 +212,7 @@
208
212
  font: inherit;
209
213
  }
210
214
 
211
- .spinner {
215
+ .dopus-spinner {
212
216
  display: inline-block;
213
217
  width: 10px;
214
218
  height: 10px;
@@ -225,7 +229,7 @@
225
229
  }
226
230
  }
227
231
 
228
- .widget {
232
+ .dopus-widget {
229
233
  position: fixed;
230
234
  right: 20px;
231
235
  bottom: 20px;
@@ -236,13 +240,13 @@
236
240
  gap: 12px;
237
241
  }
238
242
 
239
- .panel {
243
+ .dopus-panel {
240
244
  width: min(400px, calc(100vw - 40px));
241
245
  height: min(600px, calc(100vh - 120px));
242
246
  box-shadow: 0 12px 40px rgb(0 0 0 / 0.18);
243
247
  }
244
248
 
245
- .launcher {
249
+ .dopus-launcher {
246
250
  width: 52px;
247
251
  height: 52px;
248
252
  border-radius: 50%;
@@ -2,7 +2,8 @@
2
2
 
3
3
  /**
4
4
  * Chat UI for the dopus agent. Copied by `dopus init`; this file is yours to edit.
5
- * Talks to POST /api/agent with the AI SDK `useChat` protocol. Styles: ./agent-chat.module.css
5
+ * Talks to POST /api/agent with the AI SDK `useChat` protocol.
6
+ * Styles: ./agent-chat.css — plain global CSS with `dopus-` class names (works with or without Tailwind).
6
7
  */
7
8
  import { useChat } from '@ai-sdk/react'
8
9
  import {
@@ -12,8 +13,9 @@ import {
12
13
  lastAssistantMessageIsCompleteWithApprovalResponses,
13
14
  type UIMessage,
14
15
  } from 'ai'
16
+ import { useRouter } from 'next/navigation'
15
17
  import { type FormEvent, type KeyboardEvent, useEffect, useRef, useState } from 'react'
16
- import styles from './agent-chat.module.css'
18
+ import './agent-chat.css'
17
19
 
18
20
  const DEFAULT_LABELS = {
19
21
  title: 'Assistant',
@@ -58,7 +60,7 @@ function humanize(name: string) {
58
60
  }
59
61
 
60
62
  function Json({ value }: { value: unknown }) {
61
- return <pre className={styles.json}>{JSON.stringify(value, null, 2)}</pre>
63
+ return <pre className="dopus-json">{JSON.stringify(value, null, 2)}</pre>
62
64
  }
63
65
 
64
66
  function ToolCall({
@@ -76,28 +78,28 @@ function ToolCall({
76
78
  case 'input-streaming':
77
79
  case 'input-available':
78
80
  return (
79
- <div className={styles.tool} data-state="running">
80
- <span className={styles.spinner} aria-hidden /> {labels.running}: {name}
81
+ <div className="dopus-tool" data-state="running">
82
+ <span className="dopus-spinner" aria-hidden /> {labels.running}: {name}
81
83
  </div>
82
84
  )
83
85
  case 'approval-requested':
84
86
  return (
85
- <fieldset className={styles.approval}>
86
- <legend className={styles.approvalTitle}>
87
+ <fieldset className="dopus-approval">
88
+ <legend className="dopus-approval-title">
87
89
  {name} · {labels.needsApproval}
88
90
  </legend>
89
91
  <Json value={part.input} />
90
- <div className={styles.approvalActions}>
92
+ <div className="dopus-approval-actions">
91
93
  <button
92
94
  type="button"
93
- className={styles.primary}
95
+ className="dopus-primary"
94
96
  onClick={() => onRespond(part.approval.id, true)}
95
97
  >
96
98
  {labels.approve}
97
99
  </button>
98
100
  <button
99
101
  type="button"
100
- className={styles.secondary}
102
+ className="dopus-secondary"
101
103
  onClick={() => onRespond(part.approval.id, false)}
102
104
  >
103
105
  {labels.decline}
@@ -107,26 +109,26 @@ function ToolCall({
107
109
  )
108
110
  case 'approval-responded':
109
111
  return (
110
- <div className={styles.tool} data-state={part.approval.approved ? 'running' : 'declined'}>
112
+ <div className="dopus-tool" data-state={part.approval.approved ? 'running' : 'declined'}>
111
113
  {part.approval.approved ? `${labels.approved}: ${name}` : `${labels.declined}: ${name}`}
112
114
  </div>
113
115
  )
114
116
  case 'output-available':
115
117
  return (
116
- <details className={styles.tool} data-state="done">
118
+ <details className="dopus-tool" data-state="done">
117
119
  <summary>✓ {name}</summary>
118
120
  <Json value={part.output} />
119
121
  </details>
120
122
  )
121
123
  case 'output-error':
122
124
  return (
123
- <div className={styles.tool} data-state="error">
125
+ <div className="dopus-tool" data-state="error">
124
126
  ✗ {name} · {labels.failed}
125
127
  </div>
126
128
  )
127
129
  case 'output-denied':
128
130
  return (
129
- <div className={styles.tool} data-state="declined">
131
+ <div className="dopus-tool" data-state="declined">
130
132
  {labels.declined}: {name}
131
133
  </div>
132
134
  )
@@ -145,12 +147,12 @@ function Message({
145
147
  onRespond: (approvalId: string, approved: boolean) => void
146
148
  }) {
147
149
  return (
148
- <div className={styles.message} data-role={message.role}>
150
+ <div className="dopus-message" data-role={message.role}>
149
151
  {message.parts.map((part, index) => {
150
152
  const key = `${message.id}-${index}`
151
153
  if (part.type === 'text') {
152
154
  return part.text ? (
153
- <div key={key} className={styles.text}>
155
+ <div key={key} className="dopus-text">
154
156
  {part.text}
155
157
  </div>
156
158
  ) : null
@@ -171,6 +173,7 @@ export function AgentChat({
171
173
  className,
172
174
  }: AgentChatProps) {
173
175
  const labels = { ...DEFAULT_LABELS, ...labelOverrides }
176
+ const router = useRouter()
174
177
  const [transport] = useState(
175
178
  () =>
176
179
  new DefaultChatTransport({
@@ -183,6 +186,13 @@ export function AgentChat({
183
186
  useChat({
184
187
  transport,
185
188
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
189
+ // A tool ran, so data the page shows may have changed (server actions call revalidatePath):
190
+ // re-render the current route's server components.
191
+ onFinish: ({ message }) => {
192
+ if (message.parts.some((part) => isToolUIPart(part) && part.state === 'output-available')) {
193
+ router.refresh()
194
+ }
195
+ },
186
196
  })
187
197
  const [input, setInput] = useState('')
188
198
  const listRef = useRef<HTMLDivElement>(null)
@@ -217,18 +227,18 @@ export function AgentChat({
217
227
 
218
228
  return (
219
229
  <section
220
- className={[styles.chat, className].filter(Boolean).join(' ')}
230
+ className={['dopus-chat', className].filter(Boolean).join(' ')}
221
231
  aria-label={labels.title}
222
232
  >
223
- <header className={styles.header}>{labels.title}</header>
224
- <div ref={listRef} className={styles.messages} aria-live="polite">
233
+ <header className="dopus-header">{labels.title}</header>
234
+ <div ref={listRef} className="dopus-messages" aria-live="polite">
225
235
  {messages.length === 0 && suggestions.length > 0 && (
226
- <div className={styles.suggestions}>
236
+ <div className="dopus-suggestions">
227
237
  {suggestions.map((text) => (
228
238
  <button
229
239
  key={text}
230
240
  type="button"
231
- className={styles.suggestion}
241
+ className="dopus-suggestion"
232
242
  onClick={() => send(text)}
233
243
  >
234
244
  {text}
@@ -239,19 +249,19 @@ export function AgentChat({
239
249
  {messages.map((message) => (
240
250
  <Message key={message.id} message={message} labels={labels} onRespond={respond} />
241
251
  ))}
242
- {status === 'submitted' && <div className={styles.thinking}>{labels.thinking}</div>}
252
+ {status === 'submitted' && <div className="dopus-thinking">{labels.thinking}</div>}
243
253
  {error && (
244
- <div className={styles.error} role="alert">
254
+ <div className="dopus-error" role="alert">
245
255
  {labels.error}{' '}
246
- <button type="button" className={styles.link} onClick={() => regenerate()}>
256
+ <button type="button" className="dopus-link" onClick={() => regenerate()}>
247
257
  {labels.retry}
248
258
  </button>
249
259
  </div>
250
260
  )}
251
261
  </div>
252
- <form className={styles.form} onSubmit={onSubmit}>
262
+ <form className="dopus-form" onSubmit={onSubmit}>
253
263
  <textarea
254
- className={styles.input}
264
+ className="dopus-input"
255
265
  value={input}
256
266
  onChange={(event) => setInput(event.target.value)}
257
267
  onKeyDown={onKeyDown}
@@ -260,11 +270,11 @@ export function AgentChat({
260
270
  aria-label={labels.placeholder}
261
271
  />
262
272
  {busy ? (
263
- <button type="button" className={styles.secondary} onClick={() => stop()}>
273
+ <button type="button" className="dopus-secondary" onClick={() => stop()}>
264
274
  {labels.stop}
265
275
  </button>
266
276
  ) : (
267
- <button type="submit" className={styles.primary} disabled={!input.trim()}>
277
+ <button type="submit" className="dopus-primary" disabled={!input.trim()}>
268
278
  {labels.send}
269
279
  </button>
270
280
  )}
@@ -278,11 +288,14 @@ export function AgentWidget(props: AgentChatProps) {
278
288
  const [open, setOpen] = useState(false)
279
289
  const labels = { ...DEFAULT_LABELS, ...props.labels }
280
290
  return (
281
- <div className={styles.widget}>
282
- {open && <AgentChat {...props} className={styles.panel} />}
291
+ <div className="dopus-widget">
292
+ {/* Hidden rather than unmounted, so closing the panel keeps the conversation. */}
293
+ <div hidden={!open}>
294
+ <AgentChat {...props} className="dopus-panel" />
295
+ </div>
283
296
  <button
284
297
  type="button"
285
- className={styles.launcher}
298
+ className="dopus-launcher"
286
299
  aria-expanded={open}
287
300
  aria-label={open ? labels.close : labels.open}
288
301
  onClick={() => setOpen((value) => !value)}