@dudousxd/nestjs-agent-react 0.10.0 → 0.12.0

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
@@ -112,7 +112,7 @@ function Chat() {
112
112
  |---|---|
113
113
  | `items` | The mounted window, oldest first. Each item carries `blocks`, `text`, `usage`, `timestamp`, `isStreaming`, `isLastAssistant`, and its action machines. |
114
114
  | `items[i].blocks` | `{ kind: 'text' \| 'reasoning' \| 'tools' \| 'sources' \| 'elicitation' \| 'ui' }`. A `ui` block is a component the server pushed (`{ id, component, props, version }`) — look `component` up in your own registry. A `tools` block is a run of CONSECUTIVE tool parts — any other part between two calls (including a `step-start` marker) ends the run. A `reasoning` block carries `isOpen`/`toggle`, open while it streams. A `sources` block appears only under `sources: true`, an `elicitation` block only under `onAnswer` — see below. |
115
- | `items[i].blocks[n]` (`tools`) | Also carries `calls`: the same parts, each with `{ toolCallId, name, toolKind, parentId, children, approval, isAwaitingApproval, approve, reject, error }`, and `roots`: the same calls as a tree (a call nested under another by the stream's `parentId` sits in its parent's `children`). `approval` is `{ approver, expiresAt, reason }` when the runner said who has to decide, else `null`. |
115
+ | `items[i].blocks[n]` (`tools`) | Also carries `calls`: the same parts, each with `{ toolCallId, name, toolKind, parentId, children, approval, isAwaitingApproval, approve, reject, error }`, and `roots`: the same calls as a tree (a call nested under another by the stream's `parentId` sits in its parent's `children`). `approval` is `{ approver, expiresAt, reason, status, remember, decidedBy, decidedVia, decisionReason }` when the runner said who has to decide, else `null` — `status` is `pending` / `approved` / `rejected` / `expired`. |
116
116
  | `items[i].copy` | `{ available, copied, copy() }` — `copied` flashes for `copyResetMs` (default 1500). |
117
117
  | `items[i].edit` | `{ available, isEditing, draft, canSave, start(), cancel(), setDraft(), save(), getTextareaProps() }`. The prop-getter focuses with the caret at the end, saves on Enter, cancels on Escape. |
118
118
  | `items[i].fork` / `.regenerate` | `{ available, run() }`. Regenerate is offered on the last assistant message only. |
@@ -186,12 +186,32 @@ const transcript = useChatTranscript({
186
186
  });
187
187
 
188
188
  // block.kind === 'elicitation' →
189
- // { toolCallId, preamble, questionCount, isPending, outcome, error,
190
- // questions: [{ id, prompt, position, multiple, selected, isPristine,
189
+ // { toolCallId, preamble, questionCount, isPending, isValid, outcome, error,
190
+ // questions: [{ id, prompt, description, input, position, multiple, selected, value, error,
191
+ // isPristine, setValue(raw),
191
192
  // options: [{ value, label, hotkey, isSelected, isDefault, select() }] }],
192
193
  // answer: { available, isSubmitting, run() }, skip: { … } }
193
194
  ```
194
195
 
196
+ **Typed questions.** A question with `input` asks for a value instead of a pick — `input.type` is
197
+ `text` · `textarea` · `number` · `boolean` · `date` · `email` · `url` · `select` (a pick from
198
+ `options` again), with optional `placeholder`, `required`, `min`, `max` and `pattern`. `options` may
199
+ be empty for one. Render the control from `input.type` and hand whatever it produces to
200
+ `question.setValue(raw)`: it is coerced with `coerceAnswer` to the canonical strings the wire
201
+ carries (numbers as decimals, booleans as `"true"`/`"false"`, dates as `YYYY-MM-DD`). `question.error`
202
+ is the server's own verdict on the current selection (`validateAnswer`), and `block.isValid` is all
203
+ of them — the answer route answers `400` with the same message for anything it would refuse.
204
+
205
+ ```tsx
206
+ {q.input?.type === 'number' ? (
207
+ <input type="number" value={q.value} placeholder={q.input.placeholder}
208
+ min={q.input.min} max={q.input.max} onChange={(e) => q.setValue(e.target.value)} />
209
+ ) : q.input?.type === 'boolean' ? (
210
+ <input type="checkbox" checked={q.value === 'true'} onChange={(e) => q.setValue(e.target.checked)} />
211
+ ) : null}
212
+ {q.error && !q.isPristine ? <small role="alert">{q.error}</small> : null}
213
+ ```
214
+
195
215
  Every option the agent pre-picked comes back as `isDefault` and starts `isSelected`, so confirming
196
216
  is enough. `answer.run()` submits **only the questions the user touched** — the rest are omitted on
197
217
  purpose, so the server applies the same defaults it showed and the settled row still records which
@@ -223,7 +243,20 @@ An `action` tool's input lands and its output never follows on its own — the l
223
243
  between the two — so a call stuck at `input-available` IS the pending approval. A question set parks
224
244
  the same way and is deliberately excluded: approving one settles nothing.
225
245
 
226
- A refused settlement (403 "not your thread", a network failure) lands on `block.error` /
246
+ `call.approve.run({ remember: true })` approves this tool for the rest of the thread (the server
247
+ stops asking); `call.approval` says who has to decide and, once settled, who did and through what.
248
+ For a request with an expiry, `useApprovalCountdown(call.approval?.expiresAt)` ticks the time left
249
+ (`{ remainingMs, isExpired }`, headless — pair it with `formatElapsed`):
250
+
251
+ ```tsx
252
+ function ApprovalDeadline({ expiresAt }: { expiresAt: string | null }) {
253
+ const { remainingMs, isExpired } = useApprovalCountdown(expiresAt);
254
+ if (remainingMs === null) return null;
255
+ return <span>{isExpired ? 'Expired' : `${formatElapsed(remainingMs)} left`}</span>;
256
+ }
257
+ ```
258
+
259
+ A refused settlement (403 "not your thread" or "not your approval", 410 "expired", a network failure) lands on `block.error` /
227
260
  `call.error` with the affordance still live, rather than escaping as an unhandled rejection. Render
228
261
  it — a button that silently does nothing is indistinguishable from a broken one.
229
262
 
@@ -420,6 +453,7 @@ data parts, which `useChat`'s `onData` (and `useAgentChat({ onData })`) sees as
420
453
  |---|---|
421
454
  | `ui` | a `data-ui` part keyed by the component id (a repeat id updates it in place) |
422
455
  | `approval-requested` | a `data-approval-requested` part keyed by the call id, plus the SDK's native approval request — the tool part moves to `state: 'approval-requested'` |
456
+ | `approval-settled` | a `data-approval-settled` part keyed by the call id — who decided, through what, remembered or not; folded into `call.approval` |
423
457
  | `title` / `cancelled` | transient `data-title` / `data-cancelled` (never stored on the message); `useAgentChat({ onTitle })` |
424
458
  | a kind this version does not know | a `data-<kind>` part — forwarded, never dropped |
425
459