makersclaw 0.8.1 → 0.8.3

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 (102) hide show
  1. package/package.json +1 -1
  2. package/r/activity-log.json +3 -3
  3. package/r/agent-changes.json +3 -3
  4. package/r/agent-chat.json +3 -3
  5. package/r/agent-column.json +3 -3
  6. package/r/agent-dock.json +3 -3
  7. package/r/agent-kit.json +2 -2
  8. package/r/agent-marks.json +2 -2
  9. package/r/agent-pane.json +6 -6
  10. package/r/ai-action.json +4 -4
  11. package/r/annotate.json +3 -3
  12. package/r/approval-request.json +2 -2
  13. package/r/approval.json +3 -3
  14. package/r/ask-bar.json +2 -2
  15. package/r/audio-player.json +2 -2
  16. package/r/auth-form.json +2 -2
  17. package/r/automation-editor.json +7 -7
  18. package/r/automation-list.json +3 -3
  19. package/r/automation-model.json +2 -2
  20. package/r/automations-screen.json +7 -7
  21. package/r/board.json +2 -2
  22. package/r/calendar-view.json +3 -3
  23. package/r/cart.json +2 -2
  24. package/r/chart.json +2 -2
  25. package/r/chat-pane.json +2 -2
  26. package/r/chat.json +2 -2
  27. package/r/code-editor.json +2 -2
  28. package/r/comment-box.json +3 -3
  29. package/r/connect-card.json +2 -2
  30. package/r/contact-section.json +2 -2
  31. package/r/cost.json +2 -2
  32. package/r/csv-import.json +3 -3
  33. package/r/data-picker.json +3 -3
  34. package/r/data-table.json +2 -2
  35. package/r/diagram.json +2 -2
  36. package/r/doc-editor.json +2 -2
  37. package/r/doc-page.json +3 -3
  38. package/r/email-render.json +2 -2
  39. package/r/faq.json +2 -2
  40. package/r/file-upload.json +2 -2
  41. package/r/first-run.json +2 -2
  42. package/r/fix-note.json +4 -4
  43. package/r/flow-canvas.json +2 -2
  44. package/r/flow-editor.json +4 -4
  45. package/r/footer.json +2 -2
  46. package/r/gantt.json +3 -3
  47. package/r/job-panel.json +2 -2
  48. package/r/json-editor.json +3 -3
  49. package/r/library-shelf.json +2 -2
  50. package/r/map.json +2 -2
  51. package/r/margin-notes.json +3 -3
  52. package/r/menubar.json +2 -2
  53. package/r/mind-map.json +3 -3
  54. package/r/navigation-menu.json +2 -2
  55. package/r/needs-you.json +2 -2
  56. package/r/order-detail.json +2 -2
  57. package/r/org-chart.json +3 -3
  58. package/r/pace-bar.json +3 -3
  59. package/r/page-builder.json +2 -2
  60. package/r/payment-methods.json +2 -2
  61. package/r/pdf-document.json +2 -2
  62. package/r/pdf-viewer.json +2 -2
  63. package/r/product-detail.json +2 -2
  64. package/r/product-grid.json +3 -3
  65. package/r/record-form.json +2 -2
  66. package/r/record-grid.json +2 -2
  67. package/r/record-notes.json +4 -4
  68. package/r/registry.json +249 -249
  69. package/r/run-history.json +2 -2
  70. package/r/run-view.json +6 -6
  71. package/r/schedule-card.json +2 -2
  72. package/r/schedule-engine.json +2 -2
  73. package/r/schedule-gantt.json +3 -3
  74. package/r/schedule-ruler.json +2 -2
  75. package/r/search-dialog.json +2 -2
  76. package/r/settings-list.json +2 -2
  77. package/r/setup-checklist.json +2 -2
  78. package/r/share-dialog.json +2 -2
  79. package/r/sidebar.json +2 -2
  80. package/r/signature-pad.json +2 -2
  81. package/r/sketch-canvas.json +2 -2
  82. package/r/source-receipt.json +3 -3
  83. package/r/spreadsheet.json +2 -2
  84. package/r/stat-cards.json +3 -3
  85. package/r/step-picker.json +3 -3
  86. package/r/step-settings.json +5 -5
  87. package/r/suggestion.json +2 -2
  88. package/r/sum-bar.json +2 -2
  89. package/r/task-list.json +3 -3
  90. package/r/task-model.json +2 -2
  91. package/r/testimonials.json +2 -2
  92. package/r/tier-map.json +2 -2
  93. package/r/time-grid.json +2 -2
  94. package/r/time-strip.json +2 -2
  95. package/r/top-bar.json +2 -2
  96. package/r/transaction-list.json +2 -2
  97. package/r/trigger-picker.json +3 -3
  98. package/r/whiteboard.json +3 -3
  99. package/r/workflow-view.json +3 -3
  100. package/registry/blocks/agent-pane/README.md +16 -0
  101. package/registry/blocks/agent-pane/agent-pane.jsx +26 -19
  102. package/templates/next/files/lib/hooks.js +55 -5
