makersclaw 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/package.json +1 -1
- package/r/activity-log.json +52 -0
- package/r/agent-changes.json +51 -0
- package/r/agent-chat.json +53 -0
- package/r/agent-column.json +85 -0
- package/r/agent-dock.json +57 -0
- package/r/agent-kit.json +49 -0
- package/r/agent-marks.json +49 -0
- package/r/agent-pane.json +93 -0
- package/r/ai-action.json +56 -0
- package/r/annotate.json +3 -3
- package/r/approval-request.json +2 -2
- package/r/approval.json +54 -0
- package/r/ask-bar.json +50 -0
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/automation-editor.json +66 -0
- package/r/automation-list.json +57 -0
- package/r/automation-model.json +118 -0
- package/r/automations-screen.json +71 -0
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +2 -2
- package/r/chat-pane.json +84 -0
- package/r/chat.json +2 -2
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/connect-card.json +51 -0
- package/r/contact-section.json +2 -2
- package/r/core.json +13 -7
- package/r/cost.json +51 -0
- package/r/csv-import.json +3 -3
- package/r/data-picker.json +60 -0
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/first-run.json +63 -0
- package/r/fix-note.json +57 -0
- package/r/flow-canvas.json +2 -2
- package/r/flow-editor.json +79 -0
- package/r/footer.json +2 -2
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/library-shelf.json +56 -0
- package/r/map.json +2 -2
- package/r/margin-notes.json +67 -0
- package/r/menubar.json +2 -2
- package/r/mind-map.json +3 -3
- package/r/navigation-menu.json +2 -2
- package/r/needs-you.json +55 -0
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +3 -3
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +2 -2
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +2 -2
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-grid.json +52 -0
- package/r/record-notes.json +4 -4
- package/r/registry.json +4086 -1512
- package/r/run-history.json +52 -0
- package/r/run-view.json +80 -0
- package/r/schedule-card.json +54 -0
- package/r/schedule-engine.json +79 -0
- package/r/schedule-gantt.json +83 -0
- package/r/schedule-ruler.json +64 -0
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +2 -2
- package/r/source-receipt.json +52 -0
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/step-picker.json +53 -0
- package/r/step-settings.json +72 -0
- package/r/suggestion.json +58 -0
- package/r/sum-bar.json +64 -0
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/tier-map.json +67 -0
- package/r/time-grid.json +73 -0
- package/r/time-strip.json +64 -0
- package/r/top-bar.json +66 -0
- package/r/transaction-list.json +2 -2
- package/r/trigger-picker.json +61 -0
- package/r/whiteboard.json +3 -3
- package/r/workflow-view.json +55 -0
- package/registry/blocks/activity-log/README.md +112 -0
- package/registry/blocks/activity-log/activity-log.jsx +351 -0
- package/registry/blocks/activity-log/block.json +33 -0
- package/registry/blocks/activity-log/demo.jsx +48 -0
- package/registry/blocks/activity-log/index.js +4 -0
- package/registry/blocks/agent-changes/README.md +94 -0
- package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
- package/registry/blocks/agent-changes/block.json +28 -0
- package/registry/blocks/agent-changes/demo.jsx +39 -0
- package/registry/blocks/agent-changes/index.js +3 -0
- package/registry/blocks/agent-chat/README.md +145 -0
- package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
- package/registry/blocks/agent-chat/block.json +30 -0
- package/registry/blocks/agent-chat/demo.jsx +165 -0
- package/registry/blocks/agent-chat/index.js +3 -0
- package/registry/blocks/agent-column/README.md +197 -0
- package/registry/blocks/agent-column/agent-column.jsx +492 -0
- package/registry/blocks/agent-column/block.json +67 -0
- package/registry/blocks/agent-column/column-picker.jsx +163 -0
- package/registry/blocks/agent-column/demo.jsx +50 -0
- package/registry/blocks/agent-column/index.js +7 -0
- package/registry/blocks/agent-column/model.js +164 -0
- package/registry/blocks/agent-column/receipt.jsx +483 -0
- package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
- package/registry/blocks/agent-dock/README.md +210 -0
- package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
- package/registry/blocks/agent-dock/block.json +47 -0
- package/registry/blocks/agent-dock/demo.jsx +153 -0
- package/registry/blocks/agent-dock/dock-model.js +118 -0
- package/registry/blocks/agent-dock/index.js +5 -0
- package/registry/blocks/agent-kit/README.md +73 -0
- package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
- package/registry/blocks/agent-kit/block.json +10 -0
- package/registry/blocks/agent-kit/demo.jsx +35 -0
- package/registry/blocks/agent-kit/index.js +3 -0
- package/registry/blocks/agent-marks/README.md +80 -0
- package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
- package/registry/blocks/agent-marks/block.json +27 -0
- package/registry/blocks/agent-marks/demo.jsx +49 -0
- package/registry/blocks/agent-marks/index.js +3 -0
- package/registry/blocks/agent-pane/README.md +225 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
- package/registry/blocks/agent-pane/block.json +41 -0
- package/registry/blocks/agent-pane/composer.jsx +6 -0
- package/registry/blocks/agent-pane/demo.jsx +233 -0
- package/registry/blocks/agent-pane/dock-context.js +10 -0
- package/registry/blocks/agent-pane/index.js +4 -0
- package/registry/blocks/agent-pane/prose.jsx +84 -0
- package/registry/blocks/agent-pane/questions.jsx +134 -0
- package/registry/blocks/agent-pane/transcript.jsx +268 -0
- package/registry/blocks/agent-pane/work.jsx +368 -0
- package/registry/blocks/ai-action/README.md +94 -0
- package/registry/blocks/ai-action/ai-action.jsx +188 -0
- package/registry/blocks/ai-action/block.json +34 -0
- package/registry/blocks/ai-action/demo.jsx +93 -0
- package/registry/blocks/ai-action/index.js +3 -0
- package/registry/blocks/approval/README.md +145 -0
- package/registry/blocks/approval/approval.jsx +163 -0
- package/registry/blocks/approval/block.json +31 -0
- package/registry/blocks/approval/demo.jsx +121 -0
- package/registry/blocks/approval/index.js +3 -0
- package/registry/blocks/ask-bar/README.md +105 -0
- package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
- package/registry/blocks/ask-bar/block.json +26 -0
- package/registry/blocks/ask-bar/demo.jsx +78 -0
- package/registry/blocks/ask-bar/index.js +3 -0
- package/registry/blocks/automation-editor/README.md +291 -0
- package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
- package/registry/blocks/automation-editor/block.json +39 -0
- package/registry/blocks/automation-editor/demo.jsx +284 -0
- package/registry/blocks/automation-editor/index.js +4 -0
- package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
- package/registry/blocks/automation-list/README.md +129 -0
- package/registry/blocks/automation-list/automation-list.jsx +476 -0
- package/registry/blocks/automation-list/block.json +35 -0
- package/registry/blocks/automation-list/demo.jsx +67 -0
- package/registry/blocks/automation-list/index.js +3 -0
- package/registry/blocks/automation-model/README.md +109 -0
- package/registry/blocks/automation-model/block.json +135 -0
- package/registry/blocks/automation-model/catalog.js +256 -0
- package/registry/blocks/automation-model/check.js +161 -0
- package/registry/blocks/automation-model/demo.jsx +42 -0
- package/registry/blocks/automation-model/edit.js +262 -0
- package/registry/blocks/automation-model/fixes.js +287 -0
- package/registry/blocks/automation-model/glyph.jsx +13 -0
- package/registry/blocks/automation-model/graph.js +410 -0
- package/registry/blocks/automation-model/index.js +33 -0
- package/registry/blocks/automation-model/overlay.js +175 -0
- package/registry/blocks/automation-model/parts.jsx +94 -0
- package/registry/blocks/automation-model/schedule.js +53 -0
- package/registry/blocks/automation-model/templates.js +128 -0
- package/registry/blocks/automation-model/values.js +29 -0
- package/registry/blocks/automation-model/words.js +439 -0
- package/registry/blocks/automations-screen/README.md +207 -0
- package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
- package/registry/blocks/automations-screen/block.json +47 -0
- package/registry/blocks/automations-screen/demo.jsx +364 -0
- package/registry/blocks/automations-screen/index.js +4 -0
- package/registry/blocks/automations-screen/screen-model.js +129 -0
- package/registry/blocks/chat-pane/README.md +143 -0
- package/registry/blocks/chat-pane/block.json +48 -0
- package/registry/blocks/chat-pane/chat-attach.js +166 -0
- package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
- package/registry/blocks/chat-pane/chat-context.js +17 -0
- package/registry/blocks/chat-pane/chat-model.js +43 -0
- package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
- package/registry/blocks/chat-pane/chats-list.jsx +52 -0
- package/registry/blocks/chat-pane/demo.jsx +103 -0
- package/registry/blocks/chat-pane/index.js +9 -0
- package/registry/blocks/connect-card/README.md +99 -0
- package/registry/blocks/connect-card/block.json +10 -0
- package/registry/blocks/connect-card/connect-card.jsx +103 -0
- package/registry/blocks/connect-card/demo.jsx +49 -0
- package/registry/blocks/connect-card/index.js +3 -0
- package/registry/blocks/cost/README.md +115 -0
- package/registry/blocks/cost/block.json +10 -0
- package/registry/blocks/cost/cost.jsx +176 -0
- package/registry/blocks/cost/demo.jsx +66 -0
- package/registry/blocks/cost/index.js +3 -0
- package/registry/blocks/data-picker/README.md +81 -0
- package/registry/blocks/data-picker/block.json +38 -0
- package/registry/blocks/data-picker/data-picker.jsx +210 -0
- package/registry/blocks/data-picker/demo.jsx +34 -0
- package/registry/blocks/data-picker/fields.js +89 -0
- package/registry/blocks/data-picker/index.js +4 -0
- package/registry/blocks/data-table/demo.jsx +1 -1
- package/registry/blocks/first-run/README.md +118 -0
- package/registry/blocks/first-run/block.json +11 -0
- package/registry/blocks/first-run/demo.jsx +88 -0
- package/registry/blocks/first-run/first-run-model.js +51 -0
- package/registry/blocks/first-run/first-run.jsx +156 -0
- package/registry/blocks/first-run/index.js +5 -0
- package/registry/blocks/first-run/probe-rules.js +26 -0
- package/registry/blocks/fix-note/README.md +79 -0
- package/registry/blocks/fix-note/block.json +35 -0
- package/registry/blocks/fix-note/demo.jsx +30 -0
- package/registry/blocks/fix-note/fix-note.jsx +235 -0
- package/registry/blocks/fix-note/index.js +3 -0
- package/registry/blocks/flow-editor/README.md +108 -0
- package/registry/blocks/flow-editor/block.json +44 -0
- package/registry/blocks/flow-editor/demo.jsx +115 -0
- package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
- package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
- package/registry/blocks/flow-editor/index.js +6 -0
- package/registry/blocks/flow-editor/minimap.jsx +78 -0
- package/registry/blocks/flow-editor/outline.jsx +134 -0
- package/registry/blocks/library-shelf/README.md +117 -0
- package/registry/blocks/library-shelf/block.json +28 -0
- package/registry/blocks/library-shelf/demo.jsx +68 -0
- package/registry/blocks/library-shelf/index.js +3 -0
- package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
- package/registry/blocks/library-shelf/probe-rules.js +15 -0
- package/registry/blocks/margin-notes/README.md +222 -0
- package/registry/blocks/margin-notes/block.json +10 -0
- package/registry/blocks/margin-notes/demo.jsx +144 -0
- package/registry/blocks/margin-notes/index.js +20 -0
- package/registry/blocks/margin-notes/margin-model.js +224 -0
- package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
- package/registry/blocks/needs-you/README.md +177 -0
- package/registry/blocks/needs-you/block.json +34 -0
- package/registry/blocks/needs-you/demo.jsx +143 -0
- package/registry/blocks/needs-you/index.js +3 -0
- package/registry/blocks/needs-you/needs-you.jsx +441 -0
- package/registry/blocks/pace-bar/README.md +230 -0
- package/registry/blocks/pace-bar/block.json +10 -0
- package/registry/blocks/pace-bar/demo.jsx +255 -0
- package/registry/blocks/pace-bar/index.js +22 -0
- package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
- package/registry/blocks/pace-bar/pace-model.js +288 -0
- package/registry/blocks/record-grid/README.md +100 -0
- package/registry/blocks/record-grid/block.json +28 -0
- package/registry/blocks/record-grid/demo.jsx +43 -0
- package/registry/blocks/record-grid/index.js +3 -0
- package/registry/blocks/record-grid/record-grid.jsx +251 -0
- package/registry/blocks/run-history/README.md +138 -0
- package/registry/blocks/run-history/block.json +10 -0
- package/registry/blocks/run-history/demo.jsx +74 -0
- package/registry/blocks/run-history/index.js +3 -0
- package/registry/blocks/run-history/run-history.jsx +218 -0
- package/registry/blocks/run-view/README.md +174 -0
- package/registry/blocks/run-view/block.json +50 -0
- package/registry/blocks/run-view/demo.jsx +286 -0
- package/registry/blocks/run-view/index.js +5 -0
- package/registry/blocks/run-view/run-controls.jsx +266 -0
- package/registry/blocks/run-view/run-view.jsx +236 -0
- package/registry/blocks/run-view/runs-list.jsx +99 -0
- package/registry/blocks/schedule-card/README.md +121 -0
- package/registry/blocks/schedule-card/block.json +10 -0
- package/registry/blocks/schedule-card/demo.jsx +82 -0
- package/registry/blocks/schedule-card/index.js +3 -0
- package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
- package/registry/blocks/schedule-engine/README.md +137 -0
- package/registry/blocks/schedule-engine/block.json +10 -0
- package/registry/blocks/schedule-engine/demo.jsx +82 -0
- package/registry/blocks/schedule-engine/engine.js +769 -0
- package/registry/blocks/schedule-engine/index.js +9 -0
- package/registry/blocks/schedule-engine/ops.js +71 -0
- package/registry/blocks/schedule-engine/place.js +294 -0
- package/registry/blocks/schedule-engine/tree.js +79 -0
- package/registry/blocks/schedule-engine/waits.js +80 -0
- package/registry/blocks/schedule-gantt/README.md +159 -0
- package/registry/blocks/schedule-gantt/block.json +10 -0
- package/registry/blocks/schedule-gantt/demo.jsx +223 -0
- package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
- package/registry/blocks/schedule-gantt/geometry.js +176 -0
- package/registry/blocks/schedule-gantt/index.js +26 -0
- package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
- package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
- package/registry/blocks/schedule-gantt/track.jsx +190 -0
- package/registry/blocks/schedule-ruler/README.md +148 -0
- package/registry/blocks/schedule-ruler/block.json +10 -0
- package/registry/blocks/schedule-ruler/demo.jsx +181 -0
- package/registry/blocks/schedule-ruler/index.js +21 -0
- package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
- package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
- package/registry/blocks/source-receipt/README.md +97 -0
- package/registry/blocks/source-receipt/block.json +30 -0
- package/registry/blocks/source-receipt/demo.jsx +71 -0
- package/registry/blocks/source-receipt/index.js +3 -0
- package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
- package/registry/blocks/step-picker/README.md +78 -0
- package/registry/blocks/step-picker/block.json +31 -0
- package/registry/blocks/step-picker/demo.jsx +23 -0
- package/registry/blocks/step-picker/index.js +3 -0
- package/registry/blocks/step-picker/step-picker.jsx +144 -0
- package/registry/blocks/step-settings/README.md +100 -0
- package/registry/blocks/step-settings/block.json +44 -0
- package/registry/blocks/step-settings/demo.jsx +93 -0
- package/registry/blocks/step-settings/fields.jsx +58 -0
- package/registry/blocks/step-settings/flow-settings.jsx +115 -0
- package/registry/blocks/step-settings/index.js +5 -0
- package/registry/blocks/step-settings/step-settings.jsx +290 -0
- package/registry/blocks/suggestion/README.md +171 -0
- package/registry/blocks/suggestion/block.json +40 -0
- package/registry/blocks/suggestion/demo.jsx +91 -0
- package/registry/blocks/suggestion/index.js +3 -0
- package/registry/blocks/suggestion/suggestion.jsx +436 -0
- package/registry/blocks/sum-bar/README.md +122 -0
- package/registry/blocks/sum-bar/block.json +10 -0
- package/registry/blocks/sum-bar/demo.jsx +110 -0
- package/registry/blocks/sum-bar/index.js +5 -0
- package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
- package/registry/blocks/sum-bar/sum-model.js +126 -0
- package/registry/blocks/tier-map/README.md +217 -0
- package/registry/blocks/tier-map/block.json +10 -0
- package/registry/blocks/tier-map/demo.jsx +222 -0
- package/registry/blocks/tier-map/index.js +24 -0
- package/registry/blocks/tier-map/tier-map.jsx +574 -0
- package/registry/blocks/tier-map/tier-model.js +318 -0
- package/registry/blocks/time-grid/README.md +305 -0
- package/registry/blocks/time-grid/block.json +10 -0
- package/registry/blocks/time-grid/demo.jsx +279 -0
- package/registry/blocks/time-grid/grid-model.js +322 -0
- package/registry/blocks/time-grid/index.js +25 -0
- package/registry/blocks/time-grid/time-grid.jsx +1325 -0
- package/registry/blocks/time-strip/README.md +141 -0
- package/registry/blocks/time-strip/block.json +10 -0
- package/registry/blocks/time-strip/demo.jsx +129 -0
- package/registry/blocks/time-strip/index.js +19 -0
- package/registry/blocks/time-strip/strip-model.js +178 -0
- package/registry/blocks/time-strip/time-strip.jsx +260 -0
- package/registry/blocks/top-bar/README.md +149 -0
- package/registry/blocks/top-bar/block.json +35 -0
- package/registry/blocks/top-bar/demo.jsx +78 -0
- package/registry/blocks/top-bar/index.js +5 -0
- package/registry/blocks/top-bar/probe-rules.js +74 -0
- package/registry/blocks/top-bar/top-bar-model.js +152 -0
- package/registry/blocks/top-bar/top-bar.jsx +244 -0
- package/registry/blocks/trigger-picker/README.md +98 -0
- package/registry/blocks/trigger-picker/block.json +38 -0
- package/registry/blocks/trigger-picker/demo.jsx +59 -0
- package/registry/blocks/trigger-picker/index.js +3 -0
- package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
- package/registry/blocks/workflow-view/README.md +123 -0
- package/registry/blocks/workflow-view/block.json +10 -0
- package/registry/blocks/workflow-view/demo.jsx +90 -0
- package/registry/blocks/workflow-view/index.js +3 -0
- package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
- package/src/install.mjs +77 -16
- package/src/registry.mjs +3 -0
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +627 -24
- package/templates/next/files/check/automation-format.ts +2103 -0
- package/templates/next/files/check/automation-package.ts +142 -0
- package/templates/next/files/check/run.mjs +968 -9
- package/templates/next/files/components/ui/field.jsx +17 -4
- package/templates/next/files/components/ui/icon.jsx +13 -0
- package/templates/next/files/components/ui/index.js +2 -1
- package/templates/next/files/components/ui/layout.jsx +77 -2
- package/templates/next/files/components/ui/notice.jsx +70 -0
- package/templates/next/files/components/ui/page.jsx +124 -0
- package/templates/next/files/components/ui/stat.jsx +22 -7
- package/templates/next/files/components/ui/toast.jsx +6 -3
- package/templates/next/files/lib/automations.js +660 -0
- package/templates/next/files/lib/claw.js +241 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1627 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -1,16 +1,61 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { claw, publishDesk, publishStatus } from "./claw";
|
|
5
|
-
import {
|
|
4
|
+
import { claw, openApp, openConnections, openFile, pressMessage, publishDesk, publishStatus } from "./claw";
|
|
5
|
+
import {
|
|
6
|
+
activity as readActivity,
|
|
7
|
+
agentKit as readAgentKit,
|
|
8
|
+
answer as sendAnswer,
|
|
9
|
+
approval as readApproval,
|
|
10
|
+
ask,
|
|
11
|
+
cancelJob,
|
|
12
|
+
connectLink,
|
|
13
|
+
connections as readConnections,
|
|
14
|
+
jobs as readJobs,
|
|
15
|
+
library,
|
|
16
|
+
llm,
|
|
17
|
+
llmEstimate,
|
|
18
|
+
openRequest,
|
|
19
|
+
pauseApp,
|
|
20
|
+
requests as readRequests,
|
|
21
|
+
resumeApp,
|
|
22
|
+
run as readRun,
|
|
23
|
+
runNow as startRun,
|
|
24
|
+
runs as readRuns,
|
|
25
|
+
saidSomething,
|
|
26
|
+
setJob,
|
|
27
|
+
startJob,
|
|
28
|
+
stopRequest,
|
|
29
|
+
streamThread,
|
|
30
|
+
thread as readThread,
|
|
31
|
+
threadCall as readThreadCall,
|
|
32
|
+
threads as readThreads,
|
|
33
|
+
} from "./platform";
|
|
6
34
|
|
|
7
35
|
// The React side of the _claw API, without any look: fetching, polling,
|
|
8
36
|
// streaming and cleanup. A surface renders what comes back and owns every
|
|
9
37
|
// pixel. "use client" only: the _claw API is browser-side.
|
|
10
38
|
//
|
|
11
|
-
//
|
|
12
|
-
// those
|
|
13
|
-
// window (docs/ARTIFACT-CONTRACTS.md §9)
|
|
39
|
+
// Up to 0.8.1 this header said there was no runs or activity hook here
|
|
40
|
+
// and there would not be: those were STANDING surfaces, and the OS drew
|
|
41
|
+
// them as tabs on the app window (docs/ARTIFACT-CONTRACTS.md §9) so that
|
|
42
|
+
// every app showed its agent's work in one look. The kit (0.8.1, D15)
|
|
43
|
+
// changed that. One look now lives in the kit's blocks, drawn in the
|
|
44
|
+
// app's theme, so the app shows its agent's work itself, beside the rows
|
|
45
|
+
// it wrote: `useJobs` (the schedule, Run now, the switches), `useRuns` and
|
|
46
|
+
// `useRun` (the runs and one run's account), `useNeedsYou` (what waits on
|
|
47
|
+
// this member), `useActivity` (who did what, a record's timeline),
|
|
48
|
+
// `useConnections` (the app's accounts), `useAgentKit` (what its agent
|
|
49
|
+
// knows and can do) and `useLlmAction` (a model call with its price
|
|
50
|
+
// first). The OS tabs stay, as the full history; the yes, the account
|
|
51
|
+
// sign-in and money stay on the desk. Each kit hook polls only while the
|
|
52
|
+
// page is shown, backs off while its door is missing, aborts on unmount,
|
|
53
|
+
// and never throws: a missing door is `unavailable`, a quiet "not
|
|
54
|
+
// connected yet", never an error.
|
|
55
|
+
//
|
|
56
|
+
// The chat came first (0.8.1): `useAgentThread` follows the member's own
|
|
57
|
+
// thread with the app's agent, the same conversation the desk's column
|
|
58
|
+
// shows, for the `agent-chat` block. `useJob` is the other thing —
|
|
14
59
|
// work a founder asks for from inside the app, which is not a surface at
|
|
15
60
|
// all: it opens because they pressed something and it closes when the
|
|
16
61
|
// work lands (SOP §2.3, ARTIFACT-CONTRACTS §9f). Since 0.4.0 that work is
|
|
@@ -19,7 +64,10 @@ import { ask, cancelJob, llm, openRequest, requests as readRequests, startJob, s
|
|
|
19
64
|
// from a surface: the asks in flight on this app, so an empty table can
|
|
20
65
|
// show the work coming instead of a blank. `useLlm` (0.8.0) is neither:
|
|
21
66
|
// one call to a model, answered in the page, for text work on what the
|
|
22
|
-
// screen holds — nothing to follow, nothing on the desk.
|
|
67
|
+
// screen holds — nothing to follow, nothing on the desk. Nor are the
|
|
68
|
+
// Library's hooks (0.8.1): they read the collections Makersclaw keeps,
|
|
69
|
+
// once, when the screen asks (a collection's items and rows a page at a
|
|
70
|
+
// time), and have nothing to poll.
|
|
23
71
|
//
|
|
24
72
|
// While the gateway has no _claw routes yet (`unavailable`), polls back
|
|
25
73
|
// off to once a minute so a preview that is still being wired does not
|
|
@@ -698,7 +746,8 @@ const QUIET = { loading: false, result: null, error: null, unavailable: false };
|
|
|
698
746
|
* { ok: false, aborted: true } and changes nothing, so a slow answer never
|
|
699
747
|
* lands over a newer one. The hook owns the signal (one in opts is
|
|
700
748
|
* replaced). Unmounting aborts the call in flight. Nothing is called until
|
|
701
|
-
* `run` is.
|
|
749
|
+
* `run` is. `reset()` (0.8.1) clears what is shown and drops a call in
|
|
750
|
+
* flight.
|
|
702
751
|
*
|
|
703
752
|
* const { run, loading, result, error, unavailable } = useLlm();
|
|
704
753
|
* run(`Translate to French: ${text}`, { task: "translate" });
|
|
@@ -739,5 +788,1575 @@ export function useLlm() {
|
|
|
739
788
|
return answer;
|
|
740
789
|
}, []);
|
|
741
790
|
|
|
742
|
-
|
|
791
|
+
// Back to nothing shown: a call in flight is aborted and its answer
|
|
792
|
+
// changes nothing.
|
|
793
|
+
const reset = useCallback(() => {
|
|
794
|
+
abortRef.current?.abort();
|
|
795
|
+
runRef.current += 1;
|
|
796
|
+
setState(QUIET);
|
|
797
|
+
}, []);
|
|
798
|
+
|
|
799
|
+
return { ...state, run, reset };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
// The Library (0.8.1): lib/platform.js, library
|
|
804
|
+
|
|
805
|
+
/** What a Library read shows while it is out, and what it shows with
|
|
806
|
+
* nothing to read (no id yet, or `enabled` false). `key` names the read
|
|
807
|
+
* the state belongs to: a state kept for another read is not shown. */
|
|
808
|
+
const READING = { key: null, data: null, loading: true, error: null, code: null, unavailable: false };
|
|
809
|
+
const NOTHING = { key: null, data: null, loading: false, error: null, code: null, unavailable: false };
|
|
810
|
+
const NONE = [];
|
|
811
|
+
|
|
812
|
+
/** A read that did not give the hooks an answer to hold: the platform's
|
|
813
|
+
* refusal, a claw with no door, or an answer that is not the door's.
|
|
814
|
+
* `unavailable` (a claw from before the door, or no network) leaves
|
|
815
|
+
* `error` null, so a refusal and a missing door are never both set (as
|
|
816
|
+
* useLlm). `code` is the refusal's (`needs_plan`), when it named one. */
|
|
817
|
+
function unanswered(r) {
|
|
818
|
+
if (r.unavailable) return { error: null, code: null, unavailable: true };
|
|
819
|
+
return { error: r.error || "the Library did not answer", code: r.code ?? null, unavailable: false };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Is this a read the door answered with an object? A 200 of another kind
|
|
823
|
+
* (a login page in front of the app) is not the Library's. */
|
|
824
|
+
function answered(r) {
|
|
825
|
+
if (!r.ok) return r;
|
|
826
|
+
if (r.data && typeof r.data === "object" && !Array.isArray(r.data)) return r;
|
|
827
|
+
return { ok: false, status: r.status, error: "the platform answered something unexpected", unavailable: false };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* One read of the Library, once: fetches when `key` changes (the key names
|
|
832
|
+
* the read: its id and its options) and on reload(). While the key is the
|
|
833
|
+
* same a failed reload keeps the last data and sets `error`; a new key
|
|
834
|
+
* shows `loading` and nothing else, never the last read's answer. Aborts a
|
|
835
|
+
* read it no longer wants. → { data, loading, error, code, unavailable,
|
|
836
|
+
* reload }
|
|
837
|
+
*/
|
|
838
|
+
function useLibraryRead(read, key, enabled) {
|
|
839
|
+
const [state, setState] = useState(READING);
|
|
840
|
+
const [tick, setTick] = useState(0);
|
|
841
|
+
const readRef = useRef(read);
|
|
842
|
+
readRef.current = read;
|
|
843
|
+
const reload = useCallback(() => setTick((t) => t + 1), []);
|
|
844
|
+
|
|
845
|
+
useEffect(() => {
|
|
846
|
+
if (!enabled) return undefined;
|
|
847
|
+
const controller = new AbortController();
|
|
848
|
+
let alive = true;
|
|
849
|
+
(async () => {
|
|
850
|
+
const r = answered(await readRef.current(controller.signal));
|
|
851
|
+
if (!alive || r.aborted) return;
|
|
852
|
+
if (r.ok) setState({ key, data: r.data, loading: false, error: null, code: null, unavailable: false });
|
|
853
|
+
else setState((s) => ({ key, data: s.key === key ? s.data : null, loading: false, ...unanswered(r) }));
|
|
854
|
+
})();
|
|
855
|
+
return () => {
|
|
856
|
+
alive = false;
|
|
857
|
+
controller.abort();
|
|
858
|
+
};
|
|
859
|
+
}, [key, enabled, tick]);
|
|
860
|
+
|
|
861
|
+
const mine = enabled ? (state.key === key ? state : READING) : NOTHING;
|
|
862
|
+
return { data: mine.data, loading: mine.loading, error: mine.error, code: mine.code, unavailable: mine.unavailable, reload };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** A page's list, total and next cursor from a door's answer. */
|
|
866
|
+
function pageOf(data, name) {
|
|
867
|
+
return {
|
|
868
|
+
list: Array.isArray(data?.[name]) ? data[name] : NONE,
|
|
869
|
+
total: typeof data?.total === "number" ? data.total : null,
|
|
870
|
+
previewOnly: Boolean(data?.preview_only),
|
|
871
|
+
next: typeof data?.next_cursor === "string" && data.next_cursor ? data.next_cursor : null,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const READING_PAGE = {
|
|
876
|
+
key: null,
|
|
877
|
+
list: NONE,
|
|
878
|
+
total: null,
|
|
879
|
+
previewOnly: false,
|
|
880
|
+
next: null,
|
|
881
|
+
loading: true,
|
|
882
|
+
loadingMore: false,
|
|
883
|
+
error: null,
|
|
884
|
+
code: null,
|
|
885
|
+
unavailable: false,
|
|
886
|
+
};
|
|
887
|
+
const NOTHING_PAGE = { ...READING_PAGE, loading: false };
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* A collection's items or rows, a page at a time. `read(cursor, signal)`
|
|
891
|
+
* asks for one page. The first page is read when `key` changes and on
|
|
892
|
+
* reload(); loadMore() reads the one after it and adds it to the list (a
|
|
893
|
+
* row seen twice is kept once). A new key, a reload and an unmount abort a
|
|
894
|
+
* loadMore in flight, so a late page never lands on another list. →
|
|
895
|
+
* { list, total, previewOnly, hasMore, loading, loadingMore, error, code,
|
|
896
|
+
* unavailable, loadMore, reload }
|
|
897
|
+
*/
|
|
898
|
+
function useLibraryPage(name, read, key, enabled) {
|
|
899
|
+
const [state, setState] = useState(READING_PAGE);
|
|
900
|
+
const [tick, setTick] = useState(0);
|
|
901
|
+
const readRef = useRef(read);
|
|
902
|
+
readRef.current = read;
|
|
903
|
+
const nowRef = useRef({ key, state });
|
|
904
|
+
nowRef.current = { key, state };
|
|
905
|
+
const moreRef = useRef(null); // the loadMore in flight: its AbortController
|
|
906
|
+
const reload = useCallback(() => setTick((t) => t + 1), []);
|
|
907
|
+
|
|
908
|
+
useEffect(() => {
|
|
909
|
+
if (!enabled) return undefined;
|
|
910
|
+
const controller = new AbortController();
|
|
911
|
+
let alive = true;
|
|
912
|
+
(async () => {
|
|
913
|
+
const r = answered(await readRef.current(null, controller.signal));
|
|
914
|
+
if (!alive || r.aborted) return;
|
|
915
|
+
if (r.ok) setState({ ...READING_PAGE, ...pageOf(r.data, name), key, loading: false });
|
|
916
|
+
else setState((s) => ({ ...(s.key === key ? s : READING_PAGE), key, loading: false, loadingMore: false, ...unanswered(r) }));
|
|
917
|
+
})();
|
|
918
|
+
return () => {
|
|
919
|
+
alive = false;
|
|
920
|
+
controller.abort();
|
|
921
|
+
moreRef.current?.abort();
|
|
922
|
+
moreRef.current = null;
|
|
923
|
+
};
|
|
924
|
+
}, [name, key, enabled, tick]);
|
|
925
|
+
|
|
926
|
+
const loadMore = useCallback(async () => {
|
|
927
|
+
const { key: now, state: held } = nowRef.current;
|
|
928
|
+
if (held.key !== now || !held.next || moreRef.current) return undefined;
|
|
929
|
+
const controller = new AbortController();
|
|
930
|
+
moreRef.current = controller;
|
|
931
|
+
setState((s) => (s.key === now ? { ...s, loadingMore: true, error: null, code: null } : s));
|
|
932
|
+
const r = answered(await readRef.current(held.next, controller.signal));
|
|
933
|
+
if (moreRef.current === controller) moreRef.current = null;
|
|
934
|
+
if (controller.signal.aborted) return r; // a new key, a reload or an unmount took the list
|
|
935
|
+
setState((s) => {
|
|
936
|
+
if (s.key !== now) return s;
|
|
937
|
+
if (!r.ok) return { ...s, loadingMore: false, ...unanswered(r) };
|
|
938
|
+
const page = pageOf(r.data, name);
|
|
939
|
+
const seen = new Set(s.list.map((x) => x?.id));
|
|
940
|
+
return {
|
|
941
|
+
...s,
|
|
942
|
+
list: [...s.list, ...page.list.filter((x) => !seen.has(x?.id))],
|
|
943
|
+
total: page.total ?? s.total,
|
|
944
|
+
previewOnly: page.previewOnly,
|
|
945
|
+
next: page.next,
|
|
946
|
+
loadingMore: false,
|
|
947
|
+
error: null,
|
|
948
|
+
code: null,
|
|
949
|
+
unavailable: false,
|
|
950
|
+
};
|
|
951
|
+
});
|
|
952
|
+
return r;
|
|
953
|
+
}, [name]);
|
|
954
|
+
|
|
955
|
+
const mine = enabled ? (state.key === key ? state : READING_PAGE) : NOTHING_PAGE;
|
|
956
|
+
return {
|
|
957
|
+
list: mine.list,
|
|
958
|
+
total: mine.total,
|
|
959
|
+
previewOnly: mine.previewOnly,
|
|
960
|
+
hasMore: Boolean(mine.next),
|
|
961
|
+
loading: mine.loading,
|
|
962
|
+
loadingMore: mine.loadingMore,
|
|
963
|
+
error: mine.error,
|
|
964
|
+
code: mine.code,
|
|
965
|
+
unavailable: mine.unavailable,
|
|
966
|
+
loadMore,
|
|
967
|
+
reload,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Filters as a key that does not depend on the order they were written
|
|
972
|
+
* in: the ones with a value, sorted by column. */
|
|
973
|
+
function filtersKey(filters) {
|
|
974
|
+
if (!filters || typeof filters !== "object" || Array.isArray(filters)) return [];
|
|
975
|
+
return Object.entries(filters)
|
|
976
|
+
.filter(([, v]) => (typeof v === "string" ? v.trim() !== "" : typeof v === "number" || typeof v === "boolean"))
|
|
977
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* The collections on the Library's shelves (library.collections), for a
|
|
982
|
+
* screen that offers a choice. Read once; reload() reads again.
|
|
983
|
+
*
|
|
984
|
+
* const { collections, loading, error, unavailable } = useLibraryCollections({ shelf: "decks" });
|
|
985
|
+
*
|
|
986
|
+
* → { collections, loading, error, code, unavailable, reload }. Each
|
|
987
|
+
* collection is a card: { id, slug, title, blurb, kind, shelf, tags,
|
|
988
|
+
* access, status, public_page, cover_url, item_count, row_count, usable,
|
|
989
|
+
* preview_only }. `enabled: false` reads nothing.
|
|
990
|
+
*/
|
|
991
|
+
export function useLibraryCollections(opts) {
|
|
992
|
+
const { q, shelf, enabled = true } = opts ?? {};
|
|
993
|
+
const key = JSON.stringify(["collections", q ?? "", shelf ?? ""]);
|
|
994
|
+
const { data, ...rest } = useLibraryRead((signal) => library.collections({ q, shelf, signal }), key, Boolean(enabled));
|
|
995
|
+
return { collections: Array.isArray(data?.collections) ? data.collections : NONE, ...rest };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* One collection whole (library.collection): its card and `body`,
|
|
1000
|
+
* `columns` (a rows collection's: { key, label, type, filter, … }),
|
|
1001
|
+
* `source`, `licence`, `preview_count`, `personal_data`, `published_at`.
|
|
1002
|
+
* `id` is its slug or uuid. Nothing is read until there is an id.
|
|
1003
|
+
* → { collection, loading, error, code, unavailable, reload }
|
|
1004
|
+
*/
|
|
1005
|
+
export function useLibraryCollection(id, opts) {
|
|
1006
|
+
const enabled = Boolean(id) && (opts?.enabled ?? true);
|
|
1007
|
+
const key = JSON.stringify(["collection", id ?? null]);
|
|
1008
|
+
const { data, ...rest } = useLibraryRead((signal) => library.collection(id, { signal }), key, enabled);
|
|
1009
|
+
return { collection: data?.collection ?? null, ...rest };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* A files collection's items, a page at a time (library.items):
|
|
1014
|
+
*
|
|
1015
|
+
* const { items, total, previewOnly, hasMore, loadMore, loading, loadingMore } =
|
|
1016
|
+
* useLibraryItems("pitch-decks", { q: search, tag: "seed", limit: 24 });
|
|
1017
|
+
*
|
|
1018
|
+
* items the pages read so far, in the Library's order
|
|
1019
|
+
* total how many there are in all (null before the first page);
|
|
1020
|
+
* on a preview, the true count, not the count shown
|
|
1021
|
+
* previewOnly a paid collection on a Free plan: `items` are the
|
|
1022
|
+
* preview's only (the ones past it are not listed, and
|
|
1023
|
+
* useLibraryItem on one says `needs_plan`)
|
|
1024
|
+
* hasMore loadMore() has a page to read
|
|
1025
|
+
* loading the first page is out loadingMore a later one is
|
|
1026
|
+
* error, code, unavailable as for every Library hook
|
|
1027
|
+
* loadMore() reads the next page and adds it; nothing when there is
|
|
1028
|
+
* none, or while one is out
|
|
1029
|
+
* reload() reads the first page again
|
|
1030
|
+
*
|
|
1031
|
+
* A new `id`, `q`, `tag` or `limit` starts the list over (a `q` typed key
|
|
1032
|
+
* by key should be debounced by the screen). Nothing is read until there
|
|
1033
|
+
* is an id.
|
|
1034
|
+
*/
|
|
1035
|
+
export function useLibraryItems(id, opts) {
|
|
1036
|
+
const { q, tag, limit, enabled = true } = opts ?? {};
|
|
1037
|
+
const key = JSON.stringify(["items", id ?? null, q ?? "", tag ?? "", limit ?? null]);
|
|
1038
|
+
const { list, ...rest } = useLibraryPage(
|
|
1039
|
+
"items",
|
|
1040
|
+
(cursor, signal) => library.items(id, { q, tag, limit, cursor, signal }),
|
|
1041
|
+
key,
|
|
1042
|
+
Boolean(id) && Boolean(enabled),
|
|
1043
|
+
);
|
|
1044
|
+
return { items: list, ...rest };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* A rows collection's rows, a page at a time (library.rows): the same
|
|
1049
|
+
* state as useLibraryItems, with `rows` ({ id, data, source_url }) in
|
|
1050
|
+
* place of `items`. `filters` is { <column key>: value }, for the columns
|
|
1051
|
+
* the collection marks as filters; a value that is empty is left out, and
|
|
1052
|
+
* the order they are written in does not matter. An `image` cell of a
|
|
1053
|
+
* row's `data` is a signed address, good for ten minutes: reload() to
|
|
1054
|
+
* read the page again when the screen has been open longer.
|
|
1055
|
+
*/
|
|
1056
|
+
export function useLibraryRows(id, opts) {
|
|
1057
|
+
const { q, filters, limit, enabled = true } = opts ?? {};
|
|
1058
|
+
const key = JSON.stringify(["rows", id ?? null, q ?? "", filtersKey(filters), limit ?? null]);
|
|
1059
|
+
const { list, ...rest } = useLibraryPage(
|
|
1060
|
+
"rows",
|
|
1061
|
+
(cursor, signal) => library.rows(id, { q, filters, limit, cursor, signal }),
|
|
1062
|
+
key,
|
|
1063
|
+
Boolean(id) && Boolean(enabled),
|
|
1064
|
+
);
|
|
1065
|
+
return { rows: list, ...rest };
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* One item (library.item): the card's fields as `item`, its file's signed
|
|
1070
|
+
* address as `fileUrl` (good for ten minutes: show or download from it,
|
|
1071
|
+
* never keep it; reload() signs another) and the first 4,000 characters of
|
|
1072
|
+
* its text as `textExcerpt`. An item the member's plan does not open is
|
|
1073
|
+
* `code: "needs_plan"` with the platform's words in `error`, and no
|
|
1074
|
+
* `fileUrl`. Nothing is read until there is an id.
|
|
1075
|
+
* → { item, fileUrl, textExcerpt, loading, error, code, unavailable, reload }
|
|
1076
|
+
*/
|
|
1077
|
+
export function useLibraryItem(id, opts) {
|
|
1078
|
+
const enabled = Boolean(id) && (opts?.enabled ?? true);
|
|
1079
|
+
const key = JSON.stringify(["item", id ?? null]);
|
|
1080
|
+
const { data, ...rest } = useLibraryRead((signal) => library.item(id, { signal }), key, enabled);
|
|
1081
|
+
return { item: data?.item ?? null, fileUrl: data?.file_url ?? null, textExcerpt: data?.text_excerpt ?? null, ...rest };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ---------------------------------------------------------------------------
|
|
1085
|
+
// The app's own chat (0.8.1)
|
|
1086
|
+
|
|
1087
|
+
/** How often the chat looks while the agent works, and while it waits on
|
|
1088
|
+
* the member on the desk. Nothing is polled once a turn has ended. */
|
|
1089
|
+
const CHAT_WORKING_MS = 1500;
|
|
1090
|
+
const CHAT_WAITING_MS = 6000;
|
|
1091
|
+
/** Where the chat keeps its thread for a key (`<kind>:<id>`, or "app"),
|
|
1092
|
+
* so the person's chat is there again when the person is opened again.
|
|
1093
|
+
* This browser's only; the platform keeps the thread itself. */
|
|
1094
|
+
const CHAT_KEY = "claw.chat.";
|
|
1095
|
+
|
|
1096
|
+
function keptThread(key) {
|
|
1097
|
+
if (!key) return null;
|
|
1098
|
+
try {
|
|
1099
|
+
return localStorage.getItem(CHAT_KEY + key) || null;
|
|
1100
|
+
} catch {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Who else reads the same thread in this page, so a reader that was not the
|
|
1106
|
+
// one to start it follows it (2026-10-12): the agent's dock keeps its pane
|
|
1107
|
+
// mounted beside the work, and a button on a screen, the Today ask bar and
|
|
1108
|
+
// the dock are each a useAgentThread on the same key. `from` is the reader
|
|
1109
|
+
// that made the change; it does not hear itself.
|
|
1110
|
+
const THREAD_KEPT = "claw:thread-kept";
|
|
1111
|
+
|
|
1112
|
+
function keepThread(key, sessionId, from) {
|
|
1113
|
+
if (!key) return;
|
|
1114
|
+
try {
|
|
1115
|
+
if (sessionId) localStorage.setItem(CHAT_KEY + key, sessionId);
|
|
1116
|
+
else localStorage.removeItem(CHAT_KEY + key);
|
|
1117
|
+
} catch {
|
|
1118
|
+
/* private mode: the chat starts fresh next time */
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
window.dispatchEvent(new CustomEvent(THREAD_KEPT, { detail: { key, sessionId: sessionId || null, from } }));
|
|
1122
|
+
} catch {
|
|
1123
|
+
/* no window: nobody else is reading */
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const CHAT_LIVE = new Set(["queued", "working"]);
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* The member's own thread with the app's agent, for the app's own chat
|
|
1131
|
+
* (the `agent-chat` block): the same eve session the desk's agent column
|
|
1132
|
+
* shows — one conversation, two views, never a copy.
|
|
1133
|
+
*
|
|
1134
|
+
* const chat = useAgentThread({ row: { kind: "person", id, name } });
|
|
1135
|
+
* <AgentChat chat={chat} title={`Ask about ${name}`} />
|
|
1136
|
+
*
|
|
1137
|
+
* Options (all optional):
|
|
1138
|
+
* row the record the chat is about ({ kind, id, name }): it rides
|
|
1139
|
+
* with every ask as context, and the thread is kept for it
|
|
1140
|
+
* how instructions for the agent only (the AGENTS.md section)
|
|
1141
|
+
* job a declared job's key, or true; by default none — a chat is
|
|
1142
|
+
* a conversation, not a job
|
|
1143
|
+
* key where this browser keeps the thread (default `<kind>:<id>`
|
|
1144
|
+
* of the row, else "app"); null keeps none
|
|
1145
|
+
* Readers on the same key in one page follow each other: one that
|
|
1146
|
+
* starts, drops or switches the thread is heard by the rest (the
|
|
1147
|
+
* dock's pane, the Today ask bar and a screen's buttons share it)
|
|
1148
|
+
* view true asks the door for the thread as the desk's sidebar draws
|
|
1149
|
+
* it (0.8.1, D27: the `agent-pane` block; useAgentPane sets it)
|
|
1150
|
+
*
|
|
1151
|
+
* State:
|
|
1152
|
+
* sessionId the thread, or null before the first ask
|
|
1153
|
+
* state queued | working | needs_you | done | stopped | failed, or
|
|
1154
|
+
* null before the first ask
|
|
1155
|
+
* needs { kind, text, at, in_app, options?, free? } while it waits
|
|
1156
|
+
* on the member. `in_app` is how the app can settle it:
|
|
1157
|
+
* "answer" (a question: answer({ optionId | text }), with
|
|
1158
|
+
* its `options` and whether `free` words are welcome),
|
|
1159
|
+
* "press" (the one ask's yes: the `approval` block's press
|
|
1160
|
+
* frame, useApproval(request.id)), or null (on the desk:
|
|
1161
|
+
* openOnDesk)
|
|
1162
|
+
* turns [{ id, member, parts: [{ type: "text", text } |
|
|
1163
|
+
* { type: "step", label, status }], writing, status }]
|
|
1164
|
+
* pending the member's words just sent, until the thread has them
|
|
1165
|
+
* view with `view: true`, the sidebar's view: { messages, thinking,
|
|
1166
|
+
* failure, questions } (platform.js thread), else null
|
|
1167
|
+
* sameConversation the next message continues this request (D29: also
|
|
1168
|
+
* after a Stop, a finish or a failed turn); false for a plan,
|
|
1169
|
+
* a piece, a change sent up, a counted job (a new request)
|
|
1170
|
+
* place "app", or "makersclaw" for an ask made in Makersclaw (the
|
|
1171
|
+
* chat shows a pointer, not the turns)
|
|
1172
|
+
* request the request the thread is on (as useRequests reads one)
|
|
1173
|
+
* sending, loading, error, code, unavailable (a claw with no door yet)
|
|
1174
|
+
*
|
|
1175
|
+
* Actions: send(text, { files }) · stop() · answer({ optionId, text } | { answers }) ·
|
|
1176
|
+
* openOnDesk() · newChat() · openSession(sessionId) · reload(). answer is
|
|
1177
|
+
* the agent's question answered in the chat where it was asked (0.8.1,
|
|
1178
|
+
* D19): → { ok } | { ok: false, error, code }. `answers` is
|
|
1179
|
+
* [{ requestId, optionId | text }], one per question `view.questions`
|
|
1180
|
+
* lists, sent as one resume (D27). openSession switches to another of the
|
|
1181
|
+
* member's threads (the pane's Chats list). `files` are [{ name, type, data }]
|
|
1182
|
+
* (data URLs, at most 4 and 3 MB together) sent with the words (D27 phase 4);
|
|
1183
|
+
* `notice` says when they did not go (the ask is queued). With `view: true`
|
|
1184
|
+
* a working thread is streamed (platform.js streamThread) and polled as the
|
|
1185
|
+
* safety net.
|
|
1186
|
+
* send continues the thread (ask({ sessionId })) — after a Stop, a finish
|
|
1187
|
+
* or a failed turn too: the platform opens the closed request again, in the
|
|
1188
|
+
* same session, so there is no new card (D29) — and starts a fresh one only
|
|
1189
|
+
* when the old one is a teammate's or gone. The desk's column is never
|
|
1190
|
+
* opened by an ask from here (`open: false`); openOnDesk is the member's
|
|
1191
|
+
* press, and opens it.
|
|
1192
|
+
*/
|
|
1193
|
+
export function useAgentThread({ row, how, job = false, key, view = false } = {}) {
|
|
1194
|
+
const storeKey = key === null ? null : (key ?? (row?.kind && row?.id ? `${row.kind}:${row.id}` : "app"));
|
|
1195
|
+
const [sessionId, setSessionId] = useState(() => keptThread(storeKey));
|
|
1196
|
+
const me = useRef({}).current;
|
|
1197
|
+
const [state, setState] = useState({
|
|
1198
|
+
data: null,
|
|
1199
|
+
loading: Boolean(sessionId),
|
|
1200
|
+
sending: false,
|
|
1201
|
+
pending: null,
|
|
1202
|
+
error: null,
|
|
1203
|
+
code: null,
|
|
1204
|
+
unavailable: false,
|
|
1205
|
+
});
|
|
1206
|
+
const [tick, setTick] = useState(0);
|
|
1207
|
+
const reload = useCallback(() => setTick((t) => t + 1), []);
|
|
1208
|
+
const aliveRef = useRef(true);
|
|
1209
|
+
const sessionRef = useRef(sessionId);
|
|
1210
|
+
sessionRef.current = sessionId;
|
|
1211
|
+
const rowRef = useRef(row);
|
|
1212
|
+
rowRef.current = row;
|
|
1213
|
+
const viewRef = useRef(view);
|
|
1214
|
+
viewRef.current = view;
|
|
1215
|
+
// Words sent and not in the thread yet: the agent's turn has not opened,
|
|
1216
|
+
// so the chat keeps looking (for a minute at most) though the last turn
|
|
1217
|
+
// has ended.
|
|
1218
|
+
const pendingRef = useRef(null);
|
|
1219
|
+
pendingRef.current = state.pending ? (pendingRef.current?.words === state.pending ? pendingRef.current : { words: state.pending, at: Date.now() }) : null;
|
|
1220
|
+
|
|
1221
|
+
useEffect(() => {
|
|
1222
|
+
aliveRef.current = true;
|
|
1223
|
+
return () => {
|
|
1224
|
+
aliveRef.current = false;
|
|
1225
|
+
};
|
|
1226
|
+
}, []);
|
|
1227
|
+
|
|
1228
|
+
// Another record opened in the same chat: its own thread.
|
|
1229
|
+
const lastKey = useRef(storeKey);
|
|
1230
|
+
useEffect(() => {
|
|
1231
|
+
if (lastKey.current === storeKey) return;
|
|
1232
|
+
lastKey.current = storeKey;
|
|
1233
|
+
const kept = keptThread(storeKey);
|
|
1234
|
+
setSessionId(kept);
|
|
1235
|
+
setState({ data: null, loading: Boolean(kept), sending: false, pending: null, error: null, code: null, unavailable: false });
|
|
1236
|
+
}, [storeKey]);
|
|
1237
|
+
|
|
1238
|
+
// Another reader of this thread started it, dropped it or opened another
|
|
1239
|
+
// chat in it: follow (a pane that stays mounted beside the work, the Today
|
|
1240
|
+
// ask bar, a button on a screen). The one that made the change does not
|
|
1241
|
+
// hear itself.
|
|
1242
|
+
useEffect(() => {
|
|
1243
|
+
if (!storeKey || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
|
|
1244
|
+
const on = (e) => {
|
|
1245
|
+
const d = e.detail;
|
|
1246
|
+
if (!d || d.key !== storeKey || d.from === me) return;
|
|
1247
|
+
const next = d.sessionId ?? null;
|
|
1248
|
+
if (next === sessionRef.current) return;
|
|
1249
|
+
setSessionId(next);
|
|
1250
|
+
setState({ data: null, loading: Boolean(next), sending: false, pending: null, error: null, code: null, unavailable: false });
|
|
1251
|
+
};
|
|
1252
|
+
window.addEventListener(THREAD_KEPT, on);
|
|
1253
|
+
return () => window.removeEventListener(THREAD_KEPT, on);
|
|
1254
|
+
}, [storeKey, me]);
|
|
1255
|
+
|
|
1256
|
+
const forget = useCallback(() => {
|
|
1257
|
+
keepThread(storeKey, null, me);
|
|
1258
|
+
setSessionId(null);
|
|
1259
|
+
}, [storeKey, me]);
|
|
1260
|
+
|
|
1261
|
+
// Follow the thread: at once, then while the agent works or waits. With
|
|
1262
|
+
// the sidebar's view (the `agent-pane` block) a working thread is also
|
|
1263
|
+
// STREAMED (0.8.1, D27 phase 4): the gateway pushes the thread after each
|
|
1264
|
+
// burst of the agent's events, so text and steps appear as they happen.
|
|
1265
|
+
// The poll stays as the safety net: it looks every few seconds while the
|
|
1266
|
+
// stream is alive and takes over when it ends, breaks, or does not exist.
|
|
1267
|
+
useEffect(() => {
|
|
1268
|
+
if (!sessionId) return undefined;
|
|
1269
|
+
const controller = new AbortController();
|
|
1270
|
+
let alive = true;
|
|
1271
|
+
let timer = null;
|
|
1272
|
+
let inflight = false;
|
|
1273
|
+
let streaming = false;
|
|
1274
|
+
let streamOff = false;
|
|
1275
|
+
let quickEnds = 0;
|
|
1276
|
+
let lastLine = 0;
|
|
1277
|
+
let streamStop = null;
|
|
1278
|
+
const schedule = (ms) => {
|
|
1279
|
+
clearTimeout(timer);
|
|
1280
|
+
timer = null;
|
|
1281
|
+
if (alive && ms && screen.shown()) timer = setTimeout(load, ms);
|
|
1282
|
+
};
|
|
1283
|
+
const wordsLanded = (data, words) =>
|
|
1284
|
+
Boolean(words) && ((data.turns ?? []).some((t) => t.member === words) || (data.view?.messages ?? []).some((m) => m.role === "user" && m.text === words));
|
|
1285
|
+
const applyFrame = (frame) => {
|
|
1286
|
+
if (!alive) return;
|
|
1287
|
+
if (frame.desk_origin) publishDesk(frame.desk_origin);
|
|
1288
|
+
setState((s) => ({
|
|
1289
|
+
...s,
|
|
1290
|
+
data: { ...(s.data ?? {}), ...frame, turns: s.data?.turns ?? [] },
|
|
1291
|
+
loading: false,
|
|
1292
|
+
error: null,
|
|
1293
|
+
code: null,
|
|
1294
|
+
unavailable: false,
|
|
1295
|
+
pending: wordsLanded(frame, s.pending) ? null : s.pending,
|
|
1296
|
+
}));
|
|
1297
|
+
};
|
|
1298
|
+
const openStream = () => {
|
|
1299
|
+
if (!viewRef.current || streaming || streamOff || !alive || !screen.shown()) return;
|
|
1300
|
+
streaming = true;
|
|
1301
|
+
lastLine = Date.now();
|
|
1302
|
+
const began = Date.now();
|
|
1303
|
+
const stop = new AbortController();
|
|
1304
|
+
streamStop = stop;
|
|
1305
|
+
// No line (not even a ping) for 25 s: the stream is dead, not quiet.
|
|
1306
|
+
const watchdog = setInterval(() => {
|
|
1307
|
+
if (Date.now() - lastLine > 25000) stop.abort();
|
|
1308
|
+
}, 5000);
|
|
1309
|
+
streamThread(sessionId, { signal: stop.signal, onLine: () => (lastLine = Date.now()), onThread: applyFrame }).then((r) => {
|
|
1310
|
+
clearInterval(watchdog);
|
|
1311
|
+
streaming = false;
|
|
1312
|
+
streamStop = null;
|
|
1313
|
+
if (!alive) return;
|
|
1314
|
+
// A claw with no stream, or a thread it will not stream: poll.
|
|
1315
|
+
if (!r.ok && !r.aborted && (r.unavailable || r.status === 404 || r.status === 403)) streamOff = true;
|
|
1316
|
+
// Three streams in a row that ended at once: stop trying.
|
|
1317
|
+
quickEnds = Date.now() - began < 2000 ? quickEnds + 1 : 0;
|
|
1318
|
+
if (quickEnds >= 3) streamOff = true;
|
|
1319
|
+
load(); // the canonical read, and a new stream if it is still going
|
|
1320
|
+
});
|
|
1321
|
+
};
|
|
1322
|
+
const load = async () => {
|
|
1323
|
+
clearTimeout(timer);
|
|
1324
|
+
timer = null;
|
|
1325
|
+
if (!alive || inflight || !screen.shown()) return;
|
|
1326
|
+
// While a stream is alive it is the fresher view: look rarely.
|
|
1327
|
+
if (streaming && Date.now() - lastLine < 20000) {
|
|
1328
|
+
schedule(CHAT_WORKING_MS * 3);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
inflight = true;
|
|
1332
|
+
const r = await readThread(sessionId, { signal: controller.signal, view: viewRef.current });
|
|
1333
|
+
inflight = false;
|
|
1334
|
+
if (!alive || r.aborted) return;
|
|
1335
|
+
if (r.ok && r.data) {
|
|
1336
|
+
const data = r.data;
|
|
1337
|
+
if (data.desk_origin) publishDesk(data.desk_origin);
|
|
1338
|
+
setState((s) => ({
|
|
1339
|
+
...s,
|
|
1340
|
+
data,
|
|
1341
|
+
loading: false,
|
|
1342
|
+
error: null,
|
|
1343
|
+
code: null,
|
|
1344
|
+
unavailable: false,
|
|
1345
|
+
pending: wordsLanded(data, s.pending) ? null : s.pending,
|
|
1346
|
+
}));
|
|
1347
|
+
const turns = data.turns ?? [];
|
|
1348
|
+
const last = turns[turns.length - 1];
|
|
1349
|
+
const waitingForTurn = pendingRef.current && Date.now() - pendingRef.current.at < 60000;
|
|
1350
|
+
const working = CHAT_LIVE.has(data.state) || last?.status === "working" || Boolean(waitingForTurn);
|
|
1351
|
+
if (working && CHAT_LIVE.has(data.state)) openStream();
|
|
1352
|
+
schedule(working ? CHAT_WORKING_MS : data.state === "needs_you" ? CHAT_WAITING_MS : 0);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (r.status === 403 || r.status === 404) {
|
|
1356
|
+
if (r.code) {
|
|
1357
|
+
// A teammate's thread, or one this app no longer has: start fresh.
|
|
1358
|
+
forget();
|
|
1359
|
+
setState((s) => ({ ...s, data: null, loading: false }));
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
setState((s) => ({ ...s, loading: false, unavailable: true }));
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
setState((s) => ({ ...s, loading: false, error: r.error ?? null }));
|
|
1366
|
+
schedule(CHAT_WORKING_MS * 4); // a blip: look again, a little later
|
|
1367
|
+
};
|
|
1368
|
+
const screen = watchShown(load);
|
|
1369
|
+
load();
|
|
1370
|
+
return () => {
|
|
1371
|
+
alive = false;
|
|
1372
|
+
controller.abort();
|
|
1373
|
+
streamStop?.abort();
|
|
1374
|
+
clearTimeout(timer);
|
|
1375
|
+
screen.stop();
|
|
1376
|
+
};
|
|
1377
|
+
}, [sessionId, tick, forget]);
|
|
1378
|
+
|
|
1379
|
+
const send = useCallback(
|
|
1380
|
+
async (message, { files } = {}) => {
|
|
1381
|
+
const text = String(message ?? "").trim();
|
|
1382
|
+
if (!text) return { ok: false, error: "nothing to ask" };
|
|
1383
|
+
setState((s) => ({ ...s, sending: true, pending: text, error: null, code: null, notice: null }));
|
|
1384
|
+
const asking = (session) =>
|
|
1385
|
+
ask({ text, row: rowRef.current ?? undefined, how, job, files, sessionId: session ?? undefined, open: false });
|
|
1386
|
+
let result = await asking(sessionRef.current);
|
|
1387
|
+
if (!result.ok && sessionRef.current && START_FRESH.has(result.code)) result = await asking(null);
|
|
1388
|
+
if (!aliveRef.current) return result;
|
|
1389
|
+
if (!result.ok) {
|
|
1390
|
+
const unavailable = result.status === 404 && !result.code;
|
|
1391
|
+
setState((s) => ({
|
|
1392
|
+
...s,
|
|
1393
|
+
sending: false,
|
|
1394
|
+
pending: null,
|
|
1395
|
+
error: unavailable ? null : (result.error ?? "The ask could not be sent."),
|
|
1396
|
+
code: result.code ?? null,
|
|
1397
|
+
unavailable: unavailable || s.unavailable,
|
|
1398
|
+
}));
|
|
1399
|
+
return result;
|
|
1400
|
+
}
|
|
1401
|
+
announceAsk();
|
|
1402
|
+
const request = result.data?.request ?? null;
|
|
1403
|
+
const next = request?.app_session_id ?? sessionRef.current ?? null;
|
|
1404
|
+
if (next) {
|
|
1405
|
+
keepThread(storeKey, next, me);
|
|
1406
|
+
if (next !== sessionRef.current) setSessionId(next);
|
|
1407
|
+
else reload();
|
|
1408
|
+
}
|
|
1409
|
+
setState((s) => ({
|
|
1410
|
+
...s,
|
|
1411
|
+
sending: false,
|
|
1412
|
+
// The ask was taken but waits its turn: a file is not kept for later.
|
|
1413
|
+
notice:
|
|
1414
|
+
result.data?.files_attached === false
|
|
1415
|
+
? "Your files did not go with that message because it is waiting its turn. Attach them again once it starts."
|
|
1416
|
+
: null,
|
|
1417
|
+
data: s.data && next === s.data.session_id ? { ...s.data, state: "working", request: request ?? s.data.request } : s.data,
|
|
1418
|
+
}));
|
|
1419
|
+
return result;
|
|
1420
|
+
},
|
|
1421
|
+
[how, job, storeKey, reload, me],
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
const request = state.data?.request ?? null;
|
|
1425
|
+
const stop = useCallback(async () => {
|
|
1426
|
+
if (!request?.id) return { ok: false, error: "nothing to stop" };
|
|
1427
|
+
const r = await stopRequest(request.id);
|
|
1428
|
+
reload();
|
|
1429
|
+
return r;
|
|
1430
|
+
}, [request?.id, reload]);
|
|
1431
|
+
const needsAt = state.data?.needs?.at ?? null;
|
|
1432
|
+
const answer = useCallback(
|
|
1433
|
+
async ({ optionId, text, answers } = {}) => {
|
|
1434
|
+
if (!request?.id) return { ok: false, error: "nothing to answer" };
|
|
1435
|
+
const r = await sendAnswer(request.id, { optionId, text, answers, at: needsAt ?? undefined });
|
|
1436
|
+
if (r.ok) {
|
|
1437
|
+
announceAsk();
|
|
1438
|
+
setState((s) => (s.data ? { ...s, error: null, data: { ...s.data, state: "working", needs: null } } : s));
|
|
1439
|
+
}
|
|
1440
|
+
reload();
|
|
1441
|
+
return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
|
|
1442
|
+
},
|
|
1443
|
+
[request?.id, needsAt, reload],
|
|
1444
|
+
);
|
|
1445
|
+
const openOnDesk = useCallback(() => (request ? openRequest(request) : false), [request]);
|
|
1446
|
+
const newChat = useCallback(() => {
|
|
1447
|
+
forget();
|
|
1448
|
+
setState({ data: null, loading: false, sending: false, pending: null, error: null, code: null, unavailable: false });
|
|
1449
|
+
}, [forget]);
|
|
1450
|
+
// Another of the member's threads in this app (the pane's Chats list):
|
|
1451
|
+
// kept as this chat's, so the same record opens on it next time.
|
|
1452
|
+
const openSession = useCallback(
|
|
1453
|
+
(next) => {
|
|
1454
|
+
const id = typeof next === "string" && next ? next : null;
|
|
1455
|
+
if (!id || id === sessionRef.current) return;
|
|
1456
|
+
keepThread(storeKey, id, me);
|
|
1457
|
+
setSessionId(id);
|
|
1458
|
+
setState({ data: null, loading: true, sending: false, pending: null, error: null, code: null, unavailable: false });
|
|
1459
|
+
},
|
|
1460
|
+
[storeKey, me],
|
|
1461
|
+
);
|
|
1462
|
+
|
|
1463
|
+
const data = state.data;
|
|
1464
|
+
return {
|
|
1465
|
+
sessionId,
|
|
1466
|
+
state: data?.state ?? (state.sending ? "working" : null),
|
|
1467
|
+
needs: data?.needs ?? null,
|
|
1468
|
+
turns: data?.turns ?? [],
|
|
1469
|
+
view: data?.view ?? null,
|
|
1470
|
+
// Does the next message go on in this same request (D29)? True while it
|
|
1471
|
+
// is open and after a Stop, a finish or a failed turn (the platform
|
|
1472
|
+
// opens the closed request again, in the same session), false only for
|
|
1473
|
+
// a plan or a piece of one, a change sent up, a counted job and a
|
|
1474
|
+
// hand-off never started: those get a new request. Unknown (an older
|
|
1475
|
+
// claw, or before the first read): true.
|
|
1476
|
+
sameConversation: data?.same_conversation ?? true,
|
|
1477
|
+
place: data?.thread ?? "app",
|
|
1478
|
+
request,
|
|
1479
|
+
pending: state.pending,
|
|
1480
|
+
sending: state.sending,
|
|
1481
|
+
loading: state.loading,
|
|
1482
|
+
error: state.error,
|
|
1483
|
+
code: state.code,
|
|
1484
|
+
// A line to show beside the thread (files that did not go), or null.
|
|
1485
|
+
notice: state.notice ?? null,
|
|
1486
|
+
unavailable: state.unavailable,
|
|
1487
|
+
send,
|
|
1488
|
+
stop,
|
|
1489
|
+
answer,
|
|
1490
|
+
openOnDesk,
|
|
1491
|
+
newChat,
|
|
1492
|
+
openSession,
|
|
1493
|
+
reload,
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** How often the Chats list looks while a thread is going, and otherwise. */
|
|
1498
|
+
const CHATS_WORKING_MS = 6000;
|
|
1499
|
+
const CHATS_IDLE_MS = 30000;
|
|
1500
|
+
|
|
1501
|
+
/** A thread's request status as the Chats list marks it: waiting on the
|
|
1502
|
+
* member, or the agent is on it. */
|
|
1503
|
+
function chatMark(status) {
|
|
1504
|
+
if (status === "needs_you") return "needs";
|
|
1505
|
+
if (status === "queued" || status === "routing" || status === "installing" || status === "setting_up" || status === "building" || status === "working") return "working";
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* The member's own threads in this app, newest first, for the `agent-pane`
|
|
1511
|
+
* block's Chats list (GET _claw/threads, 0.8.1, D27):
|
|
1512
|
+
*
|
|
1513
|
+
* const { items, loading, error, unavailable } = useThreadList();
|
|
1514
|
+
*
|
|
1515
|
+
* items [{ sessionId, requestId, title, mark: "needs" | "working" | null, at }]
|
|
1516
|
+
*
|
|
1517
|
+
* Looks every 30 s, every 6 s while one is going or waits, and at once
|
|
1518
|
+
* when an ask is sent. A claw from before the door is `unavailable`: the
|
|
1519
|
+
* pane draws no list.
|
|
1520
|
+
*/
|
|
1521
|
+
export function useThreadList({ every = CHATS_IDLE_MS } = {}) {
|
|
1522
|
+
const watched = useWatched((signal) => readThreads({ signal }), "threads", {
|
|
1523
|
+
next: (data) => (listOf(data?.threads).some((t) => chatMark(t?.status)) ? CHATS_WORKING_MS : every),
|
|
1524
|
+
listen: onAsked,
|
|
1525
|
+
});
|
|
1526
|
+
const items = useMemo(
|
|
1527
|
+
() =>
|
|
1528
|
+
listOf(watched.data?.threads)
|
|
1529
|
+
.filter((t) => t && typeof t.session_id === "string")
|
|
1530
|
+
.map((t) => ({
|
|
1531
|
+
sessionId: t.session_id,
|
|
1532
|
+
requestId: t.request_id ?? null,
|
|
1533
|
+
title: typeof t.title === "string" && t.title.trim() ? t.title.trim() : "Untitled",
|
|
1534
|
+
mark: chatMark(t.status),
|
|
1535
|
+
at: t.updated_at ?? null,
|
|
1536
|
+
})),
|
|
1537
|
+
[watched.data],
|
|
1538
|
+
);
|
|
1539
|
+
return { items, loading: watched.loading, error: watched.error, unavailable: watched.unavailable, reload: watched.reload };
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* The desk's agent sidebar, as the app's own pane (0.8.1, D27; the
|
|
1544
|
+
* `agent-pane` block). useAgentThread with the sidebar's view, plus what
|
|
1545
|
+
* the sidebar has around its chat:
|
|
1546
|
+
*
|
|
1547
|
+
* const chat = useAgentPane({ row: { kind: "person", id, name } });
|
|
1548
|
+
* <AgentPane chat={chat} title={`Ask about ${name}`} />
|
|
1549
|
+
*
|
|
1550
|
+
* Everything useAgentThread returns (`view` is set), and
|
|
1551
|
+
*
|
|
1552
|
+
* chats { items, loading, error, unavailable }: the member's
|
|
1553
|
+
* threads in this app (useThreadList); openSession(id)
|
|
1554
|
+
* switches to one
|
|
1555
|
+
* callDetail (callId) → Promise<{ ok, call }>: one step of this
|
|
1556
|
+
* thread, opened (platform.js threadCall); the pane asks it
|
|
1557
|
+
* when a member opens a step
|
|
1558
|
+
* openRequest (requestId): opens a request's thread on the desk
|
|
1559
|
+
* open ({ kind: "app", id } | { kind: "file", path, where,
|
|
1560
|
+
* appRef }): opens an app or a file it made on the desk
|
|
1561
|
+
* (the pane's default "Open")
|
|
1562
|
+
*
|
|
1563
|
+
* Options are useAgentThread's: `row` (the record on screen, kept as that
|
|
1564
|
+
* record's own thread), `how` (for the agent only), `job`, `key`.
|
|
1565
|
+
*/
|
|
1566
|
+
export function useAgentPane(options = {}) {
|
|
1567
|
+
const chat = useAgentThread({ ...options, view: true });
|
|
1568
|
+
const list = useThreadList();
|
|
1569
|
+
const sessionId = chat.sessionId;
|
|
1570
|
+
const callDetail = useCallback(
|
|
1571
|
+
(callId) => (sessionId ? readThreadCall(sessionId, callId) : Promise.resolve({ ok: false, status: 0, error: "no thread", unavailable: false })),
|
|
1572
|
+
[sessionId],
|
|
1573
|
+
);
|
|
1574
|
+
return {
|
|
1575
|
+
...chat,
|
|
1576
|
+
chats: { items: list.items, loading: list.loading, error: list.error, unavailable: list.unavailable },
|
|
1577
|
+
callDetail,
|
|
1578
|
+
// "Open in Makersclaw" under a change the agent sent up: the desk opens
|
|
1579
|
+
// that request's thread (a member's press).
|
|
1580
|
+
openRequest: (id) => openRequest(id),
|
|
1581
|
+
// "Open" under an app or a file the agent made (D27 phase 4): the desk
|
|
1582
|
+
// opens it, on the member's press. → true when the desk was asked.
|
|
1583
|
+
open: (target) => (target?.kind === "app" ? openApp(target.id) : target?.kind === "file" ? openFile(target) : false),
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// ---------------------------------------------------------------------------
|
|
1588
|
+
// The kit (0.8.1, D15): the app shows its agent's work itself. Each hook
|
|
1589
|
+
// feeds one or more of the kit's blocks; the blocks take the hook's
|
|
1590
|
+
// answer as props and never read a door themselves.
|
|
1591
|
+
|
|
1592
|
+
/** What a kit read shows before its first answer, and with nothing to
|
|
1593
|
+
* read (`enabled` false, no id). `key` names the read the state belongs
|
|
1594
|
+
* to: a state kept for another read is not shown. */
|
|
1595
|
+
const WATCH_READING = { key: null, data: null, loading: true, error: null, code: null, unavailable: false };
|
|
1596
|
+
const WATCH_NOTHING = { ...WATCH_READING, loading: false };
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* One kit door, followed: read when `key` changes and on reload(), then
|
|
1600
|
+
* again after `next(data)` ms while the page is shown (0 or nothing: no
|
|
1601
|
+
* timer). Hidden, nothing is asked; coming back looks at once. A missing
|
|
1602
|
+
* door (`unavailable`) is looked at once a minute and leaves `error`
|
|
1603
|
+
* null; a refusal keeps the last answer and sets `error` and `code`. A
|
|
1604
|
+
* `listen` subscription (onAsked) makes it look at once. Aborts what is
|
|
1605
|
+
* out on unmount and when the key changes.
|
|
1606
|
+
* → { data, loading, error, code, unavailable, reload }
|
|
1607
|
+
*/
|
|
1608
|
+
function useWatched(read, key, { enabled = true, next, listen } = {}) {
|
|
1609
|
+
const [state, setState] = useState(WATCH_READING);
|
|
1610
|
+
const [tick, setTick] = useState(0);
|
|
1611
|
+
const readRef = useRef(read);
|
|
1612
|
+
readRef.current = read;
|
|
1613
|
+
const nextRef = useRef(next);
|
|
1614
|
+
nextRef.current = next;
|
|
1615
|
+
const reload = useCallback(() => setTick((t) => t + 1), []);
|
|
1616
|
+
|
|
1617
|
+
useEffect(() => {
|
|
1618
|
+
if (!enabled) return undefined;
|
|
1619
|
+
const controller = new AbortController();
|
|
1620
|
+
let alive = true;
|
|
1621
|
+
let timer = null;
|
|
1622
|
+
let inflight = false;
|
|
1623
|
+
let again = false;
|
|
1624
|
+
let unavailable = false;
|
|
1625
|
+
let last = null;
|
|
1626
|
+
|
|
1627
|
+
const schedule = () => {
|
|
1628
|
+
clearTimeout(timer);
|
|
1629
|
+
timer = null;
|
|
1630
|
+
if (!alive || !screen.shown()) return;
|
|
1631
|
+
if (again) {
|
|
1632
|
+
again = false;
|
|
1633
|
+
load();
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const ms = unavailable ? BACKOFF_MS : Number(nextRef.current?.(last)) || 0;
|
|
1637
|
+
if (ms > 0) timer = setTimeout(load, ms);
|
|
1638
|
+
};
|
|
1639
|
+
const load = async () => {
|
|
1640
|
+
clearTimeout(timer);
|
|
1641
|
+
timer = null;
|
|
1642
|
+
if (!alive || !screen.shown()) return;
|
|
1643
|
+
if (inflight) {
|
|
1644
|
+
again = true;
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
inflight = true;
|
|
1648
|
+
const r = await readRef.current(controller.signal);
|
|
1649
|
+
inflight = false;
|
|
1650
|
+
if (!alive || r?.aborted) return;
|
|
1651
|
+
unavailable = Boolean(r?.unavailable);
|
|
1652
|
+
if (r?.ok && r.data && typeof r.data === "object" && !Array.isArray(r.data)) {
|
|
1653
|
+
last = r.data;
|
|
1654
|
+
setState({ key, data: r.data, loading: false, error: null, code: null, unavailable: false });
|
|
1655
|
+
} else {
|
|
1656
|
+
const why = r?.ok
|
|
1657
|
+
? { error: "the platform answered something unexpected", code: null, unavailable: false }
|
|
1658
|
+
: unavailable
|
|
1659
|
+
? { error: null, code: null, unavailable: true }
|
|
1660
|
+
: { error: r?.error || "the platform did not answer", code: r?.code ?? null, unavailable: false };
|
|
1661
|
+
setState((s) => ({ key, data: s.key === key ? s.data : null, loading: false, ...why }));
|
|
1662
|
+
}
|
|
1663
|
+
schedule();
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
const screen = watchShown(load);
|
|
1667
|
+
const unsubscribe = listen ? listen(load) : null;
|
|
1668
|
+
load();
|
|
1669
|
+
return () => {
|
|
1670
|
+
alive = false;
|
|
1671
|
+
controller.abort();
|
|
1672
|
+
clearTimeout(timer);
|
|
1673
|
+
screen.stop();
|
|
1674
|
+
unsubscribe?.();
|
|
1675
|
+
};
|
|
1676
|
+
}, [key, enabled, tick]);
|
|
1677
|
+
|
|
1678
|
+
const mine = enabled ? (state.key === key ? state : WATCH_READING) : WATCH_NOTHING;
|
|
1679
|
+
return { data: mine.data, loading: mine.loading, error: mine.error, code: mine.code, unavailable: mine.unavailable, reload };
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/** How often useRuns and useJobs look while a run is going. */
|
|
1683
|
+
const RUNNING_MS = 5000;
|
|
1684
|
+
/** How often useRun looks at a run that is going. */
|
|
1685
|
+
const RUN_FOLLOW_MS = 2500;
|
|
1686
|
+
/** How often useConnections looks while an account is being connected,
|
|
1687
|
+
* and for how long. */
|
|
1688
|
+
const CONNECTING_MS = 3000;
|
|
1689
|
+
const CONNECTING_FOR_MS = 120000;
|
|
1690
|
+
|
|
1691
|
+
const NO_MAY = Object.freeze({ run: false, switch: false, pause: false });
|
|
1692
|
+
const listOf = (value) => (Array.isArray(value) ? value : NONE);
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* The app's declared jobs (its schedule), for the `schedule-card` and
|
|
1696
|
+
* `workflow-view` blocks (GET _claw/jobs):
|
|
1697
|
+
*
|
|
1698
|
+
* const { jobs, app, budget, may, busy, runNow, setEnabled } = useJobs();
|
|
1699
|
+
*
|
|
1700
|
+
* jobs [{ id, name, kind, topic, does, cron, tz, words, enabled,
|
|
1701
|
+
* next_run_at, last_run_at, last_run, plan }]
|
|
1702
|
+
* app { status, live } or null: live is the app's Live switch
|
|
1703
|
+
* budget { week_cap_credits, week_used_credits } or null (no cap)
|
|
1704
|
+
* credits the workspace's credits, as _claw/me gives them, or null
|
|
1705
|
+
* may { run, switch, pause }: what this member may press. Hide a
|
|
1706
|
+
* control they may not; the door refuses it anyway
|
|
1707
|
+
* busy { [job id]: "run" | "switch" } while a press is out
|
|
1708
|
+
* appBusy "pause" | "resume" while the Live switch's press is out
|
|
1709
|
+
* loading, error, code, unavailable, reload
|
|
1710
|
+
*
|
|
1711
|
+
* Actions, each resolving the platform call's answer and looking again
|
|
1712
|
+
* after: runNow(id) (spends: show its price on the button), setEnabled(id,
|
|
1713
|
+
* on), pause(), resume(). Looks every `every` ms (30 s), every 5 s while
|
|
1714
|
+
* a job's last run is going.
|
|
1715
|
+
*/
|
|
1716
|
+
export function useJobs({ every = 30000 } = {}) {
|
|
1717
|
+
const watched = useWatched((signal) => readJobs({ signal }), "jobs", {
|
|
1718
|
+
next: (data) => (listOf(data?.jobs).some((j) => j?.last_run?.status === "running") ? RUNNING_MS : every),
|
|
1719
|
+
listen: onAsked,
|
|
1720
|
+
});
|
|
1721
|
+
const { data, reload } = watched;
|
|
1722
|
+
const [busy, setBusy] = useState({});
|
|
1723
|
+
const [appBusy, setAppBusy] = useState(null);
|
|
1724
|
+
const [patched, setPatched] = useState({}); // jobs as a switch's answer gave them, until the next read
|
|
1725
|
+
const aliveRef = useRef(true);
|
|
1726
|
+
useEffect(() => {
|
|
1727
|
+
aliveRef.current = true;
|
|
1728
|
+
return () => {
|
|
1729
|
+
aliveRef.current = false;
|
|
1730
|
+
};
|
|
1731
|
+
}, []);
|
|
1732
|
+
useEffect(() => setPatched((p) => (Object.keys(p).length ? {} : p)), [data]);
|
|
1733
|
+
|
|
1734
|
+
const mark = useCallback((id, what) => {
|
|
1735
|
+
if (!aliveRef.current) return;
|
|
1736
|
+
setBusy((b) => {
|
|
1737
|
+
const nextBusy = { ...b };
|
|
1738
|
+
if (what) nextBusy[id] = what;
|
|
1739
|
+
else delete nextBusy[id];
|
|
1740
|
+
return nextBusy;
|
|
1741
|
+
});
|
|
1742
|
+
}, []);
|
|
1743
|
+
|
|
1744
|
+
const runNow = useCallback(
|
|
1745
|
+
async (id) => {
|
|
1746
|
+
mark(id, "run");
|
|
1747
|
+
const r = await startRun(id);
|
|
1748
|
+
mark(id, null);
|
|
1749
|
+
if (r.ok) announceAsk(); // the requests and the runs on the page look at once
|
|
1750
|
+
if (aliveRef.current) reload();
|
|
1751
|
+
return r;
|
|
1752
|
+
},
|
|
1753
|
+
[mark, reload],
|
|
1754
|
+
);
|
|
1755
|
+
|
|
1756
|
+
const setEnabled = useCallback(
|
|
1757
|
+
async (id, on) => {
|
|
1758
|
+
mark(id, "switch");
|
|
1759
|
+
const r = await setJob(id, { enabled: Boolean(on) });
|
|
1760
|
+
mark(id, null);
|
|
1761
|
+
if (!aliveRef.current) return r;
|
|
1762
|
+
if (r.ok && r.data?.job?.id) setPatched((p) => ({ ...p, [r.data.job.id]: r.data.job }));
|
|
1763
|
+
reload();
|
|
1764
|
+
return r;
|
|
1765
|
+
},
|
|
1766
|
+
[mark, reload],
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1769
|
+
const live = useCallback(
|
|
1770
|
+
async (on) => {
|
|
1771
|
+
setAppBusy(on ? "resume" : "pause");
|
|
1772
|
+
const r = await (on ? resumeApp() : pauseApp());
|
|
1773
|
+
if (!aliveRef.current) return r;
|
|
1774
|
+
setAppBusy(null);
|
|
1775
|
+
reload();
|
|
1776
|
+
return r;
|
|
1777
|
+
},
|
|
1778
|
+
[reload],
|
|
1779
|
+
);
|
|
1780
|
+
const pause = useCallback(() => live(false), [live]);
|
|
1781
|
+
const resume = useCallback(() => live(true), [live]);
|
|
1782
|
+
|
|
1783
|
+
const jobs = useMemo(() => listOf(data?.jobs).map((j) => (j && patched[j.id] ? { ...j, ...patched[j.id] } : j)), [data, patched]);
|
|
1784
|
+
return {
|
|
1785
|
+
jobs,
|
|
1786
|
+
app: data?.app ?? null,
|
|
1787
|
+
budget: data?.budget ?? null,
|
|
1788
|
+
credits: data?.credits ?? null,
|
|
1789
|
+
may: data?.may ? { ...NO_MAY, ...data.may } : NO_MAY,
|
|
1790
|
+
busy,
|
|
1791
|
+
appBusy,
|
|
1792
|
+
loading: watched.loading,
|
|
1793
|
+
error: watched.error,
|
|
1794
|
+
code: watched.code,
|
|
1795
|
+
unavailable: watched.unavailable,
|
|
1796
|
+
reload,
|
|
1797
|
+
runNow,
|
|
1798
|
+
setEnabled,
|
|
1799
|
+
pause,
|
|
1800
|
+
resume,
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* The app's runs, newest first, and their overview, for the `run-history`
|
|
1806
|
+
* and `workflow-view` blocks (GET _claw/runs):
|
|
1807
|
+
*
|
|
1808
|
+
* const { runs, overview } = useRuns({ job: wakeId, limit: 20 });
|
|
1809
|
+
*
|
|
1810
|
+
* runs [{ id, title, state, wake_id, job, trigger, by, number,
|
|
1811
|
+
* started_at, ended_at, duration_ms, credits, report, status }]
|
|
1812
|
+
* `state` is running | done | failed | stopped | needs_you
|
|
1813
|
+
* overview { completed, failed, running, avg_ms, credits_week,
|
|
1814
|
+
* cap_week_credits } or null
|
|
1815
|
+
* loading, error, code, unavailable, reload
|
|
1816
|
+
*
|
|
1817
|
+
* `job` narrows to one job (its wake id), `limit` (20, at most 60). The
|
|
1818
|
+
* jobs' runs and the member's own asks, never a teammate's ask. Looks
|
|
1819
|
+
* every 5 s while a run is going, else every `every` ms (30 s), and at
|
|
1820
|
+
* once after useJobs' Run now.
|
|
1821
|
+
*/
|
|
1822
|
+
export function useRuns({ job, limit = 20, every = 30000 } = {}) {
|
|
1823
|
+
const key = JSON.stringify(["runs", job ?? null, limit ?? null]);
|
|
1824
|
+
const watched = useWatched((signal) => readRuns({ job, limit, signal }), key, {
|
|
1825
|
+
next: (data) => (listOf(data?.runs).some((r) => r?.state === "running") ? RUNNING_MS : every),
|
|
1826
|
+
listen: onAsked,
|
|
1827
|
+
});
|
|
1828
|
+
const { data, ...rest } = watched;
|
|
1829
|
+
return { runs: listOf(data?.runs), overview: data?.overview ?? null, ...rest };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* One run's account, for the `run-history` block's RunAccount and the
|
|
1834
|
+
* `workflow-view` graph (GET _claw/runs/<id>):
|
|
1835
|
+
*
|
|
1836
|
+
* const { run, steps, text, needs } = useRun(selectedId);
|
|
1837
|
+
*
|
|
1838
|
+
* run the run, as useRuns lists it, or null
|
|
1839
|
+
* steps [{ label, status: running | done | failed }], in order
|
|
1840
|
+
* text the agent's report, or null
|
|
1841
|
+
* needs { kind, text } while it waits on the member (answered on the
|
|
1842
|
+
* desk), or null
|
|
1843
|
+
*
|
|
1844
|
+
* Nothing is read while `id` is empty. Looks every 2.5 s while the run is
|
|
1845
|
+
* going; a run that has ended is read once.
|
|
1846
|
+
*/
|
|
1847
|
+
export function useRun(id) {
|
|
1848
|
+
const enabled = Boolean(id);
|
|
1849
|
+
const watched = useWatched((signal) => readRun(id, { signal }), JSON.stringify(["run", id ?? null]), {
|
|
1850
|
+
enabled,
|
|
1851
|
+
next: (data) => (data?.run?.state === "running" ? RUN_FOLLOW_MS : 0),
|
|
1852
|
+
});
|
|
1853
|
+
const { data, ...rest } = watched;
|
|
1854
|
+
return { run: data?.run ?? null, steps: listOf(data?.steps), text: data?.text ?? null, needs: data?.needs ?? null, ...rest };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/** A request's needs as a needs-you item's kind: the yes (an approval is
|
|
1858
|
+
* one), a question, a spend (a limit), a connect; else as it is. */
|
|
1859
|
+
const NEEDS_KIND = { approval: "yes", yes: "yes", question: "question", limit: "spend", connect: "connect" };
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* What waits on this member in this app, as a needs-you item, or null.
|
|
1863
|
+
* Pure, so the tests hold it to each case: only the member's own
|
|
1864
|
+
* requests (`mine`), with a need that blocks (a `note` never does). A
|
|
1865
|
+
* claw whose requests carry no `needs` still says `needs_you`: the item
|
|
1866
|
+
* is then a question, in the request's own line.
|
|
1867
|
+
*
|
|
1868
|
+
* An item also says how the app can settle it (0.8.1, D18/D19): `via` is
|
|
1869
|
+
* "answer" (a question: answer it in place with its `options` and, when
|
|
1870
|
+
* `free`, the member's own words), "press" (the one ask's yes: the
|
|
1871
|
+
* `approval` block's press frame) or null (on the desk: connect, spend,
|
|
1872
|
+
* a tool approval, several questions at once). `needsAt` is the version
|
|
1873
|
+
* of the need an answer names.
|
|
1874
|
+
*/
|
|
1875
|
+
export function needsYouItem(request) {
|
|
1876
|
+
if (!request || typeof request !== "object" || !request.mine || ended(request)) return null;
|
|
1877
|
+
const needs = request.needs && typeof request.needs === "object" ? request.needs : null;
|
|
1878
|
+
if (needs && needs.kind === "note") return null;
|
|
1879
|
+
if (!needs && request.status !== "needs_you") return null;
|
|
1880
|
+
const line = typeof request.line === "string" ? request.line.replace(/^Needs you\s*·\s*/, "") : "";
|
|
1881
|
+
return {
|
|
1882
|
+
id: request.id,
|
|
1883
|
+
request,
|
|
1884
|
+
kind: needs ? (NEEDS_KIND[needs.kind] ?? needs.kind ?? "question") : "question",
|
|
1885
|
+
text: (needs?.text || line || request.title || "").trim(),
|
|
1886
|
+
title: request.title ?? "",
|
|
1887
|
+
at: request.updated_at ?? request.created_at ?? null,
|
|
1888
|
+
via: needs?.in_app === "answer" || needs?.in_app === "press" ? needs.in_app : null,
|
|
1889
|
+
options: needs?.in_app === "answer" && Array.isArray(needs.options) ? needs.options : null,
|
|
1890
|
+
free: needs?.free !== false,
|
|
1891
|
+
needsAt: typeof needs?.at === "string" ? needs.at : null,
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/**
|
|
1896
|
+
* What waits on this member here, for the `needs-you` block (GET
|
|
1897
|
+
* _claw/requests, the member's own only):
|
|
1898
|
+
*
|
|
1899
|
+
* const { items, working, open, takeBack, stop } = useNeedsYou();
|
|
1900
|
+
*
|
|
1901
|
+
* items [{ id, request, kind: question | yes | spend | connect | …,
|
|
1902
|
+
* text, title, at, via: "answer" | "press" | null, options,
|
|
1903
|
+
* free, needsAt }]: the agent's questions and yeses, newest
|
|
1904
|
+
* first. A question with via "answer" is answered in place
|
|
1905
|
+
* (answer(item, …)); a yes with via "press" is the `approval`
|
|
1906
|
+
* block's press frame; anything else, open(item) takes to
|
|
1907
|
+
* the desk
|
|
1908
|
+
* working the member's own requests the agent is working on now
|
|
1909
|
+
* open(item) the request's conversation on the desk
|
|
1910
|
+
* answer(item, { optionId, text }) answer a question in place (0.8.1):
|
|
1911
|
+
* → { ok } | { ok: false, error, code }; the page's
|
|
1912
|
+
* other readers look again at once
|
|
1913
|
+
* takeBack(item) stop the ask that waits (the frozen "Ready to send")
|
|
1914
|
+
* stop(request) stop a working one ("Stop keeps everything written
|
|
1915
|
+
* so far")
|
|
1916
|
+
* loading, error, code, unavailable, reload
|
|
1917
|
+
*
|
|
1918
|
+
* Looks every `every` ms (8 s) while the page is shown, and at once after
|
|
1919
|
+
* an ask from this page.
|
|
1920
|
+
*/
|
|
1921
|
+
export function useNeedsYou({ every = 8000 } = {}) {
|
|
1922
|
+
const often = Math.max(Number(every) || 8000, 1000);
|
|
1923
|
+
const watched = useWatched((signal) => readRequests({ signal }), "needs-you", { next: () => often, listen: onAsked });
|
|
1924
|
+
const { data, reload } = watched;
|
|
1925
|
+
const list = listOf(data?.requests);
|
|
1926
|
+
const items = useMemo(
|
|
1927
|
+
() =>
|
|
1928
|
+
list
|
|
1929
|
+
.map(needsYouItem)
|
|
1930
|
+
.filter(Boolean)
|
|
1931
|
+
.sort((a, b) => String(b.at ?? "").localeCompare(String(a.at ?? ""))),
|
|
1932
|
+
[list],
|
|
1933
|
+
);
|
|
1934
|
+
const working = useMemo(() => list.filter((q) => q?.mine && LIVE.has(q.status) && q.status !== "needs_you" && !needsYouItem(q)), [list]);
|
|
1935
|
+
useEffect(() => {
|
|
1936
|
+
if (data?.desk_origin) publishDesk(data.desk_origin);
|
|
1937
|
+
}, [data]);
|
|
1938
|
+
|
|
1939
|
+
const open = useCallback((item) => openRequest(item?.request ?? item), []);
|
|
1940
|
+
const stopOne = useCallback(
|
|
1941
|
+
async (request) => {
|
|
1942
|
+
const id = request?.id;
|
|
1943
|
+
if (!id) return { ok: false, error: "nothing to stop" };
|
|
1944
|
+
const r = await stopRequest(id);
|
|
1945
|
+
reload();
|
|
1946
|
+
return r;
|
|
1947
|
+
},
|
|
1948
|
+
[reload],
|
|
1949
|
+
);
|
|
1950
|
+
const takeBack = useCallback((item) => stopOne(item?.request ?? item), [stopOne]);
|
|
1951
|
+
const answerOne = useCallback(
|
|
1952
|
+
async (item, { optionId, text } = {}) => {
|
|
1953
|
+
const id = item?.request?.id ?? item?.id;
|
|
1954
|
+
if (!id) return { ok: false, error: "nothing to answer" };
|
|
1955
|
+
const r = await sendAnswer(id, { optionId, text, at: item?.needsAt ?? undefined });
|
|
1956
|
+
if (r.ok) announceAsk();
|
|
1957
|
+
reload();
|
|
1958
|
+
return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
|
|
1959
|
+
},
|
|
1960
|
+
[reload],
|
|
1961
|
+
);
|
|
1962
|
+
|
|
1963
|
+
return {
|
|
1964
|
+
items,
|
|
1965
|
+
working,
|
|
1966
|
+
loading: watched.loading,
|
|
1967
|
+
error: watched.error,
|
|
1968
|
+
code: watched.code,
|
|
1969
|
+
unavailable: watched.unavailable,
|
|
1970
|
+
reload,
|
|
1971
|
+
open,
|
|
1972
|
+
answer: answerOne,
|
|
1973
|
+
takeBack,
|
|
1974
|
+
stop: stopOne,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/** How long the press frame has to say it is ready before the card
|
|
1979
|
+
* falls back to the desk's (a frame that cannot load says nothing). */
|
|
1980
|
+
const FRAME_READY_MS = 7000;
|
|
1981
|
+
/** The frame's height, as it asks for it: kept between these. */
|
|
1982
|
+
const FRAME_MIN = 96;
|
|
1983
|
+
const FRAME_MAX = 520;
|
|
1984
|
+
/** What a card shows for a request whose yes is no longer there. */
|
|
1985
|
+
const APPROVAL_GONE = new Set(["nothing_to_approve", "unknown_request", "not_one_yes", "not_your_request"]);
|
|
1986
|
+
const NO_PRESS = { for: null, state: null, height: null, always: false, ready: false };
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* The yes, given in the app (0.8.1, D18): the facts of one request's yes
|
|
1990
|
+
* and the Makersclaw-held press frame to embed in the app's own card, for
|
|
1991
|
+
* the `approval` block.
|
|
1992
|
+
*
|
|
1993
|
+
* const approval = useApproval(item.id); // item from useNeedsYou
|
|
1994
|
+
* <Approval approval={approval} title="Send 3 follow-ups"> …the draft… </Approval>
|
|
1995
|
+
*
|
|
1996
|
+
* The app draws the card in its theme; the press is a page of
|
|
1997
|
+
* Makersclaw's in another origin, which shows the SERVER's words (the
|
|
1998
|
+
* agent's question, who, from which account, the first message, the rule
|
|
1999
|
+
* "Always allow this kind" would save) and only counts a press a person
|
|
2000
|
+
* made on a card they can see. App code cannot press it, and this hook
|
|
2001
|
+
* only listens to it.
|
|
2002
|
+
*
|
|
2003
|
+
* approval { request_id, question, label, covers, always, cap_credits,
|
|
2004
|
+
* at, frame: { src, origin }, expires_at } or null
|
|
2005
|
+
* state idle (no request) · loading · ready (the frame is up) ·
|
|
2006
|
+
* approved · declined · stale (the question moved on; looks
|
|
2007
|
+
* again by itself) · gone (nothing waits any more: answered
|
|
2008
|
+
* elsewhere) · signin (the frame needs the member signed in
|
|
2009
|
+
* on the desk) · blocked (the frame did not load in time) ·
|
|
2010
|
+
* unavailable (a claw from before this door) · error
|
|
2011
|
+
* always true once "Always allow this kind" was saved with the yes
|
|
2012
|
+
* fallback true when the frame cannot be used (signin, blocked,
|
|
2013
|
+
* unavailable, error): draw WaitingYes and open the desk
|
|
2014
|
+
* frame { src, ref, title } to spread on the <iframe>
|
|
2015
|
+
* height the px the frame asked for, kept between 96 and 520
|
|
2016
|
+
* loading, error, code, unavailable, reload
|
|
2017
|
+
*
|
|
2018
|
+
* Nothing here approves. A request with no id asks nothing.
|
|
2019
|
+
*/
|
|
2020
|
+
export function useApproval(requestId, { fallbackMs = FRAME_READY_MS } = {}) {
|
|
2021
|
+
const id = requestId ? String(requestId) : null;
|
|
2022
|
+
const watched = useWatched((signal) => readApproval(id, { signal }), JSON.stringify(["approval", id]), {
|
|
2023
|
+
enabled: Boolean(id),
|
|
2024
|
+
next: () => 0,
|
|
2025
|
+
});
|
|
2026
|
+
const { data, reload } = watched;
|
|
2027
|
+
const approval = data?.approval ?? null;
|
|
2028
|
+
const src = approval?.frame?.src ?? null;
|
|
2029
|
+
const origin = approval?.frame?.origin ?? null;
|
|
2030
|
+
const frameRef = useRef(null);
|
|
2031
|
+
const [heard, setHeard] = useState(NO_PRESS);
|
|
2032
|
+
const [timedOut, setTimedOut] = useState(null);
|
|
2033
|
+
const mine = heard.for === src ? heard : NO_PRESS;
|
|
2034
|
+
|
|
2035
|
+
useEffect(() => {
|
|
2036
|
+
if (!src || !origin || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
|
|
2037
|
+
const onMessage = (event) => {
|
|
2038
|
+
const m = pressMessage(event, { origin, source: frameRef.current?.contentWindow, request: id });
|
|
2039
|
+
if (!m) return;
|
|
2040
|
+
if (m.state === "ready") setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, ready: true }));
|
|
2041
|
+
else if (m.state === "size") {
|
|
2042
|
+
const px = Math.round(Number(m.height));
|
|
2043
|
+
if (Number.isFinite(px) && px > 0) setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, height: Math.min(Math.max(px, FRAME_MIN), FRAME_MAX) }));
|
|
2044
|
+
} else if (m.state === "approved" || m.state === "declined") {
|
|
2045
|
+
setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: m.state, always: m.state === "approved" && m.always === true }));
|
|
2046
|
+
announceAsk();
|
|
2047
|
+
} else if (m.state === "stale") {
|
|
2048
|
+
// The question moved on since this card was drawn: a new ticket, or the news that nothing waits.
|
|
2049
|
+
setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: "stale" }));
|
|
2050
|
+
reload();
|
|
2051
|
+
} else if (m.state === "signin") setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: "signin" }));
|
|
2052
|
+
};
|
|
2053
|
+
window.addEventListener("message", onMessage);
|
|
2054
|
+
const timer = setTimeout(() => setTimedOut(src), Math.max(Number(fallbackMs) || FRAME_READY_MS, 1000));
|
|
2055
|
+
return () => {
|
|
2056
|
+
window.removeEventListener?.("message", onMessage);
|
|
2057
|
+
clearTimeout(timer);
|
|
2058
|
+
};
|
|
2059
|
+
}, [src, origin, id, fallbackMs, reload]);
|
|
2060
|
+
|
|
2061
|
+
let state;
|
|
2062
|
+
if (!id) state = "idle";
|
|
2063
|
+
else if (watched.unavailable) state = "unavailable";
|
|
2064
|
+
else if (watched.error) state = APPROVAL_GONE.has(watched.code) ? "gone" : "error";
|
|
2065
|
+
else if (!approval) state = "loading";
|
|
2066
|
+
else if (mine.state) state = mine.state;
|
|
2067
|
+
else if (mine.ready) state = "ready";
|
|
2068
|
+
else state = timedOut === src ? "blocked" : "ready";
|
|
2069
|
+
const fallback = state === "signin" || state === "blocked" || state === "unavailable" || state === "error";
|
|
2070
|
+
|
|
2071
|
+
return {
|
|
2072
|
+
approval,
|
|
2073
|
+
state,
|
|
2074
|
+
always: mine.always,
|
|
2075
|
+
fallback,
|
|
2076
|
+
frame: approval && src ? { src, ref: frameRef, title: "Approve" } : null,
|
|
2077
|
+
height: mine.height,
|
|
2078
|
+
loading: watched.loading,
|
|
2079
|
+
error: watched.error,
|
|
2080
|
+
code: watched.code,
|
|
2081
|
+
unavailable: watched.unavailable,
|
|
2082
|
+
reload,
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/** A time as the key of a read: to the minute, so a `since` made fresh
|
|
2087
|
+
* each render (new Date(Date.now() - …)) does not read again each
|
|
2088
|
+
* render. */
|
|
2089
|
+
function minuteOf(time) {
|
|
2090
|
+
if (time instanceof Date) return Number.isNaN(time.getTime()) ? null : time.toISOString().slice(0, 16);
|
|
2091
|
+
return typeof time === "string" && time ? time.slice(0, 16) : null;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* Who did what in this app, newest first, for the `activity-log` and
|
|
2096
|
+
* `agent-changes` blocks (GET _claw/data/activity):
|
|
2097
|
+
*
|
|
2098
|
+
* const { rows } = useActivity({ row: deal.id }); // one record's timeline
|
|
2099
|
+
* const { rows } = useActivity({ agent: true, since: askedAt }); // what the agent did
|
|
2100
|
+
*
|
|
2101
|
+
* rows [{ id, at, action, surface, row_ref, detail, data, by }]; `by`
|
|
2102
|
+
* is user:<email>, agent:<email> (on a member's ask) or agent:job
|
|
2103
|
+
* loading, error, code, unavailable, reload
|
|
2104
|
+
*
|
|
2105
|
+
* `row` narrows to one record's lines, `surface` to one surface, `since`
|
|
2106
|
+
* (ISO or a Date, compared to the minute) to lines after it, `limit` (50);
|
|
2107
|
+
* `agent: true` keeps the agent's lines only. Looks every `every` ms (20
|
|
2108
|
+
* s) while the page is shown. lib/door.js's useActivity is the same lines
|
|
2109
|
+
* read once, with no `row` and no polling; this one is the kit's.
|
|
2110
|
+
*/
|
|
2111
|
+
export function useActivity({ row, surface, since, limit = 50, agent = false, every = 20000 } = {}) {
|
|
2112
|
+
const sinceRef = useRef(since);
|
|
2113
|
+
sinceRef.current = since;
|
|
2114
|
+
const key = JSON.stringify(["activity", row ?? null, surface ?? null, minuteOf(since), limit ?? null]);
|
|
2115
|
+
const watched = useWatched((signal) => readActivity({ row, surface, since: sinceRef.current, limit, signal }), key, {
|
|
2116
|
+
next: () => every,
|
|
2117
|
+
});
|
|
2118
|
+
const { data, ...rest } = watched;
|
|
2119
|
+
const all = listOf(data?.rows);
|
|
2120
|
+
const rows = useMemo(() => (agent ? all.filter((r) => typeof r?.by === "string" && r.by.startsWith("agent:")) : all), [all, agent]);
|
|
2121
|
+
return { rows, ...rest };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
/**
|
|
2125
|
+
* Connect one of the app's declared accounts, the way the member's press
|
|
2126
|
+
* should go: inside the desk, ask it to open its connections panel for
|
|
2127
|
+
* that need (every provider of the kind shows; no agent turn); on the
|
|
2128
|
+
* app's own host, fetch the link (POST _claw/connect) and open it in a new
|
|
2129
|
+
* tab. Signing in stays the member's, at the provider.
|
|
2130
|
+
*
|
|
2131
|
+
* The tab is opened before the link is fetched, on the press itself, and
|
|
2132
|
+
* pointed at the link once it comes: a tab opened after an await is a
|
|
2133
|
+
* popup the browser blocks. → { opened: "desk" | "tab" | false,
|
|
2134
|
+
* connected?, error?, code? }. Never throws. useConnections calls it.
|
|
2135
|
+
*/
|
|
2136
|
+
export async function connectAccount(key, win = typeof window === "undefined" ? undefined : window) {
|
|
2137
|
+
if (openConnections(key, win)) return { opened: "desk" };
|
|
2138
|
+
let tab = null;
|
|
2139
|
+
try {
|
|
2140
|
+
tab = win?.open?.("about:blank", "_blank") ?? null;
|
|
2141
|
+
if (tab) tab.opener = null;
|
|
2142
|
+
} catch {
|
|
2143
|
+
tab = null;
|
|
2144
|
+
}
|
|
2145
|
+
const r = await connectLink(key);
|
|
2146
|
+
const url = r.ok && typeof r.data?.url === "string" && /^https?:\/\//i.test(r.data.url) ? r.data.url : null;
|
|
2147
|
+
if (!url) {
|
|
2148
|
+
try {
|
|
2149
|
+
tab?.close?.();
|
|
2150
|
+
} catch {
|
|
2151
|
+
/* a tab the browser already took back */
|
|
2152
|
+
}
|
|
2153
|
+
if (r.ok && r.data?.connected) return { opened: false, connected: true };
|
|
2154
|
+
if (r.ok) return { opened: false, error: "no link came back" };
|
|
2155
|
+
return { opened: false, error: r.unavailable ? null : r.error, code: r.code ?? null, unavailable: Boolean(r.unavailable) };
|
|
2156
|
+
}
|
|
2157
|
+
try {
|
|
2158
|
+
if (tab) {
|
|
2159
|
+
tab.location.href = url;
|
|
2160
|
+
return { opened: "tab" };
|
|
2161
|
+
}
|
|
2162
|
+
const opened = win?.open?.(url, "_blank", "noopener");
|
|
2163
|
+
return { opened: opened === undefined ? false : "tab" };
|
|
2164
|
+
} catch {
|
|
2165
|
+
return { opened: false, error: "the browser would not open the link" };
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/** Is a real account behind this connection? `connected` is the
|
|
2170
|
+
* workspace listing the toolkit, which can be true while the list of
|
|
2171
|
+
* accounts that meet it is empty: a toolkit that needs no sign-in, or a
|
|
2172
|
+
* leftover (an account removed at the provider). A screen that needs an
|
|
2173
|
+
* account (a mailbox to read, a calendar to sync) asks this, not
|
|
2174
|
+
* `connected`, before it says "Connected" or offers the read: the CRM's
|
|
2175
|
+
* "Connect your mailbox" card said "Connected" over nothing (2026-10-11).
|
|
2176
|
+
* False without an `accounts` list (a claw from before it) only when
|
|
2177
|
+
* `connected` is false too. */
|
|
2178
|
+
export function hasAccount(c) {
|
|
2179
|
+
if (!c || !c.connected) return false;
|
|
2180
|
+
return Array.isArray(c.accounts) ? c.accounts.filter(Boolean).length > 0 : true;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* The app's declared accounts, for the `connect-card` block (GET
|
|
2185
|
+
* _claw/connect):
|
|
2186
|
+
*
|
|
2187
|
+
* const { connections, connect } = useConnections();
|
|
2188
|
+
* <ConnectCard connections={connections} onConnect={connect} />
|
|
2189
|
+
*
|
|
2190
|
+
* connections [{ key, label, toolkits, required, connected, accounts,
|
|
2191
|
+
* category, category_label }]
|
|
2192
|
+
* connecting the key being connected while it is watched, or null
|
|
2193
|
+
* connect(key) → { opened: "desk" | "tab" | false, error? }
|
|
2194
|
+
* (connectAccount), then looks every 3 s for two minutes
|
|
2195
|
+
* until that account is connected
|
|
2196
|
+
* loading, error, code, unavailable, reload
|
|
2197
|
+
*
|
|
2198
|
+
* Read once, then only while an account is being connected (or every
|
|
2199
|
+
* `every` ms when given), and at once when the page comes back.
|
|
2200
|
+
*/
|
|
2201
|
+
export function useConnections({ every } = {}) {
|
|
2202
|
+
const watchRef = useRef(null); // { key, until }
|
|
2203
|
+
const [connecting, setConnecting] = useState(null);
|
|
2204
|
+
const watched = useWatched((signal) => readConnections({ signal }), "connections", {
|
|
2205
|
+
next: (data) => {
|
|
2206
|
+
const watch = watchRef.current;
|
|
2207
|
+
if (watch) {
|
|
2208
|
+
const met = listOf(data?.connections).some((c) => c?.key === watch.key && c.connected);
|
|
2209
|
+
if (met || Date.now() > watch.until) {
|
|
2210
|
+
watchRef.current = null;
|
|
2211
|
+
setConnecting(null);
|
|
2212
|
+
} else return CONNECTING_MS;
|
|
2213
|
+
}
|
|
2214
|
+
return every ?? 0;
|
|
2215
|
+
},
|
|
2216
|
+
});
|
|
2217
|
+
const { data, reload } = watched;
|
|
2218
|
+
|
|
2219
|
+
const connect = useCallback(
|
|
2220
|
+
async (key) => {
|
|
2221
|
+
const r = await connectAccount(key);
|
|
2222
|
+
if (r.opened) {
|
|
2223
|
+
watchRef.current = { key, until: Date.now() + CONNECTING_FOR_MS };
|
|
2224
|
+
setConnecting(key);
|
|
2225
|
+
}
|
|
2226
|
+
reload();
|
|
2227
|
+
return r;
|
|
2228
|
+
},
|
|
2229
|
+
[reload],
|
|
2230
|
+
);
|
|
2231
|
+
|
|
2232
|
+
return {
|
|
2233
|
+
connections: listOf(data?.connections),
|
|
2234
|
+
connecting,
|
|
2235
|
+
loading: watched.loading,
|
|
2236
|
+
error: watched.error,
|
|
2237
|
+
code: watched.code,
|
|
2238
|
+
unavailable: watched.unavailable,
|
|
2239
|
+
reload,
|
|
2240
|
+
connect,
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/** What the agent knows and can do, from GET _claw/agent's answer. Pure:
|
|
2245
|
+
* the app's own skills first (from: "app"), then those added on top
|
|
2246
|
+
* (from: "added", installed or not), then its tools — the declared
|
|
2247
|
+
* actions the agent may run. */
|
|
2248
|
+
export function agentKitOf(data) {
|
|
2249
|
+
const own = listOf(data?.own_skills)
|
|
2250
|
+
.filter((s) => s && typeof s.name === "string")
|
|
2251
|
+
.map((s) => ({ name: s.name, title: s.title || s.name, when: s.when ?? null, from: "app", installed: true }));
|
|
2252
|
+
const ownNames = new Set(own.map((s) => s.name));
|
|
2253
|
+
const added = listOf(data?.skills)
|
|
2254
|
+
.filter((s) => s && typeof s.name === "string" && !ownNames.has(s.name))
|
|
2255
|
+
.map((s) => ({ name: s.name, title: s.title || s.name, when: s.when ?? null, from: "added", installed: Boolean(s.installed) }));
|
|
2256
|
+
const tools = listOf(data?.actions)
|
|
2257
|
+
.filter((a) => a && typeof a.name === "string" && a.agent !== false)
|
|
2258
|
+
.map((a) => ({ name: a.name, description: a.description ?? "" }));
|
|
2259
|
+
return { skills: [...own, ...added], tools };
|
|
743
2260
|
}
|
|
2261
|
+
|
|
2262
|
+
/**
|
|
2263
|
+
* What the app's agent knows and can do, for the `agent-kit` block (GET
|
|
2264
|
+
* _claw/agent): `skills` [{ name, title, when, from: "app" | "added",
|
|
2265
|
+
* installed }] and `tools` [{ name, description }] (agentKitOf), with
|
|
2266
|
+
* loading, error, code, unavailable, reload. Read once.
|
|
2267
|
+
*/
|
|
2268
|
+
export function useAgentKit() {
|
|
2269
|
+
const watched = useWatched((signal) => readAgentKit({ signal }), "agent-kit");
|
|
2270
|
+
const { data, ...rest } = watched;
|
|
2271
|
+
const kit = useMemo(() => agentKitOf(data), [data]);
|
|
2272
|
+
return { ...kit, ...rest };
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/** How long useLlmAction waits for the input to settle before it asks
|
|
2276
|
+
* for a price. */
|
|
2277
|
+
const ESTIMATE_AFTER_MS = 400;
|
|
2278
|
+
|
|
2279
|
+
/**
|
|
2280
|
+
* One model call on what the screen holds, with its price before the
|
|
2281
|
+
* press, for the `ai-action` and `cost` blocks:
|
|
2282
|
+
*
|
|
2283
|
+
* const shorter = useLlmAction({ input: text, system: "Make it shorter.", task: "shorten" });
|
|
2284
|
+
* <AiAction label="Shorter" action={shorter} onUse={(r) => setText(r.text)} />
|
|
2285
|
+
*
|
|
2286
|
+
* estimate { credits, credits_max } or null: the price, asked of the
|
|
2287
|
+
* platform (llmEstimate: priced, never sent to a model,
|
|
2288
|
+
* never charged) once per distinct input, 400 ms after it
|
|
2289
|
+
* settles. Null for an empty input, and on a claw with no
|
|
2290
|
+
* estimate yet. While a new input is priced the last price
|
|
2291
|
+
* stays, with `estimating` true
|
|
2292
|
+
* estimating a price is being asked for
|
|
2293
|
+
* run() the call, with the same arguments → llm()'s answer
|
|
2294
|
+
* result, loading, error, unavailable as useLlm
|
|
2295
|
+
* reset() back to nothing shown
|
|
2296
|
+
*
|
|
2297
|
+
* Options: llm()'s — input, system, schema, tier, task, maxTokens — and
|
|
2298
|
+
* `estimate` (default true; false asks no price).
|
|
2299
|
+
*/
|
|
2300
|
+
export function useLlmAction({ input, system, schema, tier, task, maxTokens, estimate = true } = {}) {
|
|
2301
|
+
const call = useLlm();
|
|
2302
|
+
const argsRef = useRef(null);
|
|
2303
|
+
argsRef.current = { input, opts: { system, schema, tier, task, maxTokens } };
|
|
2304
|
+
const priced = Boolean(estimate) && saidSomething(input);
|
|
2305
|
+
let key = null;
|
|
2306
|
+
if (priced) {
|
|
2307
|
+
try {
|
|
2308
|
+
key = JSON.stringify([input, system ?? null, schema ?? null, tier ?? null, task ?? null, maxTokens ?? null]);
|
|
2309
|
+
} catch {
|
|
2310
|
+
key = null; // an input that is not JSON: llm() refuses it too
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
const cacheRef = useRef(new Map());
|
|
2314
|
+
const [price, setPrice] = useState({ key: null, value: null, estimating: false });
|
|
2315
|
+
|
|
2316
|
+
useEffect(() => {
|
|
2317
|
+
if (!key) return undefined;
|
|
2318
|
+
const known = cacheRef.current.get(key);
|
|
2319
|
+
if (known !== undefined) {
|
|
2320
|
+
setPrice({ key, value: known, estimating: false });
|
|
2321
|
+
return undefined;
|
|
2322
|
+
}
|
|
2323
|
+
const controller = new AbortController();
|
|
2324
|
+
const timer = setTimeout(async () => {
|
|
2325
|
+
setPrice((p) => ({ ...p, estimating: true }));
|
|
2326
|
+
const { input: now, opts } = argsRef.current;
|
|
2327
|
+
const r = await llmEstimate(now, { ...opts, signal: controller.signal });
|
|
2328
|
+
if (controller.signal.aborted || r.aborted) return;
|
|
2329
|
+
const value =
|
|
2330
|
+
r.ok && r.estimate && typeof r.estimate === "object" && typeof r.estimate.credits === "number"
|
|
2331
|
+
? { credits: r.estimate.credits, credits_max: typeof r.estimate.credits_max === "number" ? r.estimate.credits_max : r.estimate.credits }
|
|
2332
|
+
: null;
|
|
2333
|
+
if (r.ok) cacheRef.current.set(key, value);
|
|
2334
|
+
setPrice({ key, value, estimating: false });
|
|
2335
|
+
}, ESTIMATE_AFTER_MS);
|
|
2336
|
+
return () => {
|
|
2337
|
+
clearTimeout(timer);
|
|
2338
|
+
controller.abort();
|
|
2339
|
+
};
|
|
2340
|
+
}, [key]);
|
|
2341
|
+
|
|
2342
|
+
const run = useCallback(() => {
|
|
2343
|
+
const { input: now, opts } = argsRef.current;
|
|
2344
|
+
return call.run(now, opts);
|
|
2345
|
+
}, [call.run]);
|
|
2346
|
+
|
|
2347
|
+
return {
|
|
2348
|
+
estimate: key ? price.value : null,
|
|
2349
|
+
estimating: Boolean(key) && (price.estimating || (price.key !== key && !cacheRef.current.has(key))),
|
|
2350
|
+
run,
|
|
2351
|
+
result: call.result,
|
|
2352
|
+
loading: call.loading,
|
|
2353
|
+
error: call.error,
|
|
2354
|
+
unavailable: call.unavailable,
|
|
2355
|
+
reset: call.reset,
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// The automations engine's hooks (useAutomations, useAutomation,
|
|
2360
|
+
// useAutomationRun) live in lib/automations.js, with their doors; a surface
|
|
2361
|
+
// imports them from here with the others.
|
|
2362
|
+
export * from "./automations";
|