@@ -5,7 +5,7 @@
5
5
  "title": "TransactionList",
6
6
  "description": "Money records as a plain list, in two variants: history (payments in and out, grouped by month, a received / paid / pending / failed mark and a signed amount) and invoices (number, date, a Paid / Due in N days / N days overdue badge, amount, download). Optional date-range filter, Export and per-row download. Empty, loading and error states built in.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.3/r/core.json"
9
9
  ],
10
10
  "dependencies": [],
11
11
  "files": [
@@ -46,7 +46,7 @@
46
46
  "docs": "Installed to components/makersclaw/blocks/transaction-list. Import: import { TransactionList } from \"@/components/makersclaw/blocks/transaction-list\". Props and an example: components/makersclaw/blocks/transaction-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
47
47
  "meta": {
48
48
  "makersclaw": {
49
- "version": "0.8.1",
49
+ "version": "0.8.3",
50
50
  "useWhen": "A billing page's invoices, an account's payment history, a wallet's recent activity: records read top to bottom, newest first. Not for records the founder sorts, filters by column or totals (data-table), or for one order (order-detail)."
51
51
  }
52
52
  }
@@ -5,8 +5,8 @@
5
5
  "title": "TriggerPicker",
6
6
  "description": "What starts an automation, as a panel: by hand, on a schedule in the team's time zone, when an app sends an event, when one of an app's actions finishes, when a record is added or changed, when the automation's own web address is called (its secret made and replaced here, shown once), or when another automation finishes. The platform says which of these it runs (the list door's triggers); a connection's own events are listed as \"soon\" with what to use meanwhile. The schedule is offered in the shapes people ask for (every day, every weekday, every week, every hour, another schedule as five-field cron) with a time of day, a weekday, the team's zone or another, and a sentence saying it back (\"Runs weekdays at 07:00, team time\").",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/automation-model.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.3/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.3/r/automation-model.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -54,7 +54,7 @@
54
54
  "docs": "Installed to components/makersclaw/blocks/trigger-picker. Import: import { TriggerPicker } from \"@/components/makersclaw/blocks/trigger-picker\". Props and an example: components/makersclaw/blocks/trigger-picker/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
55
55
  "meta": {
56
56
  "makersclaw": {
57
- "version": "0.8.1",
57
+ "version": "0.8.3",
58
58
  "useWhen": "An automation's trigger is edited in your own screen. automation-editor opens it when the trigger card is pressed; pass it the platform's triggers (useAutomations().triggers) so it offers what the platform runs."
59
59
  }
60
60
  }
package/r/whiteboard.json CHANGED
@@ -5,8 +5,8 @@
5
5
  "title": "Whiteboard",
6
6
  "description": "A full-page freeform whiteboard on sketch-canvas: a top bar with the board's title (click to rename), the save state (Saving…, Saved, or the error with Retry), a Board / Text view switch, Export PNG and an optional Share button; under it the Miro-style canvas (tool rail, floating toolbar, zoom, undo). An empty board offers starter templates — blank, a grid of titled areas, a 2×2 matrix — and the Text view lists every word on the board for a screen reader or an agent. With onCreateTask, a selected note or text becomes a task from the top bar (\"Create task\", editable title; several notes make one task each), and a small badge marks it on the board from then on. The sketch is plain JSON; saves are async callbacks answering { ok }.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/sketch-canvas.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.3/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.3/r/sketch-canvas.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -59,7 +59,7 @@
59
59
  "docs": "Installed to components/makersclaw/blocks/whiteboard. Import: import { Whiteboard, WHITEBOARD_TEMPLATES, gridSketch, matrixSketch } from \"@/components/makersclaw/blocks/whiteboard\". Props and an example: components/makersclaw/blocks/whiteboard/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
60
  "meta": {
61
61
  "makersclaw": {
62
- "version": "0.8.1",
62
+ "version": "0.8.3",
63
63
  "useWhen": "A screen is one board to think on: a team whiteboard, a planning or prioritisation board, a place to sketch an idea and keep it with a record. Not for marking up a screenshot (annotate), sketching a UI to build (sketch-to-app), a structured board of cards and connectors (flow-canvas), or a kanban (board)."
64
64
  }
65
65
  }
@@ -5,8 +5,8 @@
5
5
  "title": "WorkflowView",
6
6
  "description": "One job of the app's agent drawn as a graph on a dotted ground: the Trigger (\"Every weekday at 07:00\", \"When a deal is created\") → each step of the chosen run with how it went, as a mark and a word → the result. Over it a run picker with Re-run; in its head the Live switch and \"Trigger manually\" with its price; beside it the run history and the overview (run-history). Before any run it draws the job's declared steps in pencil, with an honest empty line. Fed by useJobs, useRuns and useRun (lib/hooks.js). Under about 768 px of its own width the history stacks under the graph and the graph runs top to bottom.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/run-history.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.3/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.3/r/run-history.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -48,7 +48,7 @@
48
48
  "docs": "Installed to components/makersclaw/blocks/workflow-view. Import: import { WorkflowView } from \"@/components/makersclaw/blocks/workflow-view\". Props and an example: components/makersclaw/blocks/workflow-view/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
49
49
  "meta": {
50
50
  "makersclaw": {
51
- "version": "0.8.1",
51
+ "version": "0.8.3",
52
52
  "useWhen": "Superseded by automation-editor and run-view for an app whose jobs run on the automations engine; still the block for an app that has not moved (its jobs are the agent's wakes). A job's own page: the founder opens an automation to see what it does, how its last runs went and to run it now — a morning read, an inbox sweep, a nightly enrichment, an event-triggered workflow. For the job as a card beside the day's work use schedule-card; for a plain list of runs use run-history."
53
53
  }
54
54
  }
@@ -64,6 +64,22 @@ Only a plan or a piece of one, a change the agent sent up, a counted job
64
64
  and a hand-off that never started get a new request; for those
65
65
  `chat.sameConversation` is false and the box keeps the app's own words.
66
66
 
67
+ ## Progress feedback
68
+
69
+ The transcript owns progress feedback: one “Thinking…” line while no live
70
+ text or specific tool work is moving, or an explicit queued line while the
71
+ ask waits. A pending follow-up is never labelled “Starting…”. The composer
72
+ keeps Stop available while the turn runs; there is no second working footer.
73
+
74
+ ## Questions keep the conversation open
75
+
76
+ While a question waits, the main composer stays available. Sending there
77
+ skips that question and continues the same thread, including attachments.
78
+ Answering the question removes it immediately while the answer is sent; a
79
+ failed submission restores it for retry. Stale thread reads cannot bring an
80
+ accepted question back, but a new question still appears. Approval, account
81
+ connection and credit-limit gates keep their existing controls.
82
+
67
83
  ## Tell the desk
68
84
 
69
85
  An app that places this block says so in its package: `"agent_pane": true`
@@ -38,10 +38,20 @@ import { MemberWords, Message } from "./transcript";
38
38
  // draws an Approve of its own. A sign-in, money, or a tool approval says
39
39
  // so and opens the desk.
40
40
 
41
- const STATE_LINE = {
42
- queued: "Queued — it starts when the one before it ends",
43
- working: "Working on it",
44
- };
41
+ /** Specific work or streamed words already tell the member what is moving. */
42
+ function showsProgress(message) {
43
+ if (message.role !== "assistant" || !message.live) return false;
44
+ return (message.blocks ?? []).some((block) => {
45
+ if (block.kind === "text") return block.streaming && Boolean(block.text?.trim());
46
+ if (block.kind === "plan") return block.items?.some((item) => item.state === "active");
47
+ if (block.kind !== "work") return false;
48
+ return (block.runs ?? []).some((run) =>
49
+ run.kind === "fold" ? run.summary?.running :
50
+ run.kind === "helpers" ? run.calls?.some((call) => call.status === "running") :
51
+ run.kind === "thought" ? run.streaming : run.status === "running",
52
+ );
53
+ });
54
+ }
45
55
 
46
56
  /** What waits on the member, as the bar under the thread says it. */
47
57
  function needsTitle(kind, inApp) {
@@ -123,22 +133,24 @@ export function AgentPane({
123
133
  const inMakersclaw = chat?.place === "makersclaw";
124
134
  const questions = view?.questions ?? [];
125
135
  const result = chat?.request?.status === "done" ? chat.request?.result?.sentence : null;
126
- // The thinking row: the turn runs and nothing visibly grows yet (the
127
- // platform says when, in the view); words just sent wait for their turn.
136
+ // One generic progress row, only while nothing specific visibly moves.
137
+ // Pending words are a send, not proof that a worker is starting.
128
138
  const lastLegacy = legacy[legacy.length - 1];
129
- const starting = Boolean(pending) && !messages.some((m) => m.role === "assistant" && m.live);
139
+ const visibleProgress = !pending && (view ? messages.some(showsProgress) : Boolean(lastLegacy?.writing || lastLegacy?.parts?.some((part) => part.type === "step" && part.status === "running")));
140
+ const queued = chat?.state === "queued";
130
141
  const thinking =
131
- !needs && !inMakersclaw && (pending ? true : view ? view.thinking : live && !(lastLegacy?.writing || lastLegacy?.parts?.length));
142
+ live && !needs && !inMakersclaw && (queued || !visibleProgress);
132
143
 
133
144
  const openThing = onOpen ?? (typeof chat?.open === "function" ? chat.open : undefined);
134
145
  const answerHere = needs?.in_app === "answer" && typeof chat?.answer === "function";
135
146
  const pressHere = needs?.in_app === "press" && yes != null && yes !== false;
136
- const blocked = Boolean(needs) || inMakersclaw;
147
+ // A message during a question skips it and continues the same thread.
148
+ // Approvals, account connections and credit limits keep their own gate.
149
+ const blocked = (Boolean(needs) && needs.kind !== "question") || inMakersclaw;
137
150
  const closedWords = needs ? (answerHere ? "Answer above first" : pressHere ? "Say yes above first" : "Answer on the desk first") : "This ask is in Makersclaw";
138
151
 
139
152
  // What sits between the lines and the box: what waits on the member, or
140
- // that the agent works (its Stop is the box's, in the arrow's place), or
141
- // how the ask ended.
153
+ // how the ask ended. Progress is in the transcript; Stop is the box's.
142
154
  const bar =
143
155
  needs && pressHere ? (
144
156
  <div className="max-h-[70%] shrink-0 overflow-y-auto border-t border-border p-2.5">{yes}</div>
@@ -167,12 +179,7 @@ export function AgentPane({
167
179
  )}
168
180
  </div>
169
181
  </div>
170
- ) : live && !inMakersclaw ? (
171
- <div className="flex shrink-0 items-center gap-2 border-t border-border px-3.5 py-1.5 text-xs text-muted-foreground">
172
- <Spinner size="sm" label="Working" />
173
- <span className="min-w-0 flex-1 truncate">{STATE_LINE[chat?.state] ?? STATE_LINE.working}</span>
174
- </div>
175
- ) : result ? (
182
+ ) : !live && result ? (
176
183
  <p className="flex shrink-0 items-start gap-2 border-t border-border px-3.5 py-1.5 text-xs text-muted-foreground">
177
184
  <Icon name="circle-check" size={14} className="mt-px shrink-0 text-success" />
178
185
  <span className="min-w-0 break-words">{result}</span>
@@ -220,8 +227,8 @@ export function AgentPane({
220
227
  ))}
221
228
  {pending ? <MemberWords>{pending}</MemberWords> : null}
222
229
  {thinking ? (
223
- <div role="status" className="flex items-center gap-2 text-xs text-muted-foreground">
224
- <Spinner size="sm" label="Thinking" /> {starting ? "Starting…" : "Thinking…"}
230
+ <div role="status" aria-label="Agent progress" className="flex items-center gap-2 text-xs text-muted-foreground">
231
+ <Spinner size="sm" label={queued ? "Queued" : "Thinking"} /> {queued ? "Queued — it starts when the one before it ends" : "Thinking…"}
225
232
  </div>
226
233
  ) : null}
227
234
  {!live && view?.failure ? (
@@ -1126,6 +1126,20 @@ function keepThread(key, sessionId, from) {
1126
1126
 
1127
1127
  const CHAT_LIVE = new Set(["queued", "working"]);
1128
1128
 
1129
+ // A pending question is identified by its request and input ids, not the
1130
+ // time its GET/stream frame arrived. Accepted answers must not reappear
1131
+ // while the agent resumes, and a genuinely new question must still show.
1132
+ function threadQuestionKey(data) {
1133
+ if (data?.state !== "needs_you" || data.needs?.kind !== "question") return null;
1134
+ const ids = (data.view?.questions ?? []).map((q) => q.id).filter(Boolean).sort();
1135
+ return JSON.stringify([data.session_id, data.request?.id, data.needs.at ?? null, ids, ids.length ? null : data.needs.text]);
1136
+ }
1137
+
1138
+ function withoutThreadQuestion(data) {
1139
+ return { ...data, state: "working", needs: null, ...(data.view ? { view: { ...data.view, questions: [], thinking: true } } : {}) };
1140
+ }
1141
+
1142
+
1129
1143
  /**
1130
1144
  * The member's own thread with the app's agent, for the app's own chat
1131
1145
  * (the `agent-chat` block): the same eve session the desk's agent column
@@ -1206,6 +1220,31 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1206
1220
  const [tick, setTick] = useState(0);
1207
1221
  const reload = useCallback(() => setTick((t) => t + 1), []);
1208
1222
  const aliveRef = useRef(true);
1223
+ const dataRef = useRef(state.data);
1224
+ dataRef.current = state.data;
1225
+ const dismissedQuestions = useRef(new Map());
1226
+ const protectQuestions = (data) => dismissedQuestions.current.has(threadQuestionKey(data)) ? withoutThreadQuestion(data) : data;
1227
+ const dismissQuestion = () => {
1228
+ const snapshot = dataRef.current;
1229
+ const questionKey = threadQuestionKey(snapshot);
1230
+ if (!questionKey) return null;
1231
+ const pending = { key: questionKey, snapshot };
1232
+ dismissedQuestions.current.set(questionKey, pending);
1233
+ setState((s) => ({ ...s, error: null, data: protectQuestions(s.data) }));
1234
+ return pending;
1235
+ };
1236
+ const restoreQuestion = (pending, error) => {
1237
+ if (!pending || dismissedQuestions.current.get(pending.key) !== pending) return;
1238
+ dismissedQuestions.current.delete(pending.key);
1239
+ const before = pending.snapshot;
1240
+ setState((s) => {
1241
+ const current = s.data;
1242
+ const otherQuestion = threadQuestionKey(current);
1243
+ if (!current || current.session_id !== before.session_id || current.request?.id !== before.request?.id || (otherQuestion && otherQuestion !== pending.key)) return s;
1244
+ return { ...s, error, data: { ...current, state: before.state, needs: before.needs, ...(before.view ? { view: { ...current.view, questions: before.view.questions, thinking: before.view.thinking } } : {}) } };
1245
+ });
1246
+ };
1247
+ const confirmQuestion = (pending) => { if (pending) pending.snapshot = null; };
1209
1248
  const sessionRef = useRef(sessionId);
1210
1249
  sessionRef.current = sessionId;
1211
1250
  const rowRef = useRef(row);
@@ -1231,6 +1270,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1231
1270
  if (lastKey.current === storeKey) return;
1232
1271
  lastKey.current = storeKey;
1233
1272
  const kept = keptThread(storeKey);
1273
+ dismissedQuestions.current.clear();
1234
1274
  setSessionId(kept);
1235
1275
  setState({ data: null, loading: Boolean(kept), sending: false, pending: null, error: null, code: null, unavailable: false });
1236
1276
  }, [storeKey]);
@@ -1246,6 +1286,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1246
1286
  if (!d || d.key !== storeKey || d.from === me) return;
1247
1287
  const next = d.sessionId ?? null;
1248
1288
  if (next === sessionRef.current) return;
1289
+ dismissedQuestions.current.clear();
1249
1290
  setSessionId(next);
1250
1291
  setState({ data: null, loading: Boolean(next), sending: false, pending: null, error: null, code: null, unavailable: false });
1251
1292
  };
@@ -1287,7 +1328,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1287
1328
  if (frame.desk_origin) publishDesk(frame.desk_origin);
1288
1329
  setState((s) => ({
1289
1330
  ...s,
1290
- data: { ...(s.data ?? {}), ...frame, turns: s.data?.turns ?? [] },
1331
+ data: protectQuestions({ ...(s.data ?? {}), ...frame, turns: s.data?.turns ?? [] }),
1291
1332
  loading: false,
1292
1333
  error: null,
1293
1334
  code: null,
@@ -1337,7 +1378,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1337
1378
  if (data.desk_origin) publishDesk(data.desk_origin);
1338
1379
  setState((s) => ({
1339
1380
  ...s,
1340
- data,
1381
+ data: protectQuestions(data),
1341
1382
  loading: false,
1342
1383
  error: null,
1343
1384
  code: null,
@@ -1380,6 +1421,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1380
1421
  async (message, { files } = {}) => {
1381
1422
  const text = String(message ?? "").trim();
1382
1423
  if (!text) return { ok: false, error: "nothing to ask" };
1424
+ const dismissed = dismissQuestion();
1383
1425
  setState((s) => ({ ...s, sending: true, pending: text, error: null, code: null, notice: null }));
1384
1426
  const asking = (session) =>
1385
1427
  ask({ text, row: rowRef.current ?? undefined, how, job, files, sessionId: session ?? undefined, open: false });
@@ -1387,6 +1429,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1387
1429
  if (!result.ok && sessionRef.current && START_FRESH.has(result.code)) result = await asking(null);
1388
1430
  if (!aliveRef.current) return result;
1389
1431
  if (!result.ok) {
1432
+ restoreQuestion(dismissed, result.error ?? "The ask could not be sent.");
1390
1433
  const unavailable = result.status === 404 && !result.code;
1391
1434
  setState((s) => ({
1392
1435
  ...s,
@@ -1398,6 +1441,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1398
1441
  }));
1399
1442
  return result;
1400
1443
  }
1444
+ confirmQuestion(dismissed);
1401
1445
  announceAsk();
1402
1446
  const request = result.data?.request ?? null;
1403
1447
  const next = request?.app_session_id ?? sessionRef.current ?? null;
@@ -1414,7 +1458,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1414
1458
  result.data?.files_attached === false
1415
1459
  ? "Your files did not go with that message because it is waiting its turn. Attach them again once it starts."
1416
1460
  : null,
1417
- data: s.data && next === s.data.session_id ? { ...s.data, state: "working", request: request ?? s.data.request } : s.data,
1461
+ data: s.data && next === s.data.session_id ? (threadQuestionKey(s.data) && threadQuestionKey(s.data) !== dismissed?.key ? s.data : { ...s.data, state: "working", request: request ?? s.data.request }) : s.data,
1418
1462
  }));
1419
1463
  return result;
1420
1464
  },
@@ -1432,12 +1476,16 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1432
1476
  const answer = useCallback(
1433
1477
  async ({ optionId, text, answers } = {}) => {
1434
1478
  if (!request?.id) return { ok: false, error: "nothing to answer" };
1479
+ const dismissed = dismissQuestion();
1435
1480
  const r = await sendAnswer(request.id, { optionId, text, answers, at: needsAt ?? undefined });
1481
+ if (!aliveRef.current) return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
1482
+ if (!r.ok) restoreQuestion(dismissed, r.error ?? "That answer did not go through. Try again.");
1436
1483
  if (r.ok) {
1484
+ confirmQuestion(dismissed);
1437
1485
  announceAsk();
1438
- setState((s) => (s.data ? { ...s, error: null, data: { ...s.data, state: "working", needs: null } } : s));
1486
+ setState((s) => ({ ...s, error: null }));
1439
1487
  }
1440
- reload();
1488
+ if (r.ok) reload();
1441
1489
  return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
1442
1490
  },
1443
1491
  [request?.id, needsAt, reload],
@@ -1445,6 +1493,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1445
1493
  const openOnDesk = useCallback(() => (request ? openRequest(request) : false), [request]);
1446
1494
  const newChat = useCallback(() => {
1447
1495
  forget();
1496
+ dismissedQuestions.current.clear();
1448
1497
  setState({ data: null, loading: false, sending: false, pending: null, error: null, code: null, unavailable: false });
1449
1498
  }, [forget]);
1450
1499
  // Another of the member's threads in this app (the pane's Chats list):
@@ -1453,6 +1502,7 @@ export function useAgentThread({ row, how, job = false, key, view = false } = {}
1453
1502
  (next) => {
1454
1503
  const id = typeof next === "string" && next ? next : null;
1455
1504
  if (!id || id === sessionRef.current) return;
1505
+ dismissedQuestions.current.clear();
1456
1506
  keepThread(storeKey, id, me);
1457
1507
  setSessionId(id);
1458
1508
  setState({ data: null, loading: true, sending: false, pending: null, error: null, code: null, unavailable: false });