makersclaw 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +234 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +246 -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 +1677 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "automation-model",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "AutomationModel",
|
|
6
|
+
"description": "The pure part of the automation blocks, with no screen of its own: the step catalog (each kind's name, face, group and price flag, a fresh step, a step's title and quiet line), the canvas layout (an automation turned into boxes, curved connectors with a slot for every +, labelled branches that rejoin, a for-each box), the edits (insert, remove, update, nudge, duplicate, what a step may read), what a run did (each node's state, the path taken), the words (a schedule in cron and in speech, a time in the automation's zone, a cost, a health, a run's outcome), a quick check of a definition, and the starting points. Glyph draws a kind's face from the kit's icons.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/automation-model/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/automation-model/index.js",
|
|
16
|
+
"content": "// The AutomationModel block — `makersclaw add automation-model`. The pure\n// part of the automation blocks: the step catalog, the layout, the edits,\n// what a run did, the words. Fixed: the other automation blocks require it.\nexport { GLYPH_ICON, GROUP_LABELS, GROUP_ORDER, KINDS, RUNNABLE, TRIGGER_TITLES, humanize, isRunnable, newStep, paletteGroups, stepSub, stepTitle, triggerGlyph, triggerTitle, uniqueStepId } from \"./catalog\";\nexport { Glyph } from \"./glyph\";\nexport { Chip, Dot, PanelSection, RunPicker, SidePanel } from \"./parts\";\nexport { LABEL_W, MARGIN, T, curveBetween, layoutAutomation, nodeIndex, readingOrder, sameSlot, slotWords } from \"./graph\";\nexport {\n addStep,\n allStepIds,\n availableBefore,\n blankAutomation,\n canonicalJson,\n duplicateStep,\n findStep,\n innerCount,\n insertStep,\n keyFromName,\n locate,\n nudgeStep,\n removeStep,\n slotAfter,\n slotAtEnd,\n stepIdForPath,\n updateStep,\n} from \"./edit\";\nexport { latestRows, overlayOf, summarizeSteps } from \"./overlay\";\nexport { ACTIVE, agoWords, clockWords, creditsWords, cronWords, deleteWords, durationWords, formatCreditAmount, healthOf, nodeStateWords, originWords, overviewOf, priceMap, reportLines, runLine, runStatusWords, summaryOf, testWords, triggerTypeWords, triggerWords, wouldWords } from \"./words\";\nexport { WEEKDAYS, cronFromParts, parseTime, partsFromCron, timeText } from \"./schedule\";\nexport { textToValue, valueToText } from \"./values\";\nexport { checkAutomation, flaggedSteps, issuesFor, issuesFromProblems, plainWords } from \"./check\";\nexport { STARTING_POINTS, startsFromPlatform } from \"./templates\";\nexport { CONTROL_DONE, CONTROL_REFUSED, RETRY_FROM, agentDraftOf, changeLines, changeLinesBetween, controlsOf, draftOfStep, fixTag, latestVersionOf, newerVersionWords, retryFromOf } from \"./fixes\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/automation-model/catalog.js",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/automation-model/catalog.js",
|
|
22
|
+
"content": "// The step catalog: what each kind of step is called, which icon wears it,\n// where the picker files it, and what a fresh one looks like.\n//\n// Pure data and small functions; no React. The canvas, the picker, the\n// outline and the settings panel all read this one table, so a kind has\n// one name and one face everywhere. It mirrors the platform's automation\n// format (format 1: a trigger and an ordered list of steps; a condition\n// has `then` and `else`, a loop has `steps`); the platform validates a\n// definition when it is saved, this file only dresses it.\n\n/** A kind's face: a kit Icon name per glyph (glyph.jsx draws it). */\nexport const GLYPH_ICON = {\n \"trigger-schedule\": \"calendar-clock\",\n \"trigger-manual\": \"hand\",\n \"trigger-event\": \"send\",\n \"trigger-action\": \"zap\",\n \"trigger-record\": \"database\",\n \"trigger-webhook\": \"globe\",\n \"trigger-automation\": \"workflow\",\n \"trigger-connection\": \"plug\",\n action: \"zap\",\n brain: \"book-open\",\n condition: \"split\",\n loop: \"repeat\",\n set: \"braces\",\n wait: \"timer\",\n stop: \"square\",\n notify: \"bell\",\n tool: \"plug\",\n search: \"search\",\n llm: \"sparkles\",\n agent: \"bot\",\n zero: \"dollar\",\n automation: \"workflow\",\n join: \"circle\",\n plus: \"plus\",\n};\n\n/** How each way of starting is named on the canvas, in the outline and at\n * the top of its panel: one short title per trigger type. */\nexport const TRIGGER_TITLES = {\n manual: \"By hand\",\n schedule: \"Schedule\",\n event: \"Event\",\n action: \"App action\",\n record: \"Record\",\n webhook: \"Web address\",\n automation: \"After another\",\n};\n\n/** The glyph of a trigger type (an unknown type wears the hand's). */\nexport function triggerGlyph(type) {\n return Object.prototype.hasOwnProperty.call(TRIGGER_TITLES, type) ? `trigger-${type}` : \"trigger-manual\";\n}\n\n/** The title of a trigger type. */\nexport function triggerTitle(type) {\n return TRIGGER_TITLES[type] ?? \"By hand\";\n}\n\n/** The picker's groups, in the order it shows them. A step's tone (plain,\n * model, agent) says how it is dressed: an agent step is solid in the AI\n * hue, a model call carries a \"model\" mark in it, the rest are plain. */\nexport const GROUP_LABELS = {\n app: \"Your apps\",\n think: \"Think\",\n flow: \"Flow\",\n data: \"Data\",\n tell: \"Tell\",\n more: \"More\",\n};\n\nexport const GROUP_ORDER = [\"app\", \"think\", \"flow\", \"data\", \"tell\", \"more\"];\n\n/** The kinds the platform knows, in format 1. */\nexport const KINDS = {\n action: { kind: \"action\", name: \"App action\", blurb: \"Do something in one of your apps: add people, save a draft, close a task.\", glyph: \"action\", tone: \"plain\", group: \"app\", paid: false },\n \"brain.read\": { kind: \"brain.read\", name: \"Read the company brain\", blurb: \"Look up notes and records you can read.\", glyph: \"brain\", tone: \"plain\", group: \"data\", paid: false },\n condition: { kind: \"condition\", name: \"Condition\", blurb: \"Go one way when something is true and another way when it is not.\", glyph: \"condition\", tone: \"plain\", group: \"flow\", paid: false },\n loop: { kind: \"loop\", name: \"For each\", blurb: \"Do the same steps for every item in a list.\", glyph: \"loop\", tone: \"plain\", group: \"flow\", paid: false },\n set: { kind: \"set\", name: \"Set values\", blurb: \"Build or reshape a value for the steps after it.\", glyph: \"set\", tone: \"plain\", group: \"data\", paid: false },\n wait: { kind: \"wait\", name: \"Wait\", blurb: \"Pause for a while, or until a time. Nothing runs while it waits.\", glyph: \"wait\", tone: \"plain\", group: \"flow\", paid: false },\n stop: { kind: \"stop\", name: \"Stop\", blurb: \"End the run here with a plain message.\", glyph: \"stop\", tone: \"plain\", group: \"flow\", paid: false },\n notify: { kind: \"notify\", name: \"Tell someone\", blurb: \"Leave a line for the owner: a note in their Needs you.\", glyph: \"notify\", tone: \"plain\", group: \"tell\", paid: false },\n tool: { kind: \"tool\", name: \"Connected tool\", blurb: \"Use one of your connected accounts: read mail, post a message, add a row.\", glyph: \"tool\", tone: \"plain\", group: \"app\", paid: false },\n search: { kind: \"search\", name: \"Search the web\", blurb: \"Look something up on the web.\", glyph: \"search\", tone: \"plain\", group: \"data\", paid: true },\n llm: { kind: \"llm\", name: \"Ask the model\", blurb: \"One model call: write a line, sort a list, pull facts out of text.\", glyph: \"llm\", tone: \"model\", group: \"think\", paid: true, mark: \"model\" },\n agent: { kind: \"agent\", name: \"Agent\", blurb: \"Hand a job that needs judgment to the agent, with a limit on what it may spend.\", glyph: \"agent\", tone: \"agent\", group: \"think\", paid: true, mark: \"agent\" },\n zero: { kind: \"zero\", name: \"Buy paid data\", blurb: \"Pay for one call to a paid data service, up to a price you set. Plain steps, and the same call is never paid for twice.\", glyph: \"zero\", tone: \"plain\", group: \"data\", paid: true },\n automation: { kind: \"automation\", name: \"Another automation\", blurb: \"Run another automation and use what it makes.\", glyph: \"automation\", tone: \"plain\", group: \"more\", paid: false },\n};\n\n/** The kinds the engine runs today. Anything else can be drawn and is\n * saved only once the engine knows it (the platform refuses it until\n * then). A platform that runs more says so in `runnable` (a prop). */\nexport const RUNNABLE = new Set([\"action\", \"brain.read\", \"condition\", \"loop\", \"set\", \"wait\", \"stop\", \"notify\", \"tool\", \"search\", \"llm\", \"agent\", \"zero\"]);\n\nexport const isRunnable = (kind, runnable = RUNNABLE) => runnable.has(kind);\n\nconst PALETTE_ORDER = [\"action\", \"tool\", \"llm\", \"agent\", \"condition\", \"loop\", \"wait\", \"stop\", \"set\", \"brain.read\", \"search\", \"zero\", \"notify\", \"automation\"];\n\n/** Palette order: the groups, and in each group the kinds in a fixed order. */\nexport function paletteGroups() {\n return GROUP_ORDER.map((group) => ({\n group,\n label: GROUP_LABELS[group],\n kinds: PALETTE_ORDER.map((k) => KINDS[k]).filter((k) => k.group === group),\n })).filter((g) => g.kinds.length > 0);\n}\n\n/* ---- a fresh step ------------------------------------------------------ */\n\n/** `brain.read` becomes `brain_read`: a step id's own alphabet. */\nconst idBase = (kind) => kind.replace(/[^a-z0-9]+/g, \"_\");\n\n/** A step id no other step uses: `wait`, `wait_2`, `wait_3` … */\nexport function uniqueStepId(kind, taken) {\n const base = idBase(kind);\n if (!taken.has(base)) return base;\n for (let n = 2; n < 1000; n += 1) {\n const id = `${base}_${n}`;\n if (!taken.has(id)) return id;\n }\n return `${base}_${taken.size + 1}`;\n}\n\n/** A new step of a kind, with whatever default makes it read as itself.\n * Some kinds cannot be valid until a person names something (an action,\n * a tool): the settings panel asks for it, and the save waits. */\nexport function newStep(kind, taken) {\n const id = uniqueStepId(kind, taken);\n switch (kind) {\n case \"action\":\n return { id, kind, action: \"\", input: {} };\n case \"brain.read\":\n return { id, kind, op: \"search\", input: { query: \"\" } };\n case \"condition\":\n return { id, kind, if: \"{{ true }}\", then: [], else: [] };\n case \"loop\":\n return { id, kind, over: \"{{ trigger.items }}\", max: 100, steps: [] };\n case \"set\":\n return { id, kind, values: { value: \"\" } };\n case \"wait\":\n return { id, kind, for: \"10m\" };\n case \"stop\":\n return { id, kind, message: \"Done\" };\n case \"notify\":\n return { id, kind, message: \"Something to look at\" };\n case \"tool\":\n return { id, kind, needs: { category: \"mail\" }, tool: \"\", input: {} };\n case \"search\":\n return { id, kind, query: \"\", limit: 5 };\n case \"llm\":\n return { id, kind, prompt: \"\", tier: \"fast\" };\n case \"agent\":\n return { id, kind, instructions: \"\", cap_credits: 50 };\n case \"zero\":\n return { id, kind, capability: \"\", url: \"https://\", max_pay_usd: 0.05 };\n case \"automation\":\n return { id, kind, key: \"\", input: {} };\n default:\n return { id, kind };\n }\n}\n\n/* ---- words for a step on a card ---------------------------------------- */\n\nconst strip = (text) => String(text ?? \"\").replace(/\\{\\{\\s*|\\s*\\}\\}/g, \"\").replace(/\\s+/g, \" \").trim();\nconst cut = (text, n) => (text.length > n ? `${text.slice(0, n - 1).trimEnd()}…` : text);\n\n/** `read_mail` becomes \"Read mail\". */\nexport function humanize(name) {\n const s = String(name ?? \"\").replace(/[._-]+/g, \" \").replace(/\\s+/g, \" \").trim();\n return s ? s[0].toUpperCase() + s.slice(1) : \"\";\n}\n\n/** The card's title: the label the person gave it, else the kind's name\n * with what it is about. Written in full; the card wraps it. */\nexport function stepTitle(step) {\n if (step.label?.trim()) return step.label.trim();\n switch (step.kind) {\n case \"action\":\n return step.action ? humanize(step.action) : \"Pick an action\";\n case \"tool\":\n return step.tool ? humanize(step.tool) : \"Pick a tool\";\n case \"condition\":\n return \"Check a condition\";\n case \"loop\":\n return \"For each item\";\n case \"brain.read\":\n return step.op === \"get\" ? \"Read one record\" : step.op === \"list\" ? \"List records\" : \"Search the company brain\";\n case \"set\":\n return \"Set values\";\n case \"wait\":\n return step.until ? \"Wait until a time\" : `Wait ${step.for ?? \"\"}`.trim();\n case \"stop\":\n return step.as === \"failed\" ? \"Stop as failed\" : \"Stop\";\n case \"notify\":\n return \"Tell the owner\";\n case \"search\":\n return \"Search the web\";\n case \"llm\":\n return \"Ask the model\";\n case \"agent\":\n return \"Agent step\";\n case \"zero\":\n return \"Buy paid data\";\n case \"automation\":\n return step.key ? `Run ${humanize(step.key)}` : \"Run another automation\";\n default:\n return humanize(step.kind);\n }\n}\n\n/** \"api.example.com · up to $0.05\": where it goes and the most it may pay. */\nfunction zeroSub(step) {\n const host = /^https:\\/\\/([^\\s/{]+)/i.exec(String(step.url ?? \"\").trim())?.[1] ?? \"\";\n const price = Number.isFinite(step.max_pay_usd) && step.max_pay_usd > 0 ? `up to $${Number(step.max_pay_usd).toString()}` : \"\";\n return [host || \"Needs an address\", price].filter(Boolean).join(\" · \");\n}\n\n/** The one quiet line under the title: \"verb: object\". */\nexport function stepSub(step) {\n switch (step.kind) {\n case \"action\":\n return step.app ? `in ${humanize(step.app)}` : \"App action\";\n case \"tool\":\n return step.needs?.category ? `Using ${step.needs.category}` : \"Connected tool\";\n case \"condition\":\n return cut(strip(step.if), 52);\n case \"loop\":\n return cut(`over ${strip(step.over)}${step.max ? `, up to ${step.max}` : \"\"}`, 52);\n case \"brain.read\":\n return Object.keys(step.input ?? {}).length ? `with ${Object.keys(step.input).slice(0, 3).join(\", \")}` : \"Company brain\";\n case \"set\":\n return `Sets ${Object.keys(step.values ?? {}).slice(0, 3).join(\", \") || \"values\"}`;\n case \"wait\":\n return step.until ? cut(strip(step.until), 52) : \"Nothing runs meanwhile\";\n case \"stop\":\n return cut(strip(step.message), 52);\n case \"notify\":\n return cut(strip(step.message), 52);\n case \"search\":\n return cut(strip(step.query) || \"Web search\", 52);\n case \"llm\":\n return step.tier === \"smart\" ? \"Careful model\" : \"Quick model\";\n case \"agent\":\n return cut(strip(step.instructions) || \"Needs instructions\", 52);\n case \"zero\":\n return cut(zeroSub(step), 52);\n case \"automation\":\n return \"Another automation\";\n default:\n return \"\";\n }\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/automation-model/graph.js",
|
|
26
|
+
"type": "registry:component",
|
|
27
|
+
"target": "@components/makersclaw/blocks/automation-model/graph.js",
|
|
28
|
+
"content": "// The canvas's layout: an automation (a trigger and a tree of steps)\n// turned into boxes and curved connectors, left to right.\n//\n// Pure. No React, no DOM, no measuring: every size is a constant, so the\n// same definition always gives the same picture and a test can read it.\n//\n// What the picture says:\n// - the trigger at the left, then the steps, a connector with a + between\n// any two of them, and a + bubble where a path ends;\n// - a condition is a router: its branches are labelled paths, one above\n// the other, and they meet again at a small join dot where the next\n// step carries on. A branch that ends in a Stop never meets it;\n// - \"for each\" is a dashed box around its steps, with a door in and a\n// door out; collapsed it is one card that lists them.\n//\n// The format has no cycles and no free joins, so a branch can only rejoin\n// where its condition ends. That is what keeps this a layout, not a\n// graph-drawing problem.\n//\n// Every connector knows its `slot`: the place in the definition a step\n// inserted there would land (edit.js insertStep).\n\nimport { KINDS, stepSub, stepTitle, triggerTitle } from \"./catalog\";\n\n/* ---- sizes -------------------------------------------------------------- */\n\nexport const T = 72; // a step's tile\nconst PITCH = 196; // one step's column: the tile, its label, room for a connector\nconst LABEL_BELOW = 76; // the title and the quiet line under a tile\nconst ABOVE_TILE = 22; // a badge and a step number above it\nconst JOIN_W = 52; // the column a join dot takes\nconst MIN_BRANCH = 110; // an empty branch is still a path\nconst BRANCH_GAP = 30;\nconst LOOP_PAD_L = 44;\nconst LOOP_PAD_R = 44;\nconst LOOP_PAD_T = 46;\nconst LOOP_PAD_B = 22;\nconst LOOP_LEAD = 28; // before the box's door\nconst LOOP_MIN_INNER = 150;\nconst PORT = 14;\nconst END = 28; // the + bubble at a path's end\nconst END_GAP = 64;\nconst CARD_W = 232;\nexport const MARGIN = 90;\n/** The label under a tile is this wide, centred on the tile. */\nexport const LABEL_W = 156;\n\n/* ---- small helpers ------------------------------------------------------ */\n\nconst emptyFrag = () => ({ nodes: [], edges: [], groups: [], entry: null, tails: [], w: 0, above: 0, below: 0 });\n\nfunction shift(f, dx, dy) {\n for (const n of f.nodes) {\n n.x += dx;\n n.y += dy;\n }\n for (const g of f.groups) {\n g.x += dx;\n g.y += dy;\n }\n return f;\n}\n\nfunction absorb(into, f) {\n into.nodes.push(...f.nodes);\n into.edges.push(...f.edges);\n into.groups.push(...f.groups);\n}\n\nlet edgeCounter = 0;\nfunction edge(from, to, t, plus = true) {\n edgeCounter += 1;\n return { id: `e${edgeCounter}:${from}>${to}`, from, to, label: t.label, branch: t.branch, slot: t.slot, plus: plus && t.slot !== null };\n}\n\nexport const sameSlot = (a, b) => a.index === b.index && JSON.stringify(a.list) === JSON.stringify(b.list);\n\n/* ---- steps -------------------------------------------------------------- */\n\nfunction leaf(step, num) {\n const info = KINDS[step.kind] ?? KINDS.action;\n return {\n id: step.id,\n type: \"step\",\n stepId: step.id,\n kind: step.kind,\n x: (PITCH - T) / 2,\n y: -T / 2,\n w: T,\n h: T,\n title: stepTitle(step),\n sub: stepSub(step),\n tone: info.tone,\n mark: info.mark,\n num,\n };\n}\n\nfunction stepFrag(step, ref, index, hasNext, ctx) {\n const after = { list: ref, index: index + 1 };\n if (step.kind === \"condition\") return conditionFrag(step, ref, index, hasNext, ctx);\n if (step.kind === \"loop\") return loopFrag(step, ref, index, ctx);\n const f = emptyFrag();\n f.nodes.push(leaf(step, ctx.next()));\n f.entry = step.id;\n // A Stop ends its path: nothing follows it.\n f.tails = step.kind === \"stop\" ? [] : [{ from: step.id, slot: after }];\n f.w = PITCH;\n f.above = T / 2 + ABOVE_TILE;\n f.below = T / 2 + LABEL_BELOW;\n return f;\n}\n\nfunction seq(steps, ref, ctx) {\n const out = emptyFrag();\n let prev = [];\n let x = 0;\n steps.forEach((step, i) => {\n const f = shift(stepFrag(step, ref, i, i < steps.length - 1, ctx), x, 0);\n absorb(out, f);\n if (i === 0) out.entry = f.entry;\n else if (f.entry) for (const t of prev) out.edges.push(edge(t.from, f.entry, t, true));\n prev = f.tails;\n x += f.w;\n out.above = Math.max(out.above, f.above);\n out.below = Math.max(out.below, f.below);\n });\n out.tails = prev;\n out.w = x;\n return out;\n}\n\nfunction conditionFrag(step, ref, index, hasNext, ctx) {\n const f = emptyFrag();\n f.nodes.push(leaf(step, ctx.next()));\n f.entry = step.id;\n const branches = [\n { key: \"then\", label: \"Yes\", list: step.then ?? [], ref: { at: \"then\", step: step.id } },\n { key: \"else\", label: \"No\", list: step.else ?? [], ref: { at: \"else\", step: step.id } },\n ];\n const built = branches.map((b) => ({ b, frag: seq(b.list, b.ref, ctx) }));\n const wb = Math.max(MIN_BRANCH, ...built.map((x) => x.frag.w));\n\n // Stack the branches: the first above the main line, the second below it.\n const [first, second] = built;\n const line0 = -(BRANCH_GAP / 2 + first.frag.below);\n const line1 = BRANCH_GAP / 2 + second.frag.above;\n const lines = [line0, line1];\n\n const joinId = `${step.id}:join`;\n const tails = [];\n built.forEach(({ b, frag }, k) => {\n const start = { from: step.id, label: b.label, branch: b.key, slot: { list: b.ref, index: 0 } };\n if (frag.entry) {\n shift(frag, PITCH, lines[k]);\n absorb(f, frag);\n f.edges.push(edge(step.id, frag.entry, start, true));\n tails.push(...frag.tails);\n } else {\n tails.push(start);\n }\n });\n\n const width = PITCH + wb;\n if (hasNext) {\n f.nodes.push({\n id: joinId,\n type: \"join\",\n stepId: step.id,\n kind: null,\n x: width + JOIN_W / 2 - 7,\n y: -7,\n w: 14,\n h: 14,\n title: \"\",\n sub: \"\",\n tone: \"plain\",\n num: 0,\n });\n for (const t of tails) f.edges.push(edge(t.from, joinId, t, true));\n f.tails = [{ from: joinId, slot: { list: ref, index: index + 1 } }];\n f.w = width + JOIN_W;\n } else {\n f.tails = tails;\n f.w = width + 20;\n }\n f.above = Math.max(T / 2 + ABOVE_TILE, -line0 + first.frag.above);\n f.below = Math.max(T / 2 + LABEL_BELOW, line1 + second.frag.below);\n return f;\n}\n\nfunction loopFrag(step, ref, index, ctx) {\n const f = emptyFrag();\n const after = { list: ref, index: index + 1 };\n const num = ctx.next();\n const info = KINDS.loop;\n const inId = `${step.id}:in`;\n const outId = `${step.id}:out`;\n const innerRef = { at: \"loop\", step: step.id };\n const title = stepTitle(step);\n const sub = stepSub(step);\n const inside = step.steps ?? [];\n\n if (ctx.collapsed.has(step.id)) {\n // One card that lists what is inside.\n const items = inside.map((s) => ({ id: s.id, title: stepTitle(s) }));\n const h = 64 + Math.max(1, items.length) * 32;\n f.nodes.push({\n id: step.id,\n type: \"card\",\n stepId: step.id,\n kind: \"loop\",\n x: LOOP_LEAD,\n y: -h / 2,\n w: CARD_W,\n h,\n title,\n sub,\n tone: info.tone,\n num,\n items,\n });\n f.entry = step.id;\n f.tails = [{ from: step.id, slot: after }];\n f.w = LOOP_LEAD + CARD_W + 36;\n f.above = h / 2 + 12;\n f.below = h / 2 + 12;\n return f;\n }\n\n const inner = seq(inside, innerRef, ctx);\n const empty = inside.length === 0;\n const innerW = empty ? LOOP_MIN_INNER : Math.max(LOOP_MIN_INNER, inner.w);\n const above = empty ? T / 2 + ABOVE_TILE : inner.above;\n const below = empty ? T / 2 + 12 : inner.below;\n const w = LOOP_PAD_L + innerW + LOOP_PAD_R;\n const h = above + below + LOOP_PAD_T + LOOP_PAD_B;\n f.groups.push({ id: step.id, stepId: step.id, x: LOOP_LEAD, y: -(above + LOOP_PAD_T), w, h, title, sub });\n const port = (id, x) => ({\n id,\n type: \"port\",\n stepId: step.id,\n kind: \"loop\",\n x: x - PORT / 2,\n y: -PORT / 2,\n w: PORT,\n h: PORT,\n title: \"\",\n sub: \"\",\n tone: info.tone,\n num: 0,\n });\n f.nodes.push(port(inId, LOOP_LEAD), port(outId, LOOP_LEAD + w));\n f.entry = inId;\n if (!empty && inner.entry) {\n shift(inner, LOOP_LEAD + LOOP_PAD_L, 0);\n absorb(f, inner);\n f.edges.push(edge(inId, inner.entry, { slot: { list: innerRef, index: 0 } }, true));\n for (const t of inner.tails) f.edges.push(edge(t.from, outId, t, true));\n } else {\n f.edges.push(edge(inId, outId, { slot: { list: innerRef, index: 0 } }, true));\n }\n f.tails = [{ from: outId, slot: after }];\n f.w = LOOP_LEAD + w + 36;\n f.above = above + LOOP_PAD_T + 6;\n f.below = below + LOOP_PAD_B + 6;\n return f;\n}\n\n/* ---- the whole picture --------------------------------------------------- */\n\n/**\n * layoutAutomation(def, { triggerWords, collapsed })\n * → { nodes, edges, groups, bounds }\n * `triggerWords` is the trigger in words (\"Weekdays at 07:00\"); `collapsed`\n * a Set of loop step ids drawn as one card.\n */\nexport function layoutAutomation(def, opts = {}) {\n edgeCounter = 0;\n let counter = 0;\n const ctx = { collapsed: opts.collapsed ?? new Set(), next: () => (counter += 1) };\n const trig = def.trigger ?? { type: \"manual\" };\n\n const f = emptyFrag();\n f.nodes.push({\n id: \"trigger\",\n type: \"trigger\",\n stepId: null,\n kind: \"trigger\",\n x: (PITCH - T) / 2,\n y: -T / 2,\n w: T,\n h: T,\n title: triggerTitle(trig.type),\n sub: opts.triggerWords ?? (trig.type === \"schedule\" ? trig.cron : \"Run it when you press Run\"),\n tone: \"plain\",\n num: 0,\n triggerType: trig.type,\n });\n const body = shift(seq(def.steps ?? [], { at: \"root\" }, ctx), PITCH, 0);\n absorb(f, body);\n const root = { list: { at: \"root\" }, index: 0 };\n let tails;\n if (body.entry) {\n f.edges.push(edge(\"trigger\", body.entry, { slot: root }, true));\n tails = body.tails;\n } else {\n tails = [{ from: \"trigger\", slot: root }];\n }\n\n // A + bubble at the end of every path that goes on.\n const byId = new Map(f.nodes.map((n) => [n.id, n]));\n tails.forEach((t, i) => {\n const src = byId.get(t.from);\n if (!src) return;\n const id = `end:${i}`;\n f.nodes.push({\n id,\n type: \"end\",\n stepId: null,\n kind: null,\n x: src.x + src.w + END_GAP,\n y: src.y + src.h / 2 - END / 2,\n w: END,\n h: END,\n title: body.entry ? \"Add a step\" : \"Add the first step\",\n sub: \"\",\n tone: \"plain\",\n num: 0,\n slot: t.slot,\n });\n f.edges.push(edge(t.from, id, t, false));\n });\n\n return { nodes: f.nodes, edges: f.edges, groups: f.groups, bounds: boundsOf(f.nodes, f.groups) };\n}\n\nfunction boundsOf(nodes, groups) {\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const n of nodes) {\n // A tile's label hangs below it and is wider than the tile.\n const labelled = n.type === \"step\" || n.type === \"trigger\";\n const nx0 = labelled ? n.x - (PITCH - T) / 2 : n.x;\n const nx1 = labelled ? n.x + n.w + (PITCH - T) / 2 : n.x + n.w;\n const ny1 = labelled ? n.y + n.h + LABEL_BELOW : n.y + n.h;\n x0 = Math.min(x0, nx0);\n y0 = Math.min(y0, n.y - (labelled ? ABOVE_TILE : 0));\n x1 = Math.max(x1, nx1);\n y1 = Math.max(y1, ny1);\n }\n for (const g of groups) {\n x0 = Math.min(x0, g.x);\n y0 = Math.min(y0, g.y);\n x1 = Math.max(x1, g.x + g.w);\n y1 = Math.max(y1, g.y + g.h);\n }\n if (!Number.isFinite(x0)) return { x: 0, y: 0, w: 0, h: 0 };\n return { x: x0 - MARGIN, y: y0 - MARGIN, w: x1 - x0 + MARGIN * 2, h: y1 - y0 + MARGIN * 2 };\n}\n\n/* ---- connectors ---------------------------------------------------------- */\n\n/** A connector from one node's right side to another's left side:\n * { d, mid, start, end, at(t) }. `mid` is where the + sits. */\nexport function curveBetween(a, b) {\n const x1 = a.x + a.w;\n const y1 = a.y + a.h / 2;\n const x2 = b.x;\n const y2 = b.y + b.h / 2;\n const start = { x: x1, y: y1 };\n const end = { x: x2, y: y2 };\n const mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };\n if (Math.abs(y2 - y1) < 1 || x2 <= x1) {\n return { d: `M ${x1} ${y1} L ${x2} ${y2}`, mid, start, end, at: (t) => ({ x: x1 + (x2 - x1) * t, y: y1 + (y2 - y1) * t }) };\n }\n const dx = Math.max(48, (x2 - x1) * 0.5);\n const c1 = { x: x1 + dx, y: y1 };\n const c2 = { x: x2 - dx, y: y2 };\n const at = (t) => {\n const u = 1 - t;\n return {\n x: u * u * u * x1 + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * x2,\n y: u * u * u * y1 + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * y2,\n };\n };\n return { d: `M ${x1} ${y1} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${x2} ${y2}`, mid, start, end, at };\n}\n\n/** The nodes by id, for connectors. */\nexport function nodeIndex(layout) {\n return new Map(layout.nodes.map((n) => [n.id, n]));\n}\n\n/** The selectable nodes in reading order (left to right, top to bottom):\n * the keyboard's walk through the canvas. */\nexport function readingOrder(layout) {\n return layout.nodes\n .filter((n) => n.type === \"step\" || n.type === \"trigger\" || n.type === \"card\")\n .sort((a, b) => a.x - b.x || a.y - b.y);\n}\n\n/** What the end of a path is called, for a screen reader. */\nexport function slotWords(slot) {\n if (slot.list.at === \"root\") return slot.index === 0 ? \"at the start\" : \"in the main path\";\n if (slot.list.at === \"loop\") return `inside ${slot.list.step}`;\n return `in the ${slot.list.at === \"then\" ? \"yes\" : \"no\"} path of ${slot.list.step}`;\n}\n"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "registry/blocks/automation-model/edit.js",
|
|
32
|
+
"type": "registry:component",
|
|
33
|
+
"target": "@components/makersclaw/blocks/automation-model/edit.js",
|
|
34
|
+
"content": "// Edits to a definition, as pure functions: each takes the definition and\n// returns a new one, never touching the old (so undo is a stack of them).\n//\n// The canvas speaks in slots (graph.js: a list and a place in it) and in\n// step ids; the format speaks in `then`, `else` and `steps` lists. This\n// file is the only place the two meet.\n//\n// Step ids are the merge key between a shipped version and a member's\n// edit, so an edit never renames one.\n\nimport { newStep } from \"./catalog\";\n\nconst clone = (v) => JSON.parse(JSON.stringify(v));\n\n/** Every step id in the definition, nested ones included. */\nexport function allStepIds(steps) {\n const ids = new Set();\n const walk = (list) => {\n for (const s of list) {\n ids.add(s.id);\n if (s.kind === \"condition\") {\n walk(s.then ?? []);\n walk(s.else ?? []);\n } else if (s.kind === \"loop\") walk(s.steps ?? []);\n }\n };\n walk(steps ?? []);\n return ids;\n}\n\n/** A step by id, wherever it sits. */\nexport function findStep(steps, id) {\n for (const s of steps ?? []) {\n if (s.id === id) return s;\n if (s.kind === \"condition\") {\n const hit = findStep(s.then ?? [], id) ?? findStep(s.else ?? [], id);\n if (hit) return hit;\n } else if (s.kind === \"loop\") {\n const hit = findStep(s.steps ?? [], id);\n if (hit) return hit;\n }\n }\n return null;\n}\n\n/** The list a ref names, in a (mutable, cloned) definition. A condition's\n * empty branch is made when it is first needed. */\nfunction listOf(steps, ref) {\n if (ref.at === \"root\") return steps;\n const owner = findStep(steps, ref.step);\n if (!owner) return null;\n if (ref.at === \"loop\") return owner.kind === \"loop\" ? (owner.steps ??= []) : null;\n if (owner.kind !== \"condition\") return null;\n if (ref.at === \"then\") return (owner.then ??= []);\n return (owner.else ??= []);\n}\n\n/** Put a step into the definition at a slot. */\nexport function insertStep(def, slot, step) {\n const next = clone(def);\n const list = listOf(next.steps, slot.list);\n if (!list) return def;\n list.splice(Math.max(0, Math.min(slot.index, list.length)), 0, step);\n return next;\n}\n\n/** A new step of `kind` at a slot, with an id nobody uses. */\nexport function addStep(def, slot, kind) {\n const step = newStep(kind, allStepIds(def.steps));\n return { def: insertStep(def, slot, step), id: step.id };\n}\n\n/** Where a step sits: its list and its index. */\nexport function locate(steps, id, ref = { at: \"root\" }) {\n for (let i = 0; i < (steps ?? []).length; i += 1) {\n const s = steps[i];\n if (s.id === id) return { list: ref, index: i };\n if (s.kind === \"condition\") {\n const hit = locate(s.then ?? [], id, { at: \"then\", step: s.id }) ?? locate(s.else ?? [], id, { at: \"else\", step: s.id });\n if (hit) return hit;\n } else if (s.kind === \"loop\") {\n const hit = locate(s.steps ?? [], id, { at: \"loop\", step: s.id });\n if (hit) return hit;\n }\n }\n return null;\n}\n\n/** The slot right after a step: where \"add a step here\" puts one. */\nexport function slotAfter(def, id) {\n const at = locate(def.steps, id);\n return at ? { list: at.list, index: at.index + 1 } : null;\n}\n\n/** The end of the main path: where a palette press adds a step when none\n * is selected. */\nexport function slotAtEnd(def) {\n return { list: { at: \"root\" }, index: def.steps.length };\n}\n\n/** How many steps a step holds inside it (a condition's branches, a loop). */\nexport function innerCount(step) {\n if (step.kind === \"condition\") return (step.then?.length ?? 0) + (step.else?.length ?? 0);\n if (step.kind === \"loop\") return step.steps?.length ?? 0;\n return 0;\n}\n\n/** Take a step out, with everything inside it. */\nexport function removeStep(def, id) {\n const next = clone(def);\n const cut = (list) => {\n const i = list.findIndex((s) => s.id === id);\n if (i >= 0) {\n list.splice(i, 1);\n return true;\n }\n for (const s of list) {\n if (s.kind === \"condition\" && (cut(s.then ?? []) || cut(s.else ?? []))) return true;\n if (s.kind === \"loop\" && cut(s.steps ?? [])) return true;\n }\n return false;\n };\n return cut(next.steps) ? next : def;\n}\n\n/** Change a step's own fields. Its id and kind never change. */\nexport function updateStep(def, id, patch) {\n const next = clone(def);\n const step = findStep(next.steps, id);\n if (!step) return def;\n for (const [k, v] of Object.entries(patch)) {\n if (k === \"id\" || k === \"kind\") continue;\n if (v === undefined) delete step[k];\n else step[k] = v;\n }\n return next;\n}\n\n/** Move a step one place earlier or later within its own list. */\nexport function nudgeStep(def, id, by) {\n const at = locate(def.steps, id);\n if (!at) return def;\n const next = clone(def);\n const list = listOf(next.steps, at.list);\n if (!list) return def;\n const to = at.index + by;\n if (to < 0 || to >= list.length) return def;\n const [moved] = list.splice(at.index, 1);\n list.splice(to, 0, moved);\n return next;\n}\n\n/** A copy of a step (and what is inside it) right after it, with fresh\n * ids. → { def, id } or null. */\nexport function duplicateStep(def, id) {\n const at = locate(def.steps, id);\n const step = findStep(def.steps, id);\n if (!at || !step) return null;\n const taken = allStepIds(def.steps);\n const copy = clone(step);\n const rename = (s) => {\n const base = s.id.replace(/_\\d+$/, \"\");\n let n = 2;\n let fresh = `${base}_${n}`;\n while (taken.has(fresh)) fresh = `${base}_${(n += 1)}`;\n taken.add(fresh);\n s.id = fresh;\n if (s.kind === \"condition\") {\n (s.then ?? []).forEach(rename);\n (s.else ?? []).forEach(rename);\n } else if (s.kind === \"loop\") (s.steps ?? []).forEach(rename);\n };\n rename(copy);\n return { def: insertStep(def, { list: at.list, index: at.index + 1 }, copy), id: copy.id };\n}\n\n/** What a step may read: the steps that run before it (and, in a loop, the\n * item). For the data picker. → { steps: [{ id, kind, label }], inLoop } */\nexport function availableBefore(def, id, titleOf) {\n const out = [];\n let inLoop = false;\n const entry = (s) => ({ id: s.id, kind: s.kind, label: titleOf(s) });\n const walk = (list, inside) => {\n for (const s of list) {\n if (s.id === id) {\n inLoop = inside;\n return true;\n }\n out.push(entry(s));\n // A step inside a branch or a loop sees what ran before it there, and\n // the step that holds it; never the sibling branch it did not take.\n if (s.kind === \"condition\") {\n const then = s.then ?? [];\n const other = s.else ?? [];\n if (findStep(then, id)) return walk(then, inside);\n if (findStep(other, id)) return walk(other, inside);\n } else if (s.kind === \"loop\" && findStep(s.steps ?? [], id)) return walk(s.steps ?? [], true);\n }\n return false;\n };\n walk(def.steps ?? [], false);\n return { steps: out, inLoop };\n}\n\n/** A key from a name: `Morning read` becomes `morning-read`. */\nexport function keyFromName(name, taken = new Set()) {\n let base = String(name ?? \"\")\n .toLowerCase()\n .normalize(\"NFKD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 52);\n if (!/^[a-z]/.test(base)) base = `a-${base}`.replace(/-+$/, \"\");\n if (base === \"a\" || base === \"\") base = \"automation\";\n if (!taken.has(base)) return base;\n for (let n = 2; n < 1000; n += 1) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;\n return `${base}-${taken.size + 1}`;\n}\n\n/** An empty automation: a trigger and nothing else. */\nexport function blankAutomation(name, key, trigger = { type: \"manual\" }) {\n return {\n format: 1,\n key,\n name,\n trigger,\n inputs: {},\n settings: { overlap: \"skip\", on_failure: \"retry_then_agent\", only_if_changed: false },\n steps: [],\n };\n}\n\n/** The step a problem's path points into, for the red mark on its card:\n * `steps[2].then[0].input.since` becomes the step at that place. A path\n * that is not inside a step (`trigger.cron`, `settings`) answers null. */\nexport function stepIdForPath(def, path) {\n const segs = [...String(path ?? \"\").matchAll(/(steps|then|else)\\[(\\d+)\\]/g)].map((m) => ({ key: m[1], idx: Number(m[2]) }));\n if (segs.length === 0 || segs[0].key !== \"steps\") return null;\n let step = def.steps?.[segs[0].idx];\n for (const seg of segs.slice(1)) {\n if (!step) break;\n let list;\n if (step.kind === \"condition\" && (seg.key === \"then\" || seg.key === \"else\")) list = step[seg.key] ?? [];\n else if (step.kind === \"loop\" && seg.key === \"steps\") list = step.steps;\n const next = list?.[seg.idx];\n if (!next) break;\n step = next;\n }\n return step?.id ?? null;\n}\n\n/** A definition as text with its keys in order, so two that read the same\n * are equal whatever order they came in. */\nexport function canonicalJson(value) {\n const sort = (v) => {\n if (Array.isArray(v)) return v.map(sort);\n if (v && typeof v === \"object\") return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])]));\n return v;\n };\n return JSON.stringify(sort(value));\n}\n"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"path": "registry/blocks/automation-model/overlay.js",
|
|
38
|
+
"type": "registry:component",
|
|
39
|
+
"target": "@components/makersclaw/blocks/automation-model/overlay.js",
|
|
40
|
+
"content": "// What a run did, read off the canvas's layout.\n//\n// A run is a list of step rows, one per attempt per place a step ran; a\n// step inside a loop has a row for every pass (`loop[3]/step`). The\n// canvas draws a definition, not a run, so this file lines the two up:\n// for every node, how it went; for every connector, whether the run went\n// along it.\n//\n// Pure; no React. The rule that matters: the path taken is lit and the\n// paths not taken are dimmed, so a run reads as one route through the\n// flow.\n//\n// A step row: { path, step_id, kind?, attempt, status, started_at,\n// ended_at, credits, error: { code, words } | null, input?, output?,\n// fix?: { by: agent | member, kind, status?, before?, after?, why?, credits?,\n// effect?: read | write | send, undo? } | null }.\n// status: running | done | failed | skipped | waiting | unknown.\n// A TEST run (a dry run) reads real data and changes nothing: a step that\n// stood in for itself carries `simulated: true` (and `output.would` when the\n// viewer may read data), a step that needs what a simulated step would have\n// made carries `unchecked: true`.\n\nimport { wouldWords } from \"./words\";\n\nconst num = (v) => {\n const n = typeof v === \"string\" ? Number(v) : (v ?? 0);\n return Number.isFinite(n) ? n : 0;\n};\n\n/** The strongest state wins: a step that failed on one item failed. */\nconst RANK = { failed: 8, unknown: 7, running: 6, waiting: 5, fixed: 4, would: 3, done: 2, skipped: 1, notreached: 0 };\n\n/** The latest attempt of every place a step ran. */\nexport function latestRows(rows) {\n const byPath = new Map();\n for (const r of rows ?? []) {\n const prior = byPath.get(r.path);\n if (!prior || r.attempt > prior.attempt) byPath.set(r.path, r);\n }\n return [...byPath.values()];\n}\n\n// A step the AGENT fixed wears the fixed mark; a person's own edit of the\n// run's data is a step that is done, with the edit noted on it. A step that\n// stood in for itself in a test run is \"would\": it would have run, it did not.\nconst stateOf = (row) => {\n if (row.status === \"done\" && row.simulated === true) return \"would\";\n return row.status === \"done\" && row.fix && row.fix.by !== \"member\" ? \"fixed\" : row.status;\n};\n\n/** Per step id: { stepId, state, passes, credits, words, would, unchecked,\n * fix, started_at, ended_at, rows }. `would` is what the step would have\n * done, in words, when its state is \"would\" (else null); `unchecked` is true\n * when a pass of it could not be checked in a test run. */\nexport function summarizeSteps(rows) {\n const out = new Map();\n for (const row of latestRows(rows)) {\n const state = stateOf(row);\n const prior = out.get(row.step_id);\n const words = row.error && typeof row.error.words === \"string\" ? row.error.words : null;\n if (!prior) {\n out.set(row.step_id, {\n stepId: row.step_id,\n state,\n passes: 1,\n credits: num(row.credits),\n words: state === \"failed\" || state === \"unknown\" ? words : null,\n would: state === \"would\" ? wouldWords(row) : null,\n unchecked: row.unchecked === true,\n fix: row.fix ?? null,\n started_at: row.started_at ?? null,\n ended_at: row.ended_at ?? null,\n rows: [row],\n });\n continue;\n }\n prior.passes += 1;\n prior.credits += num(row.credits);\n prior.rows.push(row);\n if (row.unchecked === true) prior.unchecked = true;\n if (RANK[state] > RANK[prior.state]) {\n prior.state = state;\n prior.words = state === \"failed\" || state === \"unknown\" ? words : prior.words;\n }\n if (state === \"would\" && prior.state === \"would\" && !prior.would) prior.would = wouldWords(row);\n if (row.fix && !prior.fix) prior.fix = row.fix;\n if (row.started_at && (!prior.started_at || row.started_at < prior.started_at)) prior.started_at = row.started_at;\n if (row.ended_at && (!prior.ended_at || row.ended_at > prior.ended_at)) prior.ended_at = row.ended_at;\n }\n return out;\n}\n\n/** Which way a condition went: its own output when the member may read it,\n * else whichever branch the run went into. */\nfunction branchTaken(cond, summary, layout) {\n const own = summary.get(cond.id);\n const out = own?.rows[0]?.output;\n if (out && (out.taken === \"then\" || out.taken === \"else\")) return out.taken;\n const target = (key) => {\n const to = layout.edges.find((e) => e.from === cond.id && e.branch === key)?.to;\n return to ? layout.nodes.find((n) => n.id === to) : undefined;\n };\n const isBody = (n) => !!n && (n.type === \"step\" || n.type === \"card\" || n.type === \"port\");\n const entered = (n) => isBody(n) && (n.type === \"port\" ? summary.has(n.stepId ?? \"\") : summary.has(n.id));\n const [yes, no] = [target(\"then\"), target(\"else\")];\n if (entered(yes)) return \"then\";\n if (entered(no)) return \"else\";\n // A branch with nothing in it leaves no trace: if the condition finished\n // and the other branch was never entered, the run went down the empty one.\n if (own && (own.state === \"done\" || own.state === \"fixed\")) {\n if (isBody(yes) && !isBody(no)) return \"else\";\n if (isBody(no) && !isBody(yes)) return \"then\";\n }\n return null;\n}\n\n/**\n * overlayOf(layout, rows, runStatus)\n * → { summary, nodes, edges, taken }\n * `nodes` by node id: done | would | running | failed | fixed | waiting | skipped |\n * unknown | notreached. `edges` by connector id: taken | running | dim.\n * `taken` by condition id: \"then\" | \"else\".\n */\nexport function overlayOf(layout, rows, runStatus) {\n const summary = summarizeSteps(rows);\n const nodes = new Map();\n const edges = new Map();\n const taken = new Map();\n const reached = new Set([\"trigger\"]);\n\n nodes.set(\"trigger\", (rows ?? []).length || runStatus ? \"done\" : \"notreached\");\n // A loop's box wears its loop's state.\n for (const g of layout.groups) nodes.set(g.id, summary.get(g.stepId)?.state ?? \"notreached\");\n for (const n of layout.nodes) {\n if (n.type === \"step\" || n.type === \"card\") {\n const s = summary.get(n.id);\n nodes.set(n.id, s ? s.state : \"notreached\");\n if (s) reached.add(n.id);\n }\n // A loop's doors: in when it started, out once it came through.\n if (n.type === \"port\" && n.stepId) {\n const s = summary.get(n.stepId);\n const isOut = n.id.endsWith(\":out\");\n if (s && (!isOut || s.state === \"done\" || s.state === \"fixed\")) reached.add(n.id);\n }\n }\n for (const n of layout.nodes) {\n if (n.type === \"step\" && n.kind === \"condition\") {\n const b = branchTaken(n, summary, layout);\n if (b) taken.set(n.id, b);\n }\n }\n\n const byId = new Map(layout.nodes.map((n) => [n.id, n]));\n const decide = (e) => {\n if (!reached.has(e.from)) return \"dim\";\n if (e.branch && taken.get(e.from) !== e.branch) return \"dim\";\n const to = byId.get(e.to);\n if (!to || to.type === \"end\") return \"dim\";\n if (to.type === \"join\") return \"taken\";\n if (!reached.has(e.to)) return \"dim\";\n return summary.get(e.to)?.state === \"running\" ? \"running\" : \"taken\";\n };\n // A join is reached once a connector into it was taken; a few passes\n // settle the one level of joins a condition makes.\n for (let round = 0; round < 3; round += 1) {\n for (const e of layout.edges) {\n const state = decide(e);\n edges.set(e.id, state);\n const to = byId.get(e.to);\n if (to?.type === \"join\" && state === \"taken\") reached.add(to.id);\n }\n }\n return { summary, nodes, edges, taken };\n}\n"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"path": "registry/blocks/automation-model/fixes.js",
|
|
44
|
+
"type": "registry:component",
|
|
45
|
+
"target": "@components/makersclaw/blocks/automation-model/fixes.js",
|
|
46
|
+
"content": "// The agent's fixes and a person's run controls, in words and in rules.\n//\n// Pure; no React. Three questions the screens ask:\n//\n// draftOfStep(fixes, stepId) is there a change for next time, drafted\n// for this step, and has a person decided?\n// changeLines(patch, def) what a drafted change would touch, by step\n// controlsOf(run, rows, opts) which controls a run offers right now\n// retryFromOf(run, rows, def, stepId)\n// whether \"Retry from this step\" shows for a\n// step, where it starts, and whether it asks\n//\n// A run's `fixes` are the fixer's attempts (GET automation-runs/<id>):\n// [{ id, step_path, step_id, attempt, status, credits, drafts: [{ id, why,\n// status: proposed | kept | dropped, base_version, new_version }] }].\n// A drafted change is `{ changes: [{ id: <step id>, input?, values?, retry?,\n// prompt?, system?, query? }] }`.\n//\n// And the versions of one automation, for a screen that shares the draft with\n// the agent: which version is the newest, whether the draft is the agent's and\n// what it said it did, what changed between two definitions, and the words of\n// \"a newer version was saved while you were editing\".\n\nimport { findStep } from \"./edit\";\nimport { stepTitle } from \"./catalog\";\nimport { latestRows } from \"./overlay\";\nimport { ACTIVE, triggerWords } from \"./words\";\n\n/** The two words a card wears for a step that was fixed or changed:\n * \"The agent fixed this\" or, for a person's own edit, \"You changed its data\". */\nexport function fixTag(fix) {\n if (!fix) return \"\";\n if (fix.by === \"member\") return fix.kind === \"skip\" ? \"You skipped it\" : \"You changed its data\";\n return \"The agent fixed this\";\n}\n\n/** The newest draft the fixer made for a step id (any pass of it), or null. */\nexport function draftOfStep(fixes, stepId) {\n let found = null;\n for (const f of fixes ?? []) {\n if (f.step_id !== stepId) continue;\n for (const d of f.drafts ?? []) found = d;\n }\n return found;\n}\n\nconst PARTS = { retry: \"how it tries again\", prompt: \"the words it asks with\", system: \"its instructions\", query: \"what it searches for\" };\n\n/** What a drafted change touches: [{ stepId, title, what }], one line per\n * step. A value is named by its key, never shown. */\nexport function changeLines(patch, def) {\n const out = [];\n for (const change of Array.isArray(patch?.changes) ? patch.changes : []) {\n if (!change || typeof change.id !== \"string\") continue;\n const step = def ? findStep(def.steps, change.id) : null;\n const parts = [];\n for (const field of [\"input\", \"values\"]) {\n const v = change[field];\n if (v && typeof v === \"object\") parts.push(...Object.keys(v).map((k) => `“${k}”`));\n }\n for (const [key, words] of Object.entries(PARTS)) if (change[key] !== undefined) parts.push(words);\n out.push({ stepId: change.id, title: step ? stepTitle(step) : change.id, what: parts.length ? parts.join(\", \") : \"its settings\" });\n }\n return out;\n}\n\n/** The step a parked run is stuck at: the last that failed or whose send is\n * not known to have gone. */\nfunction blockedRow(rows) {\n let found = null;\n for (const r of latestRows(rows)) if (r.status === \"failed\" || r.status === \"unknown\") found = r;\n return found;\n}\n\n/** The ways a run can be limited by money: only the run's own limit can be\n * raised from here; the wallet, the owner's cap and the app's weekly\n * allowance are not this screen's to lift. */\nconst RAISABLE = \"run_cap\";\n\n/**\n * Which controls a run offers right now.\n *\n * run { status, paused?, cap_credits?, error? }\n * rows the run's step rows\n * opts { stepsVisible }: the member may read a step's data (so may edit it)\n *\n * → { state: ended | active | paused | parked, pause, resume, stop, retry,\n * skip, edit, raise, blocked: { path, stepId, unknownSend } | null,\n * raiseDefault }\n */\nexport function controlsOf(run, rows, { stepsVisible = false } = {}) {\n const off = { state: \"ended\", pause: false, resume: false, stop: false, retry: false, skip: false, edit: false, raise: false, blocked: null, raiseDefault: 0 };\n if (!run || !ACTIVE.has(run.status)) return off;\n const paused = Boolean(run.paused);\n const parked = run.status === \"needs_you\";\n const row = parked ? blockedRow(rows) : null;\n const blocked = row ? { path: row.path, stepId: row.step_id, unknownSend: row.status === \"unknown\" } : null;\n const cap = Number(run.cap_credits) || 0;\n return {\n state: paused ? \"paused\" : parked ? \"parked\" : \"active\",\n pause: !paused && !parked,\n resume: paused || parked,\n stop: true,\n retry: Boolean(blocked),\n skip: Boolean(blocked),\n edit: Boolean(blocked) && stepsVisible && !blocked.unknownSend,\n raise: parked && run.error?.code === RAISABLE,\n blocked,\n raiseDefault: cap > 0 ? Math.max(10, Math.ceil(cap / 2)) : 100,\n };\n}\n\nconst OVER = new Set([\"done\", \"failed\", \"stopped\"]);\n\n/**\n * \"Retry from this step\": start a run over at a step, running it and every\n * step after it again. It is offered for a step that ran, in a run that is\n * over (done, failed, stopped) or parked on its owner; never in one that is\n * going or paused, and never in a run the app's own job runner started.\n *\n * run { status, trigger_key? }\n * rows the run's step rows\n * def the definition the run used\n * stepId the step the member is looking at\n *\n * → { show, path, title, inLoop, loopTitle, sent }\n * path the step to start from, at the TOP level of the flow: a step\n * inside a loop or a branch starts over from the loop or branch\n * that holds it (the platform takes the top-level step's id)\n * inLoop the step sits inside another step, so `path` is that step\n * loopTitle that holding step's name, for the words\n * sent a step from there on is known to have sent something, so ask\n * first; the platform asks too (`needs_confirm_resend`) when this\n * cannot tell\n */\nexport function retryFromOf(run, rows, def, stepId) {\n const off = { show: false, path: null, title: null, inLoop: false, loopTitle: null, sent: false };\n if (!run || !def || !Array.isArray(def.steps) || !stepId) return off;\n if (!OVER.has(run.status) && run.status !== \"needs_you\") return off;\n if (run.paused || (typeof run.trigger_key === \"string\" && run.trigger_key.startsWith(\"brief:\"))) return off;\n const at = def.steps.findIndex((top) => top.id === stepId || findStep([top], stepId));\n if (at < 0) return off;\n const top = def.steps[at];\n const latest = latestRows(rows);\n // Only a step that ran can be started over from, and the platform starts at\n // the top-level step's own place.\n if (!latest.some((r) => r.step_id === stepId) || !latest.some((r) => r.path === top.id)) return off;\n const index = new Map();\n def.steps.forEach((s, i) => {\n index.set(s.id, i);\n for (const inner of nestedIds(s)) index.set(inner, i);\n });\n const sent = latest.some((r) => (index.get(r.step_id) ?? -1) >= at && (r.status === \"done\" || r.status === \"unknown\") && (r.status === \"unknown\" || r.effect === \"send\" || r.fix?.effect === \"send\"));\n const inLoop = top.id !== stepId;\n return { show: true, path: top.id, title: stepTitle(findStep([top], stepId)), inLoop, loopTitle: inLoop ? stepTitle(top) : null, sent };\n}\n\nfunction nestedIds(step) {\n const out = [];\n for (const list of [step.steps, step.then, step.else]) {\n for (const s of Array.isArray(list) ? list : []) out.push(s.id, ...nestedIds(s));\n }\n return out;\n}\n\n/** The words of \"Retry from this step\". */\nexport const RETRY_FROM = {\n button: \"Retry from this step\",\n what: \"Runs this step and every step after it again. The steps before it keep what they made.\",\n inLoop: (title) => `This step is inside “${title}”, so it starts over from there.`,\n ask: \"It already sent something. Run it again from here?\",\n yes: \"Run it again\",\n no: \"Cancel\",\n};\n\n/** A control's answer in words: what it did, or why it could not. */\nexport const CONTROL_DONE = {\n pause: \"Paused. Nothing new starts until you resume it.\",\n resume: \"Resumed.\",\n stop: \"Stopped.\",\n retry: \"Trying that step again.\",\n skip: \"Skipped that step.\",\n edit: \"Trying that step again with your data.\",\n retry_from: \"Starting over from that step.\",\n};\n\n/** The platform's refusal codes, for when it sends no words of its own. */\nexport const CONTROL_REFUSED = {\n not_found: \"That run is gone.\",\n not_active: \"This run has already ended.\",\n not_blocked: \"Nothing is stuck right now.\",\n running: \"A step is running. Try again in a moment.\",\n needs_confirm: \"Say whether it went first.\",\n unknown_step: \"That step is not part of this run.\",\n output_needed: \"A later step uses what this one makes, so it cannot be skipped.\",\n bad_request: \"That did not work. Check what you typed.\",\n bad_step: \"That step has not run in this run, so there is nothing to start over from.\",\n busy: \"This automation is already running. Let that one finish, or stop it, then start over here.\",\n not_controllable: \"This run was started by the app's own job runner, so it cannot be changed here.\",\n needs_confirm_resend: \"A step from here on already sent something. Confirm that is right, or start over from a later step.\",\n};\n\n/* ---- the versions, and the agent's draft ------------------------------------ */\n\n/** The newest version number: the highest in `versions`, else the draft's,\n * else the live one's. Null when there is none (an automation not saved). */\nexport function latestVersionOf({ versions, draftVersion, liveVersion } = {}) {\n let top = null;\n for (const v of Array.isArray(versions) ? versions : []) {\n const n = Number(v?.version);\n if (Number.isInteger(n) && (top === null || n > top)) top = n;\n }\n if (top !== null) return top;\n for (const n of [draftVersion, liveVersion]) if (Number.isInteger(n) && (top === null || n > top)) top = n;\n return top;\n}\n\n/** Whether a version row was written by the agent. */\nconst byAgent = (v) => Boolean(v) && (v.kind === \"agent\" || v.author === \"agent\");\n\n/** The note on an agent's version, said as a person would: \"also copy Owen\".\n * The agent files it as \"Changed: also copy Owen\"; the prefix goes. */\nfunction noteWords(note) {\n const text = typeof note === \"string\" ? note.trim().replace(/^changed:\\s*/i, \"\").replace(/^changed by the agent\\.?$/i, \"\").trim() : \"\";\n return text.length > 160 ? `${text.slice(0, 159).trimEnd()}…` : text;\n}\n\n/**\n * The draft, when the agent wrote it and it is not live yet: `{ version,\n * note }` (the note may be \"\"), else null. The editor draws \"Drawn by the\n * agent · from: <note>\" under its header while this is not null.\n */\nexport function agentDraftOf({ versions, draftVersion, liveVersion } = {}) {\n if (!Number.isInteger(draftVersion) || draftVersion === liveVersion) return null;\n const row = (Array.isArray(versions) ? versions : []).find((v) => v?.version === draftVersion);\n return byAgent(row) ? { version: draftVersion, note: noteWords(row.note) } : null;\n}\n\n/**\n * The words over \"a newer version arrived while you have changes\".\n * version the newer version's number\n * versions the version rows, to see who wrote it\n */\nexport function newerVersionWords(version, versions) {\n const row = (Array.isArray(versions) ? versions : []).find((v) => v?.version === version);\n return byAgent(row)\n ? `The agent drew v${version} from your last save. You have changes that are not saved.`\n : `Version ${version} was saved after you opened this. You have changes that are not saved.`;\n}\n\n/** Every step in a list with its nested ones, by id. */\nfunction indexSteps(steps, into = new Map()) {\n for (const s of Array.isArray(steps) ? steps : []) {\n into.set(s.id, s);\n indexSteps(s.then, into);\n indexSteps(s.else, into);\n indexSteps(s.steps, into);\n }\n return into;\n}\n\n/** A step without the steps inside it: a container's own change is its own fields. */\nconst bare = (s) => (s.kind === \"condition\" ? { ...s, then: undefined, else: undefined } : s.kind === \"loop\" ? { ...s, steps: undefined } : s);\n\n/**\n * What changed between two definitions, one plain line each, at most 20:\n * how it starts, its name, a step added, removed or changed (by its title),\n * its settings. Pure; a value is never shown, only what it touched. The\n * platform's agent writes the same lines for itself (agent-app\n * automation-verbs `changeLines`); this is the screen's copy of it.\n */\nexport function changeLinesBetween(before, after) {\n if (!before || !after) return [];\n const a = indexSteps(before.steps);\n const b = indexSteps(after.steps);\n const out = [];\n if (JSON.stringify(before.trigger ?? null) !== JSON.stringify(after.trigger ?? null)) out.push(`Starts: ${triggerWords(before.trigger)} → ${triggerWords(after.trigger)}`);\n if ((before.name ?? \"\") !== (after.name ?? \"\")) out.push(`Renamed to “${after.name ?? \"\"}”`);\n for (const [id, s] of b) if (!a.has(id)) out.push(`Added “${stepTitle(s)}”`);\n for (const [id, s] of a) if (!b.has(id)) out.push(`Removed “${stepTitle(s)}”`);\n for (const [id, s] of b) {\n const was = a.get(id);\n if (was && JSON.stringify(bare(was)) !== JSON.stringify(bare(s))) out.push(`Changed “${stepTitle(s)}”`);\n }\n if (JSON.stringify(before.settings ?? null) !== JSON.stringify(after.settings ?? null)) out.push(\"Changed its settings\");\n return out.slice(0, 20);\n}\n"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"path": "registry/blocks/automation-model/words.js",
|
|
50
|
+
"type": "registry:component",
|
|
51
|
+
"target": "@components/makersclaw/blocks/automation-model/words.js",
|
|
52
|
+
"content": "// The automation blocks' words: a schedule, a time, a cost, a health, a\n// run's outcome, each in the plain way a person reads them.\n//\n// Pure; the clock and the zone come in as arguments so a test can read\n// them. A time is shown in the automation's own zone: a 07:00 schedule\n// says 07:00 whoever is looking (an app once said \"07:00\" for a wake that\n// ran at 06:00 UTC; this file is why it cannot again).\n\n/* ---- a schedule in words (five-field cron) ------------------------------- */\n\nconst andList = (names) => {\n const unique = [...new Set(names)];\n if (unique.length <= 1) return unique[0] ?? \"\";\n return `${unique.slice(0, -1).join(\", \")} and ${unique[unique.length - 1]}`;\n};\n\nconst DAY_NAMES = [\"Sundays\", \"Mondays\", \"Tuesdays\", \"Wednesdays\", \"Thursdays\", \"Fridays\", \"Saturdays\"];\nconst MONTH_NAMES = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nconst pad2 = (n) => String(n).padStart(2, \"0\");\nconst clock = (h, m) => `${pad2(h)}:${pad2(m)}`;\nconst capital = (s) => (s ? s[0].toUpperCase() + s.slice(1) : s);\n\nfunction ordinal(n) {\n const last2 = n % 100;\n if (last2 >= 11 && last2 <= 13) return `${n}th`;\n return `${n}${[\"th\", \"st\", \"nd\", \"rd\"][n % 10] ?? \"th\"}`;\n}\n\n/** A field's numbers: a literal, a list, ranges inside the list, or a `*` /\n * `a-b` step. Sorted, each once; null when a piece is anything else or out\n * of [min, max]. */\nfunction fieldValues(field, min, max) {\n const out = new Set();\n for (const piece of field.split(\",\")) {\n const m = /^(?:(\\*)|(\\d{1,2})(?:-(\\d{1,2}))?)(?:\\/(\\d{1,2}))?$/.exec(piece);\n if (!m) return null;\n const step = m[4] ? Number(m[4]) : 1;\n if (step < 1) return null;\n const from = m[1] ? min : Number(m[2]);\n const to = m[1] ? max : m[3] !== undefined ? Number(m[3]) : m[4] ? max : from;\n if (from < min || to > max || from > to) return null;\n for (let v = from; v <= to; v += step) out.add(v);\n }\n return [...out].sort((a, b) => a - b);\n}\n\nfunction daysPhrase(dow) {\n if (dow === \"*\") return \"\";\n const values = fieldValues(dow, 0, 7);\n if (!values) return null;\n const days = [...new Set(values.map((d) => d % 7))];\n const key = [...days].sort((a, b) => a - b).join(\",\");\n if (days.length === 7) return \"\";\n if (key === \"1,2,3,4,5\") return \"weekdays\";\n if (key === \"0,6\") return \"weekends\";\n const mondayFirst = days.sort((a, b) => ((a + 6) % 7) - ((b + 6) % 7));\n return andList(mondayFirst.map((d) => DAY_NAMES[d]));\n}\n\n/** The cron as a sentence (\"Weekdays at 07:00\", \"Every 15 minutes\"), or\n * null when it is not one of the shapes read here. A cron that names both\n * days of the month and days of the week is null: cron runs it on EITHER,\n * which no short sentence says honestly. */\nexport function cronWords(cron) {\n const fields = String(cron ?? \"\").trim().split(/\\s+/);\n if (fields.length !== 5) return null;\n const [m, h, dom, mon, dow] = fields;\n const minute = /^\\d{1,2}$/.test(m) && Number(m) < 60 ? Number(m) : null;\n const hours = /^\\d{1,2}(,\\d{1,2})*$/.test(h) ? fieldValues(h, 0, 23) : null;\n const times = minute !== null && hours ? andList(hours.map((x) => clock(x, minute))) : null;\n\n if (dom !== \"*\" || mon !== \"*\") {\n if (dow !== \"*\" || !times) return null;\n const days = dom === \"*\" ? null : fieldValues(dom, 1, 31);\n const months = mon === \"*\" ? null : fieldValues(mon, 1, 12);\n if ((dom !== \"*\" && !days) || (mon !== \"*\" && !months)) return null;\n const monthWords = months ? andList(months.map((x) => MONTH_NAMES[x - 1])) : null;\n if (!days) return `At ${times} every day in ${monthWords}`;\n return `At ${times} on the ${andList(days.map(ordinal))} of ${monthWords ?? \"each month\"}`;\n }\n\n const days = daysPhrase(dow);\n if (days === null) return null;\n const withDays = (body) => (days ? `${capital(days)}, ${body}` : capital(body));\n const everyMin = /^\\*\\/(\\d{1,2})$/.exec(m);\n if (everyMin && h === \"*\") {\n const n = Number(everyMin[1]);\n if (n < 1 || n > 59) return null;\n return withDays(n === 1 ? \"every minute\" : `every ${n} minutes`);\n }\n if (m === \"*\" && h === \"*\") return withDays(\"every minute\");\n if (minute === null) return null;\n const past = minute ? ` at :${pad2(minute)}` : \"\";\n if (h === \"*\") return withDays(`every hour${past}`);\n const everyH = /^\\*\\/(\\d{1,2})$/.exec(h);\n if (everyH) {\n const n = Number(everyH[1]);\n if (n < 1 || n > 23) return null;\n return withDays(n === 1 ? `every hour${past}` : `every ${n} hours${past}`);\n }\n const range = /^(\\d{1,2})-(\\d{1,2})(?:\\/(\\d{1,2}))?$/.exec(h);\n if (range) {\n const [a, b] = [Number(range[1]), Number(range[2])];\n const step = range[3] ? Number(range[3]) : 1;\n if (a > 23 || b > 23 || a >= b || step < 1 || step > 23) return null;\n const every = step === 1 ? \"every hour\" : `every ${step} hours`;\n return withDays(`${every}, ${clock(a, minute)}–${clock(b, minute)}`);\n }\n if (!times) return null;\n return days ? `${capital(days)} at ${times}` : `Every day at ${times}`;\n}\n\n/* ---- triggers ------------------------------------------------------------ */\n\n/** A name the platform writes with underscores or dots, as a person says it\n * (\"add_people\" is \"add people\"). */\nconst plain = (name) => String(name ?? \"\").replace(/[._-]+/g, \" \").replace(/\\s+/g, \" \").trim().toLowerCase();\n\n/** The kinds a record trigger names, as a person says them: \"a contact\",\n * \"a contact or a deal\", \"a record\" when it names none. */\nfunction kindsWords(kinds) {\n const names = (Array.isArray(kinds) ? kinds : []).map(plain).filter(Boolean);\n if (names.length === 0) return \"a record\";\n return names.map((n) => `a ${n}`).join(\" or \");\n}\n\n/** \"Weekdays at 07:00\" / \"By hand\" / \"When “rows.imported” is sent\". The zone\n * is named only when it is not the team's own. */\nexport function triggerWords(trigger) {\n if (!trigger || trigger.type === \"manual\") return \"By hand\";\n switch (trigger.type) {\n case \"schedule\": {\n if (typeof trigger.cron !== \"string\") return \"By hand\";\n const said = cronWords(trigger.cron) ?? trigger.cron;\n const zone = typeof trigger.tz === \"string\" && trigger.tz !== \"team\" ? trigger.tz : null;\n return zone ? `${said} (${zone})` : said;\n }\n case \"event\":\n return trigger.topic ? `When “${trigger.topic}” is sent${trigger.app ? ` by ${trigger.app}` : \"\"}` : \"When an event is sent\";\n case \"action\":\n return trigger.action ? `When ${plain(trigger.action)} finishes${trigger.app ? ` in ${trigger.app}` : \"\"}` : \"When an app action finishes\";\n case \"record\": {\n const ops = Array.isArray(trigger.ops) && trigger.ops.length === 1 ? (trigger.ops[0] === \"create\" ? \"added\" : \"changed\") : \"added or changed\";\n return `When ${kindsWords(trigger.kinds)} is ${ops}${trigger.app ? ` in ${trigger.app}` : \"\"}`;\n }\n case \"webhook\":\n return \"When its web address is called\";\n case \"automation\": {\n const when = trigger.when === \"failed\" ? \"fails\" : trigger.when === \"any\" ? \"ends\" : \"finishes\";\n return trigger.key ? `When “${trigger.key}” ${when}` : \"When another automation finishes\";\n }\n default:\n return \"By hand\";\n }\n}\n\n/** What a delete asks first, in plain words: what it does to this one. */\nexport function deleteWords(name, status) {\n return `Deletes \"${name}\". Its runs stay in history.${status === \"on\" ? \" It is switched on, so deleting also switches it off.\" : \"\"}`;\n}\n\n/** A trigger type, as a person says it. */\nexport function triggerTypeWords(type) {\n switch (type) {\n case \"schedule\":\n return \"On schedule\";\n case \"manual\":\n return \"By hand\";\n case \"event\":\n return \"By an event\";\n case \"action\":\n return \"By an app action\";\n case \"record\":\n return \"By a record\";\n case \"webhook\":\n return \"By a web address\";\n case \"automation\":\n return \"By another automation\";\n default:\n return String(type ?? \"\");\n }\n}\n\n/* ---- times --------------------------------------------------------------- */\n\nfunction zoneOr(tz) {\n try {\n new Intl.DateTimeFormat(\"en\", { timeZone: tz });\n return tz;\n } catch {\n return \"UTC\";\n }\n}\n\n/** \"Tue 07:00\" for the next run, in the automation's zone; \"Today 07:00\"\n * and \"Tomorrow 07:00\" when it is that close. */\nexport function clockWords(iso, tz, now = new Date()) {\n if (!iso) return null;\n const at = new Date(iso);\n if (Number.isNaN(at.getTime())) return null;\n const zone = zoneOr(tz);\n const dayKey = (d) => new Intl.DateTimeFormat(\"en-CA\", { timeZone: zone, year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(d);\n const time = new Intl.DateTimeFormat(\"en-GB\", { timeZone: zone, hour: \"2-digit\", minute: \"2-digit\", hour12: false }).format(at);\n const today = dayKey(now);\n const that = dayKey(at);\n if (that === today) return `Today ${time}`;\n if (that === dayKey(new Date(now.getTime() + 86_400_000))) return `Tomorrow ${time}`;\n const weekday = new Intl.DateTimeFormat(\"en-GB\", { timeZone: zone, weekday: \"short\" }).format(at);\n const within = at.getTime() - now.getTime() < 6 * 86_400_000 && at.getTime() > now.getTime();\n if (within) return `${weekday} ${time}`;\n const date = new Intl.DateTimeFormat(\"en-GB\", { timeZone: zone, day: \"numeric\", month: \"short\" }).format(at);\n return `${date} ${time}`;\n}\n\n/** \"3m ago\", \"yesterday\", \"5d ago\". */\nexport function agoWords(iso, now = new Date()) {\n if (!iso) return null;\n const t = new Date(iso).getTime();\n if (Number.isNaN(t)) return null;\n const s = (now.getTime() - t) / 1000;\n if (s < 60) return \"just now\";\n if (s < 3600) return `${Math.floor(s / 60)}m ago`;\n if (s < 86_400) return `${Math.floor(s / 3600)}h ago`;\n if (s < 172_800) return \"yesterday\";\n return `${Math.floor(s / 86_400)}d ago`;\n}\n\n/** \"42 s\", \"3 min 10 s\". */\nexport function durationWords(from, to) {\n if (!from || !to) return null;\n const ms = new Date(to).getTime() - new Date(from).getTime();\n if (!Number.isFinite(ms) || ms < 0) return null;\n const s = Math.round(ms / 1000);\n if (s < 1) return \"under a second\";\n if (s < 60) return `${s} s`;\n const m = Math.floor(s / 60);\n if (m < 60) return `${m} min${s % 60 ? ` ${s % 60} s` : \"\"}`;\n return `${Math.floor(m / 60)} h ${m % 60} min`;\n}\n\n/* ---- money --------------------------------------------------------------- */\n\n/** One decimal only when there is one: 3 is \"3\", 2.4 is \"2.4\", 29000 is\n * \"29,000\". */\nexport const formatCreditAmount = (credits) => Number(credits).toLocaleString(\"en-US\", { minimumFractionDigits: 0, maximumFractionDigits: 1 });\n\n/** \"free\" for nothing, else \"2.4 cr\". */\nexport function creditsWords(value) {\n const n = typeof value === \"string\" ? Number(value) : (value ?? 0);\n if (!Number.isFinite(n) || n <= 0) return \"free\";\n return `${formatCreditAmount(Math.round(n * 10) / 10)} cr`;\n}\n\n/** The price tag of each metered step, by step id, from a run's estimate\n * ({ steps: [{ id, credits, credits_max }] }): the figure a run is\n * expected to spend there (a loop's inner step: across its passes). */\nexport function priceMap(estimate) {\n const out = new Map();\n for (const s of estimate?.steps ?? []) {\n const prior = out.get(s.id);\n out.set(s.id, { credits: (prior?.credits ?? 0) + s.credits, max: (prior?.max ?? 0) + s.credits_max });\n }\n return out;\n}\n\n/* ---- health and outcomes ------------------------------------------------- */\n\n/** A run's status as a tone and a word: { tone: green|blue|amber|red|grey,\n * text }. Never a status code. */\nexport function runStatusWords(status, dry = false) {\n if (dry) {\n switch (status) {\n case \"done\":\n return { tone: \"green\", text: \"Test passed\" };\n case \"failed\":\n return { tone: \"red\", text: \"Test failed\" };\n case \"stopped\":\n case \"needs_you\":\n return { tone: \"amber\", text: \"Test stopped\" };\n case \"running\":\n case \"queued\":\n return { tone: \"blue\", text: \"Testing\" };\n default:\n break;\n }\n }\n switch (status) {\n case \"done\":\n return { tone: \"green\", text: \"Done\" };\n case \"running\":\n return { tone: \"blue\", text: \"Running\" };\n case \"queued\":\n return { tone: \"blue\", text: \"Starting\" };\n case \"waiting\":\n return { tone: \"amber\", text: \"Waiting\" };\n case \"needs_you\":\n return { tone: \"amber\", text: \"Needs its owner\" };\n case \"failed\":\n return { tone: \"red\", text: \"Failed\" };\n case \"stopped\":\n return { tone: \"grey\", text: \"Stopped\" };\n default:\n return { tone: \"grey\", text: String(status ?? \"\") };\n }\n}\n\n/** What a test run says a step would have done: the platform's own words\n * when the viewer may read them, else a plain line. Never an id. */\nexport function wouldWords(row) {\n const w = row?.output?.would;\n const words = w && typeof w.words === \"string\" ? w.words.trim() : \"\";\n return words || \"Would run.\";\n}\n\n/** A test run, in one or two plain sentences: \"Test passed: nothing was sent\n * or changed. 3 steps would have run.\" `detail` is a run's account\n * ({ run }). Null for a run that was not a test. */\nexport function testWords(detail) {\n const run = detail?.run;\n if (!run || run.dry !== true) return null;\n const would = Number(run.result?.dry?.simulated ?? 0);\n const count = Number.isFinite(would) && would > 0 ? would : 0;\n if (run.status === \"done\") {\n const tail = count === 0 ? \"No step needed to change anything.\" : `${count} ${count === 1 ? \"step\" : \"steps\"} would have run.`;\n return `Test passed: nothing was sent or changed. ${tail}`;\n }\n if (run.status === \"failed\") {\n const why = run.error && typeof run.error.words === \"string\" ? run.error.words.trim() : \"\";\n return why ? `Test failed. ${why}` : \"Test failed.\";\n }\n if (run.status === \"stopped\" || run.status === \"needs_you\" || run.status === \"waiting\") {\n const why = run.error && typeof run.error.words === \"string\" ? run.error.words.trim() : \"\";\n return why ? `Test stopped: ${why.charAt(0).toLowerCase()}${why.slice(1)}` : run.status === \"stopped\" ? \"Test stopped.\" : \"Test stopped: it needs something first.\";\n }\n if (run.status === \"running\" || run.status === \"queued\") return \"Testing: reading your real data, changing nothing.\";\n return null;\n}\n\n/** The list's health word for an automation row: how it is doing now. */\nexport function healthOf(a) {\n if (a.status === \"needs_owner\") return { tone: \"amber\", text: \"Needs an owner\" };\n if (a.status === \"draft\" || a.live_version === null || a.live_version === undefined) return { tone: \"grey\", text: \"Not live yet\" };\n if (a.status === \"off\") return { tone: \"grey\", text: \"Off\" };\n if ((a.active_runs ?? 0) > 0) return { tone: \"blue\", text: \"Running\" };\n const last = a.last_run;\n if (!last) return { tone: \"grey\", text: \"Has not run yet\" };\n if (last.status === \"failed\") return { tone: \"red\", text: \"Last run failed\" };\n if (last.status === \"needs_you\") return { tone: \"amber\", text: \"Needs its owner\" };\n if (last.status === \"waiting\") return { tone: \"amber\", text: \"Waiting\" };\n return { tone: \"green\", text: \"Healthy\" };\n}\n\nexport function nodeStateWords(state) {\n switch (state) {\n case \"done\":\n return \"Done\";\n case \"running\":\n return \"Running\";\n case \"failed\":\n return \"Failed\";\n case \"fixed\":\n return \"Fixed by the agent\";\n case \"would\":\n return \"Would run\";\n case \"waiting\":\n return \"Waiting\";\n case \"skipped\":\n return \"Skipped\";\n case \"unknown\":\n return \"Needs a look\";\n case \"notreached\":\n return \"Not reached\";\n default:\n return \"\";\n }\n}\n\n/** The list's one line of where an automation lives: the team's, or an\n * app's. `appName(id)` names an app, or null. */\nexport function originWords(a, appName) {\n if (a.owner_kind === \"app\" && a.app_id) return appName?.(a.app_id) ?? \"An app\";\n return \"Team\";\n}\n\n/** \"Run 7 · 3m ago · Done\": a run, in one line. */\nexport function runLine(run, n) {\n const status = runStatusWords(run.status, run.dry === true).text;\n return `${run.is_test ? \"Test \" : \"Run \"}${n} · ${agoWords(run.created_at) ?? \"just now\"} · ${status}`;\n}\n\n/* ---- the list's and the runs' counts ------------------------------------- */\n\n/** What the list's one line counts: what is on, what failed, what waits for\n * its owner, and which runs next. */\nexport function summaryOf(rows, now = new Date()) {\n let on = 0;\n let failing = 0;\n let needs = 0;\n let next = null;\n for (const r of rows ?? []) {\n if (r.status === \"on\") on += 1;\n if (r.last_run?.status === \"failed\") failing += 1;\n if (r.status === \"needs_owner\" || r.last_run?.status === \"needs_you\") needs += 1;\n if (r.status === \"on\" && r.next_run_at && new Date(r.next_run_at) > now && (!next || r.next_run_at < next.next_run_at)) next = r;\n }\n return { on, failing, needs, next };\n}\n\n/** A run still going or waiting on someone. */\nexport const ACTIVE = new Set([\"queued\", \"running\", \"waiting\", \"needs_you\"]);\n\n/** An automation's runs, counted: what finished, what failed, what is\n * going, and what it cost. */\nexport function overviewOf(runs) {\n let done = 0;\n let failed = 0;\n let active = 0;\n let credits = 0;\n for (const r of runs ?? []) {\n if (r.status === \"done\") done += 1;\n else if (r.status === \"failed\") failed += 1;\n else if (ACTIVE.has(r.status)) active += 1;\n const c = typeof r.credits === \"string\" ? Number(r.credits) : r.credits;\n if (Number.isFinite(c)) credits += c;\n }\n return { done, failed, active, credits };\n}\n\n/** The report a run left, as short lines (a number or a word each). */\nexport function reportLines(result) {\n if (!result) return [];\n const out = [];\n for (const [k, v] of Object.entries(result)) {\n if (out.length >= 8) break;\n if (v === null || v === undefined) continue;\n if (typeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\") out.push({ label: k.replace(/_/g, \" \"), value: String(v) });\n }\n return out;\n}\n"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"path": "registry/blocks/automation-model/schedule.js",
|
|
56
|
+
"type": "registry:component",
|
|
57
|
+
"target": "@components/makersclaw/blocks/automation-model/schedule.js",
|
|
58
|
+
"content": "// A schedule in a person's terms and in cron, both ways.\n//\n// The trigger picker offers the shapes people ask for (\"every weekday at\n// 07:00\"); the definition holds a five-field cron. Anything the picker\n// cannot read back stays a custom cron, kept exactly as written.\n\nconst clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));\n\n/** parts: { freq: daily|weekdays|weekly|hourly|custom, hour, minute,\n * weekday (0 is Sunday), cron } */\nexport function cronFromParts(p) {\n const h = clamp(Math.round(p.hour), 0, 23);\n const m = clamp(Math.round(p.minute), 0, 59);\n switch (p.freq) {\n case \"daily\":\n return `${m} ${h} * * *`;\n case \"weekdays\":\n return `${m} ${h} * * 1-5`;\n case \"weekly\":\n return `${m} ${h} * * ${clamp(Math.round(p.weekday), 0, 6)}`;\n case \"hourly\":\n return `${m} * * * *`;\n default:\n return p.cron;\n }\n}\n\nexport function partsFromCron(cron) {\n const c = String(cron ?? \"\").trim().replace(/\\s+/g, \" \");\n const base = { hour: 7, minute: 0, weekday: 1, cron: c };\n let m = /^(\\d{1,2}) (\\d{1,2}) \\* \\* \\*$/.exec(c);\n if (m) return { ...base, freq: \"daily\", minute: Number(m[1]), hour: Number(m[2]) };\n m = /^(\\d{1,2}) (\\d{1,2}) \\* \\* 1-5$/.exec(c);\n if (m) return { ...base, freq: \"weekdays\", minute: Number(m[1]), hour: Number(m[2]) };\n m = /^(\\d{1,2}) (\\d{1,2}) \\* \\* ([0-6])$/.exec(c);\n if (m) return { ...base, freq: \"weekly\", minute: Number(m[1]), hour: Number(m[2]), weekday: Number(m[3]) };\n m = /^(\\d{1,2}) \\* \\* \\* \\*$/.exec(c);\n if (m) return { ...base, freq: \"hourly\", minute: Number(m[1]) };\n return { ...base, freq: \"custom\" };\n}\n\nexport const WEEKDAYS = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\n\n/** `07:00` for a time input. */\nexport const timeText = (hour, minute) => `${String(hour).padStart(2, \"0\")}:${String(minute).padStart(2, \"0\")}`;\n\nexport function parseTime(text) {\n const m = /^(\\d{1,2}):(\\d{2})$/.exec(String(text ?? \"\"));\n if (!m) return null;\n const hour = Number(m[1]);\n const minute = Number(m[2]);\n return hour <= 23 && minute <= 59 ? { hour, minute } : null;\n}\n"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"path": "registry/blocks/automation-model/values.js",
|
|
62
|
+
"type": "registry:component",
|
|
63
|
+
"target": "@components/makersclaw/blocks/automation-model/values.js",
|
|
64
|
+
"content": "// A step's value as the text a person types, and back. Plain numbers,\n// true/false/null and JSON objects are kept as what they are; everything\n// else is text. A value that is one `{{ expression }}` is text the engine\n// reads later.\n\n/** A step's value, as the text a person types. */\nexport function valueToText(v) {\n if (v === undefined) return \"\";\n if (typeof v === \"string\") return v;\n if (v === null) return \"null\";\n return typeof v === \"object\" ? JSON.stringify(v) : String(v);\n}\n\n/** What a person typed, as a value. */\nexport function textToValue(text) {\n const t = String(text ?? \"\").trim();\n if (/^-?\\d+(\\.\\d+)?$/.test(t)) return Number(t);\n if (t === \"true\") return true;\n if (t === \"false\") return false;\n if (t === \"null\") return null;\n if ((t.startsWith(\"{\") && t.endsWith(\"}\") && !t.startsWith(\"{{\")) || (t.startsWith(\"[\") && t.endsWith(\"]\"))) {\n try {\n return JSON.parse(t);\n } catch {\n return text;\n }\n }\n return text;\n}\n"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"path": "registry/blocks/automation-model/check.js",
|
|
68
|
+
"type": "registry:component",
|
|
69
|
+
"target": "@components/makersclaw/blocks/automation-model/check.js",
|
|
70
|
+
"content": "// What is wrong with a definition, before the platform is asked.\n//\n// The platform validates every definition when it is saved (one validator,\n// whatever wrote the definition); a refusal comes back with `problems`,\n// one line each, and the builder shows those too. This file is the quick\n// check a screen makes on every edit, so a card can wear a red mark and\n// the Save button can say why it waits: the mistakes a person makes most,\n// in the words a person uses. It never says a definition is valid; only\n// the platform does.\n\nimport { KINDS, RUNNABLE } from \"./catalog\";\nimport { stepIdForPath } from \"./edit\";\n\n/** Words for the paths a person never sees spelled out. */\nfunction plainPath(path) {\n if (!path) return \"\";\n if (path.startsWith(\"trigger\")) return \"The trigger\";\n if (path.startsWith(\"settings\")) return \"The settings\";\n if (path.startsWith(\"inputs\")) return \"A question\";\n if (path.startsWith(\"name\")) return \"The name\";\n return \"\";\n}\n\n/** The platform's own wording for the mistakes a person makes most, in the\n * words a person uses. Anything not listed passes through as written. */\nexport function plainWords(message) {\n if (/^a condition needs steps in then, else or both/.test(message)) return \"Add a step to the Yes or the No path, with the + on it.\";\n if (/^a loop has steps/.test(message)) return \"Add a step inside the box, with the + on it.\";\n if (/^steps is a list with at least one step/.test(message)) return \"Add a first step to save it.\";\n const unavailable = /^([a-z.]+) steps are not available yet/.exec(message);\n if (unavailable) {\n const info = KINDS[unavailable[1]];\n return `${info?.name ?? unavailable[1]} steps cannot run yet. You can draw one, and save it once they can.`;\n }\n return message;\n}\n\nconst blank = (v) => typeof v !== \"string\" || v.trim() === \"\";\n\n/** The most one Zero step may pay for a call, in USD: the platform's own ceiling\n * (claw shared/src/automation.ts ZERO_STEP_MAX_PAY_USD; keep the two equal). */\nconst ZERO_MAX_PAY_USD = 2;\n\n/** One step's own problems: [{ path, message }]. */\nfunction stepProblems(step, path, runnable) {\n const out = [];\n const bad = (field, message) => out.push({ path: `${path}.${field}`, message });\n if (!KINDS[step.kind]) {\n out.push({ path, message: `${step.kind} is not a kind of step this editor knows.` });\n return out;\n }\n if (!runnable.has(step.kind)) out.push({ path, message: `${step.kind} steps are not available yet.` });\n switch (step.kind) {\n case \"action\":\n if (blank(step.action)) bad(\"action\", \"Pick the action to run.\");\n break;\n case \"tool\":\n if (blank(step.tool)) bad(\"tool\", \"Pick the tool to use.\");\n if (blank(step.needs?.category)) bad(\"needs\", \"Say what kind of account it uses.\");\n break;\n case \"condition\":\n if (blank(step.if)) bad(\"if\", \"Say when it goes the Yes way.\");\n if ((step.then ?? []).length + (step.else ?? []).length === 0) out.push({ path, message: \"a condition needs steps in then, else or both.\" });\n break;\n case \"loop\":\n if (blank(step.over)) bad(\"over\", \"Say which list to go through.\");\n if ((step.steps ?? []).length === 0) out.push({ path, message: \"a loop has steps to repeat.\" });\n break;\n case \"llm\":\n if (blank(step.prompt)) bad(\"prompt\", \"Say what to ask the model.\");\n break;\n case \"agent\":\n if (blank(step.instructions)) bad(\"instructions\", \"Tell the agent what to do.\");\n break;\n case \"search\":\n if (blank(step.query)) bad(\"query\", \"Say what to search for.\");\n break;\n case \"zero\":\n // A paid call needs the service, its address and a price it may not pass.\n if (blank(step.capability)) bad(\"capability\", \"Say which paid service it uses.\");\n if (blank(step.url) || (!/^https:\\/\\/[^\\s/]+/i.test(step.url.trim()) && !step.url.trimStart().startsWith(\"{{\"))) bad(\"url\", \"Give the service's address, starting with https://.\");\n if (!(typeof step.max_pay_usd === \"number\" && Number.isFinite(step.max_pay_usd) && step.max_pay_usd > 0 && step.max_pay_usd <= ZERO_MAX_PAY_USD)) bad(\"max_pay_usd\", `Set the most one call may cost: more than $0, and at most $${ZERO_MAX_PAY_USD}.`);\n break;\n case \"stop\":\n case \"notify\":\n if (blank(step.message)) bad(\"message\", \"Write the message.\");\n break;\n case \"wait\":\n if (blank(step.for) && blank(step.until)) bad(\"for\", \"Say how long to wait, or until when.\");\n break;\n case \"automation\":\n if (blank(step.key)) bad(\"key\", \"Say which automation to run.\");\n break;\n default:\n break;\n }\n return out;\n}\n\n/** Every step's problems, depth first, with the path the platform would\n * name (`steps[2].then[0].input`). */\nfunction walk(list, base, runnable, seen, out) {\n list.forEach((step, i) => {\n const path = `${base}[${i}]`;\n if (seen.has(step.id)) out.push({ path, message: `the step id ${step.id} is used twice.` });\n seen.add(step.id);\n out.push(...stepProblems(step, path, runnable));\n if (step.kind === \"condition\") {\n walk(step.then ?? [], `${path}.then`, runnable, seen, out);\n walk(step.else ?? [], `${path}.else`, runnable, seen, out);\n } else if (step.kind === \"loop\") walk(step.steps ?? [], `${path}.steps`, runnable, seen, out);\n });\n}\n\n/**\n * checkAutomation(def, { runnable })\n * → [{ path, message, stepId }]\n * `message` is for a person; `stepId` is the step it is about (null for\n * the flow or its trigger). An empty canvas is not an error, only not\n * finished, but it cannot be saved: the first problem says so.\n */\nexport function checkAutomation(def, { runnable = RUNNABLE } = {}) {\n const raw = [];\n if (blank(def.name)) raw.push({ path: \"name\", message: \"Give it a name.\" });\n const trigger = def.trigger ?? {};\n if (trigger.type === \"schedule\" && String(trigger.cron ?? \"\").trim().split(/\\s+/).length !== 5) {\n raw.push({ path: \"trigger.cron\", message: \"Use five fields: minute, hour, day, month, weekday.\" });\n }\n if (trigger.type === \"event\" && blank(trigger.topic)) raw.push({ path: \"trigger.topic\", message: \"Say which event starts it.\" });\n if (trigger.type === \"action\" && blank(trigger.action)) raw.push({ path: \"trigger.action\", message: \"Pick the action it waits for.\" });\n if (trigger.type === \"automation\" && blank(trigger.key)) raw.push({ path: \"trigger.key\", message: \"Pick the automation it follows.\" });\n if ((def.steps ?? []).length === 0) raw.push({ path: \"steps\", message: \"steps is a list with at least one step.\" });\n walk(def.steps ?? [], \"steps\", runnable, new Set(), raw);\n return raw.map((p) => {\n const said = plainWords(p.message);\n const stepId = stepIdForPath(def, p.path);\n // A blank canvas is not a mistake, only not finished.\n const who = p.path === \"steps\" ? \"\" : stepId ? `“${stepId}”` : plainPath(p.path);\n return { path: p.path, message: who ? `${who}: ${said}` : said, stepId };\n });\n}\n\n/** Turn the platform's refusal lines (`problems`) into the same shape. */\nexport function issuesFromProblems(def, problems) {\n return (problems ?? []).map((line) => {\n const m = /^([\\w[\\].]+):\\s*(.*)$/.exec(line);\n const path = m ? m[1] : \"\";\n const stepId = path ? stepIdForPath(def, path) : null;\n return { path, message: plainWords(m ? m[2] : line), stepId };\n });\n}\n\n/** The steps with something wrong, for the red marks. */\nexport function flaggedSteps(issues) {\n return new Set(issues.map((i) => i.stepId).filter(Boolean));\n}\n\n/** The lines to show in one step's own panel. */\nexport function issuesFor(issues, stepId) {\n return issues.filter((i) => i.stepId === stepId).map((i) => i.message.replace(/^“[^”]*”: /, \"\"));\n}\n"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"path": "registry/blocks/automation-model/templates.js",
|
|
74
|
+
"type": "registry:component",
|
|
75
|
+
"target": "@components/makersclaw/blocks/automation-model/templates.js",
|
|
76
|
+
"content": "// Starting points: a few ready-made flows a person can open and change,\n// so a first automation is never a blank canvas. They use only steps the\n// engine runs today; each is a whole definition with its blanks marked (an\n// action to pick, a message to write).\n//\n// Content, not code: add one by adding an entry. An app may hand its own\n// list to the list screen (`starts`), which is how an app offers flows\n// that name its own actions.\n\nimport { blankAutomation, keyFromName } from \"./edit\";\n\nfunction base(name, taken, trigger) {\n return blankAutomation(name, keyFromName(name, taken), trigger);\n}\n\n/** Each: { id, name, blurb, glyph, needs, build(taken) → definition }. */\nexport const STARTING_POINTS = [\n {\n id: \"weekly-reminder\",\n name: \"A reminder every Monday\",\n blurb: \"Leaves a note for you at the start of each week.\",\n glyph: \"trigger-schedule\",\n needs: null,\n build: (taken) => ({\n ...base(\"Monday reminder\", taken, { type: \"schedule\", cron: \"0 9 * * 1\", tz: \"team\" }),\n does: \"Leaves a note for the owner every Monday morning.\",\n steps: [{ id: \"note\", kind: \"notify\", label: \"Leave the reminder\", message: \"It is Monday. Look at the week ahead.\" }],\n }),\n },\n {\n id: \"check-then-tell\",\n name: \"Check something, then tell me\",\n blurb: \"Goes one way when a condition holds and another way when it does not.\",\n glyph: \"condition\",\n needs: \"A condition to check\",\n build: (taken) => ({\n ...base(\"Check and tell\", taken, { type: \"manual\" }),\n does: \"Checks a condition and says which way it went.\",\n steps: [\n {\n id: \"check\",\n kind: \"condition\",\n label: \"Is it worth a look?\",\n if: \"{{ true }}\",\n then: [{ id: \"say_yes\", kind: \"notify\", label: \"Tell me it is\", message: \"Yes, it is worth a look.\" }],\n else: [{ id: \"say_no\", kind: \"stop\", label: \"Nothing to do\", message: \"Nothing to look at today.\" }],\n },\n ],\n }),\n },\n {\n id: \"wait-then-follow-up\",\n name: \"Wait, then follow up\",\n blurb: \"Waits a couple of days, then leaves a note. Nothing runs while it waits.\",\n glyph: \"wait\",\n needs: null,\n build: (taken) => ({\n ...base(\"Follow up later\", taken, { type: \"manual\" }),\n does: \"Waits two days, then reminds the owner.\",\n steps: [\n { id: \"pause\", kind: \"wait\", for: \"2d\" },\n { id: \"remind\", kind: \"notify\", label: \"Remind me\", message: \"Time to follow up.\" },\n ],\n }),\n },\n {\n id: \"app-action-daily\",\n name: \"Run an app's action every morning\",\n blurb: \"Picks one of your apps' actions and runs it each weekday.\",\n glyph: \"action\",\n needs: \"An app and one of its actions\",\n build: (taken) => ({\n ...base(\"Morning action\", taken, { type: \"schedule\", cron: \"0 7 * * 1-5\", tz: \"team\" }),\n does: \"Runs an app's action each weekday morning.\",\n steps: [\n { id: \"act\", kind: \"action\", label: \"Run the action\", action: \"\", input: {} },\n { id: \"done\", kind: \"notify\", message: \"It ran.\" },\n ],\n }),\n },\n {\n id: \"ask-model\",\n name: \"Ask the model to write a line\",\n blurb: \"One model call, then a note with what it wrote.\",\n glyph: \"llm\",\n needs: \"What to ask\",\n build: (taken) => ({\n ...base(\"Write a line\", taken, { type: \"manual\" }),\n does: \"Asks the model for a line and leaves it as a note.\",\n steps: [\n { id: \"write\", kind: \"llm\", label: \"Write the line\", prompt: \"Write one friendly line that wishes the team a good week.\", tier: \"fast\" },\n { id: \"tell\", kind: \"notify\", message: \"{{ steps.write.output }}\" },\n ],\n }),\n },\n];\n\n/** The starting points the platform keeps (the desk's `GET automation-starts`\n * → `starts`), in the shape the list draws:\n * { id, name, blurb, glyph, needs (a line or null), ai (a line or \"\"),\n * price: { credits, per }, build(taken) → a whole definition }.\n * `needs` is joined into one plain line (\"payments, a mailbox\"); `price.per`\n * is \"each\" when the cost is per item, else null. `build` hands back a copy\n * of the definition with a key nobody has taken. Anything that is not a\n * start is left out. */\nexport function startsFromPlatform(list) {\n const out = [];\n for (const s of Array.isArray(list) ? list : []) {\n if (!s || typeof s !== \"object\" || typeof s.id !== \"string\" || !s.definition || typeof s.definition !== \"object\") continue;\n const needs = (Array.isArray(s.needs) ? s.needs : []).filter((x) => typeof x === \"string\" && x.trim()).map((x) => x.trim());\n const credits = Number(s.credits);\n out.push({\n id: s.id,\n name: String(s.name ?? s.definition.name ?? s.id),\n blurb: String(s.blurb ?? \"\"),\n glyph: typeof s.glyph === \"string\" && s.glyph ? s.glyph : undefined,\n needs: needs.length ? needs.join(\", \") : null,\n ai: typeof s.ai === \"string\" ? s.ai : \"\",\n price: { credits: Number.isFinite(credits) && credits > 0 ? credits : 0, per: s.per === \"each\" ? \"each\" : null },\n build: (taken) => {\n const def = JSON.parse(JSON.stringify(s.definition));\n def.key = keyFromName(def.name ?? s.name ?? s.id, taken instanceof Set ? taken : new Set(taken ?? []));\n return def;\n },\n });\n }\n return out;\n}\n"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"path": "registry/blocks/automation-model/glyph.jsx",
|
|
80
|
+
"type": "registry:component",
|
|
81
|
+
"target": "@components/makersclaw/blocks/automation-model/glyph.jsx",
|
|
82
|
+
"content": "\"use client\";\n\nimport { Icon } from \"../../ui\";\nimport { GLYPH_ICON } from \"./catalog\";\n\n// The face of a step kind, drawn with the kit's own icons. One table\n// (catalog.js GLYPH_ICON): the canvas, the picker, the outline and the\n// settings panel all ask it, so a kind wears one face everywhere.\n\n/** name: a glyph from the catalog (\"action\", \"condition\", \"trigger-schedule\" …). */\nexport function Glyph({ name, size = 22, className }) {\n return <Icon name={GLYPH_ICON[name] ?? \"zap\"} size={size} className={className} />;\n}\n"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"path": "registry/blocks/automation-model/parts.jsx",
|
|
86
|
+
"type": "registry:component",
|
|
87
|
+
"target": "@components/makersclaw/blocks/automation-model/parts.jsx",
|
|
88
|
+
"content": "\"use client\";\n\n// The small pieces the automation blocks share: a health chip and dot, and\n// the side panel every settings view sits in (a head with the thing's\n// face, a scrolling body, a foot). Presentational; words and callbacks in.\n\nimport { Icon, Select, cx } from \"../../ui\";\nimport { creditsWords, runLine } from \"./words\";\n\nconst CHIP = {\n green: \"border-success/40 text-success\",\n blue: \"border-info/40 text-info\",\n amber: \"border-warning/50 text-warning\",\n red: \"border-destructive/50 text-destructive\",\n grey: \"border-border text-muted-foreground\",\n};\nconst DOT = { green: \"bg-success\", blue: \"bg-info\", amber: \"bg-warning\", red: \"bg-destructive\", grey: \"bg-muted-foreground/60\" };\n\n/** A small word in a pill. tone: green | blue | amber | red | grey. */\nexport function Chip({ tone = \"grey\", className, children }) {\n return <span className={cx(\"inline-flex shrink-0 items-center whitespace-nowrap rounded-full border px-2 py-px text-xs font-medium\", CHIP[tone] ?? CHIP.grey, className)}>{children}</span>;\n}\n\n/** A dot of the same tones, beside a health word. */\nexport function Dot({ tone = \"grey\", className }) {\n return <i aria-hidden=\"true\" className={cx(\"inline-block size-2 shrink-0 rounded-full\", DOT[tone] ?? DOT.grey, className)} />;\n}\n\n/**\n * The panel beside (or, narrow, over) a canvas.\n * icon a kit Icon name drawn in the head\n * glyph or: a node here (automation-model's Glyph) instead\n * title, subtitle\n * onClose draws the close button\n * footer a node pinned under the body\n * label what a screen reader calls it\n */\nexport function SidePanel({ icon, glyph, title, subtitle, onClose, footer, label, tone = \"plain\", className, children }) {\n return (\n <aside aria-label={label ?? title} className={cx(\"flex min-h-0 min-w-0 flex-col bg-card text-card-foreground\", className)}>\n <header className=\"flex min-w-0 items-start gap-2.5 border-b border-border px-3.5 py-3\">\n {glyph || icon ? (\n <span className={cx(\"inline-flex size-9 shrink-0 items-center justify-center rounded-lg border\", tone === \"plain\" ? \"border-border text-muted-foreground\" : \"border-primary bg-primary/15 text-primary\")}>\n {glyph ?? <Icon name={icon} size={18} />}\n </span>\n ) : null}\n <div className=\"grid min-w-0 flex-1 gap-0.5\">\n <h3 className=\"m-0 break-words text-sm font-semibold leading-tight\">{title}</h3>\n {subtitle ? <p className=\"m-0 text-xs text-muted-foreground\">{subtitle}</p> : null}\n </div>\n {onClose ? (\n <button type=\"button\" aria-label=\"Close\" onClick={onClose} className=\"inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Icon name=\"x\" size={16} />\n </button>\n ) : null}\n </header>\n <div data-scroll className=\"grid min-h-0 flex-1 content-start gap-4 overflow-y-auto px-3.5 py-3.5\">\n {children}\n </div>\n {footer ? <footer className=\"flex flex-wrap items-center gap-2 border-t border-border px-3.5 py-2.5\">{footer}</footer> : null}\n </aside>\n );\n}\n\n/** A titled group inside a panel. */\nexport function PanelSection({ title, children }) {\n return (\n <section className=\"grid gap-3 border-t border-border pt-3\">\n <h4 className=\"m-0 text-2xs font-semibold uppercase tracking-wide text-muted-foreground\">{title}</h4>\n {children}\n </section>\n );\n}\n\n/**\n * Newest first, numbered from the oldest: the number a person says.\n * runs [{ id, status, created_at, is_test, credits }]\n * value the run shown, or null\n * onPick(runId)\n */\nexport function RunPicker({ runs, value = null, onPick, className }) {\n if (!runs?.length) return null;\n const total = runs.length;\n return (\n <span className={cx(\"block w-64 max-w-full\", className)}>\n <Select\n aria-label=\"Pick a run\"\n value={value ?? \"\"}\n onChange={(e) => e.target.value && onPick?.(e.target.value)}\n options={[...(value === null ? [{ value: \"\", label: \"Runs\" }] : []), ...runs.map((r, i) => ({ value: r.id, label: `${runLine(r, total - i)} · ${creditsWords(r.credits)}` }))]}\n />\n </span>\n );\n}\n"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"path": "registry/blocks/automation-model/README.md",
|
|
92
|
+
"type": "registry:file",
|
|
93
|
+
"target": "@components/makersclaw/blocks/automation-model/README.md",
|
|
94
|
+
"content": "# AutomationModel\n\nThe pure part of the automation blocks. It has no screen of its own: it is\nwhat `flow-editor`, `step-picker`, `step-settings`, `data-picker`,\n`trigger-picker`, `automation-list`, `automation-editor` and `run-view`\nall read, so a kind of step has one name, one face and one price flag\neverywhere, and the canvas, the outline and a run say the same thing about\nthe same definition. It is laid with the blocks that need it; you place it\non its own only to build a screen of your own on automations.\n\n```jsx\nimport { KINDS, layoutAutomation, triggerWords } from \"../components/blocks/automation-model\"; // a Makersclaw app\nimport { KINDS, layoutAutomation, triggerWords } from \"@/components/makersclaw/blocks/automation-model\"; // a shadcn project\n```\n\nAn automation is the platform's format 1: a trigger and an ordered list of\nsteps. A condition has `then` and `else` lists, a loop has a `steps` list,\nno arrows, no cycles. The platform checks every definition when it is\nsaved; this block only reads and dresses one.\n\n## What is in it\n\n- **The catalog** — `KINDS` (each kind's `name`, `blurb`, `glyph`, `tone`\n plain · model · agent, `group`, `paid`), `paletteGroups()`,\n `newStep(kind, takenIds)`, `stepTitle(step)`, `stepSub(step)`,\n `RUNNABLE` (the kinds the platform runs today; a prop on the blocks\n overrides it), `Glyph` (a kind's face, from the kit's icons).\n- **The layout** — `layoutAutomation(def, { triggerWords, collapsed })` →\n `{ nodes, edges, groups, bounds }`: boxes and curved connectors, left to\n right, a branch's two paths one above the other that meet at a join, a\n for-each as a dashed box or one card. Every connector carries its\n `slot`, the place in the definition a step added there would land.\n `curveBetween(a, b)`, `readingOrder(layout)`, `slotWords(slot)`.\n- **The edits** — pure, the old definition never touched: `addStep`,\n `insertStep`, `removeStep`, `updateStep`, `nudgeStep`, `duplicateStep`,\n `findStep`, `locate`, `slotAfter`, `slotAtEnd`, `availableBefore(def,\n id, stepTitle)` (what a step may read), `keyFromName`,\n `blankAutomation`, `canonicalJson`.\n- **A run** — `overlayOf(layout, stepRows, runStatus)` → how each card\n went, which connectors were walked, which way each condition took it;\n `summarizeSteps(rows)`. In a **test run** (a dry run: it reads real data\n and changes nothing) a step that stood in for itself has the state\n `would` (it would have run; it did not), ranked between `done` and\n `fixed`. Each summary also carries `would` (the step's own words, \"Would\n send with send_email.\", else \"Would run.\") and `unchecked` (it needs\n what a would-step would have made, so it could not be checked).\n- **Fixes and run controls** (`fixes.js`) — `draftOfStep(fixes, stepId)`\n (the change the agent drafted for a step), `changeLines(patch, def)` (the\n steps it would touch and what in them, by key, never a value),\n `controlsOf(run, rows, { stepsVisible })` (which controls a run offers\n right now), `retryFromOf(run, rows, def, stepId)` (whether \"Retry from this\n step\" shows for a step, the top-level step it starts at, and whether to\n ask first because something already sent; its words are `RETRY_FROM`), `fixTag(fix)` (the card's two words: \"The agent fixed this\",\n or for a person's own edit \"You changed its data\"), and the words\n `CONTROL_DONE` and `CONTROL_REFUSED`. For a screen that shares the draft\n with the agent: `changeLinesBetween(before, after)` (what differs between\n two definitions, one plain line each: how it starts, the name, a step\n added, removed or changed, the settings; never a value),\n `latestVersionOf({ versions, draftVersion, liveVersion })`,\n `agentDraftOf({ versions, draftVersion, liveVersion })` (the draft, when the\n agent wrote it and it is not live yet: `{ version, note }`) and\n `newerVersionWords(version, versions)`. A step the AGENT fixed is `fixed`;\n a person's own edit leaves the step `done`.\n- **The words** — `triggerWords`, `cronWords`, `clockWords(iso, tz)` (a\n time in the automation's own zone: a 07:00 schedule says 07:00 whoever\n looks), `agoWords`, `durationWords`, `creditsWords`, `healthOf(row)`,\n `runStatusWords(status, dry?)` (a test says Test passed, Test failed,\n Test stopped), `nodeStateWords`, `wouldWords(row)`, `testWords(detail)`\n (the one line a test run leaves: \"Test passed: nothing was sent or\n changed. 3 steps would have run.\"), `summaryOf(rows)`,\n `overviewOf(runs)`, `priceMap(estimate)`.\n- **The quick check** — `checkAutomation(def)` → `[{ path, message,\n stepId }]`: the mistakes a person makes most (a blank action, an empty\n branch, a step the platform cannot run yet), in words and by step. It\n never says a definition is valid; only the platform does.\n `issuesFromProblems(def, problems)` puts the platform's own refusal\n lines on their steps.\n- **The shared pieces** — `Chip`, `Dot`, `SidePanel`, `PanelSection`,\n `RunPicker`; `STARTING_POINTS`, the ready-made flows the list offers;\n `startsFromPlatform(starts)` turns the desk's own list (`useAutomations().starts`)\n into the same shape plus `ai` and `price: { credits, per }`, each with a\n `build(taken)` that returns a whole definition under a free key.\n\n## Never\n\n- Never a status by colour alone: each comes with its word.\n- Never a time shown in the viewer's zone for a schedule: pass the\n automation's `tz`.\n- Never trust `checkAutomation` over the platform: a save the platform\n refuses is the answer.\n\n## Example\n\n```jsx\n\"use client\";\nimport { KINDS, layoutAutomation, triggerWords } from \"../components/blocks/automation-model\";\n\n// How many cards a definition draws, and what starts it, in words.\nexport default function Summary({ definition }) {\n const layout = layoutAutomation(definition, { triggerWords: triggerWords(definition.trigger) });\n const cards = layout.nodes.filter((n) => n.type === \"step\" || n.type === \"card\");\n return (\n <p>\n {definition.name}: {cards.length} steps, {triggerWords(definition.trigger).toLowerCase()}.{\" \"}\n {cards.some((n) => KINDS[n.kind]?.paid) ? \"Some steps cost credits.\" : \"All steps are free.\"}\n </p>\n );\n}\n```\n"
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"categories": [
|
|
98
|
+
"automation",
|
|
99
|
+
"workflow",
|
|
100
|
+
"steps",
|
|
101
|
+
"trigger",
|
|
102
|
+
"schedule",
|
|
103
|
+
"cron",
|
|
104
|
+
"catalog",
|
|
105
|
+
"layout",
|
|
106
|
+
"graph",
|
|
107
|
+
"words",
|
|
108
|
+
"model",
|
|
109
|
+
"runs"
|
|
110
|
+
],
|
|
111
|
+
"docs": "Installed to components/makersclaw/blocks/automation-model. Import: import { ACTIVE, GLYPH_ICON, GROUP_LABELS, GROUP_ORDER, Glyph, KINDS, LABEL_W, MARGIN, RUNNABLE, STARTING_POINTS, startsFromPlatform, testWords, wouldWords, T, WEEKDAYS, addStep, agoWords, allStepIds, availableBefore, blankAutomation, canonicalJson, checkAutomation, clockWords, creditsWords, deleteWords, cronFromParts, cronWords, curveBetween, duplicateStep, durationWords, findStep, flaggedSteps, formatCreditAmount, healthOf, humanize, innerCount, insertStep, isRunnable, issuesFor, issuesFromProblems, keyFromName, latestRows, layoutAutomation, locate, newStep, nodeIndex, nodeStateWords, nudgeStep, originWords, overlayOf, overviewOf, paletteGroups, parseTime, partsFromCron, plainWords, priceMap, readingOrder, removeStep, reportLines, runLine, runStatusWords, sameSlot, slotAfter, slotAtEnd, slotWords, stepIdForPath, stepSub, stepTitle, summaryOf, summarizeSteps, textToValue, timeText, triggerTypeWords, triggerWords, uniqueStepId, updateStep, valueToText, Chip, Dot, PanelSection, SidePanel, RunPicker, CONTROL_DONE, CONTROL_REFUSED, RETRY_FROM, agentDraftOf, changeLines, changeLinesBetween, controlsOf, draftOfStep, fixTag, latestVersionOf, newerVersionWords, retryFromOf, TRIGGER_TITLES, triggerGlyph, triggerTitle } from \"@/components/makersclaw/blocks/automation-model\". Props and an example: components/makersclaw/blocks/automation-model/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
112
|
+
"meta": {
|
|
113
|
+
"makersclaw": {
|
|
114
|
+
"version": "0.8.2",
|
|
115
|
+
"useWhen": "You are building your own screen on automations (flow-editor, step-settings, automation-list and run-view all do) and need the catalog, the words or the layout without a canvas. Rarely placed on its own: it is laid with the blocks that need it."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "automations-screen",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "AutomationsScreen",
|
|
6
|
+
"description": "The whole Automations page of an app as ONE block. The places a member goes: the list (every automation as a row that opens whole, a switch, New automation with starting points and Describe it, the week's allowance shown once, a slot for jobs that run on request and one for the app's own extras); a new automation in the editor; one automation (its flow, edited, tested, kept and switched on; its Runs; the app's own tabs); and one run, step by step with the run's controls and the agent's fixes. It composes automation-list, automation-editor, run-view and cost, and decides what they leave open: which place is showing and how it travels in a page address (placeFromParams, paramsOfPlace), Back to where the member WAS (the list with the words in its search box, the Runs tab a run was opened from), a save that names the version the copy began on, a delete that asks first and can be undone for a few seconds, the platform's triggers and webhook handed to both editors, Describe it (to the platform, or handed on to the agent beside the page), and an older schedule an app has not updated yet shown read-only. It reads nothing itself: the engine's hooks come in as a hooks prop.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/automation-editor.json",
|
|
10
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/automation-list.json",
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/automation-model.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/cost.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/run-view.json"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": [],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"path": "registry/blocks/automations-screen/index.js",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "@components/makersclaw/blocks/automations-screen/index.js",
|
|
21
|
+
"content": "// The AutomationsScreen block — `makersclaw add automations-screen`. Fixed:\n// compose it through its props (README.md beside this file).\nexport { AutomationsScreen } from \"./automations-screen\";\nexport { deletedWords, normalizePlace, paramsOfPlace, placeFromParams } from \"./screen-model\";\n"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "registry/blocks/automations-screen/automations-screen.jsx",
|
|
25
|
+
"type": "registry:component",
|
|
26
|
+
"target": "@components/makersclaw/blocks/automations-screen/automations-screen.jsx",
|
|
27
|
+
"content": "\"use client\";\n\n// The Automations page of an app, as one block. The places a member goes:\n//\n// the list every automation as a row that opens whole, a switch,\n// New automation (starting points, Describe it), the\n// week's allowance once, and the app's own slots\n// New a new one in the editor; not saved until Create\n// one automation its flow, edited, tested, kept and switched on;\n// its Runs; the app's own tabs\n// one run step by step, with the run's controls and the\n// agent's fixes\n//\n// It composes `automation-list`, `automation-editor`, `run-view` and `cost`,\n// and decides only what they leave open: which place is showing (and how it\n// travels in a URL), Back to where the member WAS (the list with its search,\n// the Runs tab a run was opened from), what each editor is handed (the\n// platform's triggers, the webhook, the version a save began on), a delete\n// that asks first and can be undone, \"Describe it\", and the old schedule an\n// app has not updated yet (read-only).\n//\n// It reads nothing itself. The engine's hooks come in as `hooks`; a block may\n// not import lib/. Every wire is a prop; none given, the page still draws.\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport { AppLayout, Button, Notice, Skeleton, Tabs, toast } from \"../../ui\";\nimport { AutomationList } from \"../automation-list\";\nimport { AutomationEditor } from \"../automation-editor\";\nimport { RunView, RunsList } from \"../run-view\";\nimport { STARTING_POINTS, blankAutomation, keyFromName, startsFromPlatform } from \"../automation-model\";\nimport { Allowance } from \"../cost\";\nimport { FLOW, NEW, RUNS, backFrom, combineStarts, deletedWords, hooksProblem, normalizePlace, originAfter, tabsOf } from \"./screen-model\";\n\nconst FILL = \"flex min-h-0 flex-1 flex-col\";\nconst LIST = normalizePlace(null);\nconst UNDO_MS = 8000;\nconst LEGACY_WORDS = \"This one is the app's older schedule. Its steps come with the next update of the app; until then you can switch it on or off and run it.\";\nconst UNAVAILABLE_WORDS = \"Automations are not set up on this Makersclaw yet.\";\n\n/** A slot is a node, or a function that gets what it may need and returns one. */\nconst slot = (s, arg) => (typeof s === \"function\" ? s(arg) : s ?? null);\n\n/** The frame when the app gives none: the kit's AppLayout, filling the slot it is in. */\nfunction defaultFrame({ label, fill, children }) {\n return (\n <AppLayout height=\"parent\" contentLabel={label} contentMode={fill ? \"fill\" : \"scroll\"}>\n {children}\n </AppLayout>\n );\n}\n\nfunction Said({ children, onBack, back = \"Back to the automations\" }) {\n return (\n <div className=\"grid content-start justify-items-start gap-3 p-5 text-sm\">\n <p className=\"m-0 text-muted-foreground\">{children}</p>\n {onBack ? (\n <Button size=\"sm\" variant=\"secondary\" icon=\"arrow-left\" onClick={onBack}>\n {back}\n </Button>\n ) : null}\n </div>\n );\n}\n\n/* ---- the list ---------------------------------------------------------- */\n\nfunction ListPage({ c }) {\n const { list } = c;\n const unavailable = Boolean(list.unavailable);\n const waiting = list.loading && list.automations.length === 0 && !list.error && !unavailable;\n const failed = !unavailable && !list.loading && list.automations.length === 0 && Boolean(list.error);\n const ready = !unavailable && !waiting && !failed;\n const a = c.allowance;\n const body = (\n <div className=\"@container grid min-w-0 content-start\">\n {a ? (\n <div className=\"px-4 pt-4\">\n <Allowance used={a.used} of={a.of} words={a.words} title={null} className=\"max-w-md\" />\n </div>\n ) : null}\n {unavailable ? (\n <p className=\"m-0 p-4 text-sm text-muted-foreground\">{c.unavailableWords}</p>\n ) : waiting ? (\n <div aria-busy=\"true\" aria-label=\"Loading the automations\" className=\"grid gap-2 p-4\">\n <Skeleton className=\"h-6 w-1/3\" />\n <Skeleton shape=\"rect\" className=\"h-16 w-full\" />\n <Skeleton shape=\"rect\" className=\"h-16 w-full\" />\n <Skeleton shape=\"rect\" className=\"h-16 w-full\" />\n </div>\n ) : failed ? (\n <div className=\"p-4\">\n <Notice tone=\"danger\" title=\"The automations did not load.\" action={<Button size=\"sm\" onClick={() => list.reload?.()}>Try again</Button>}>\n Nothing was changed. Try again in a moment.\n </Notice>\n </div>\n ) : (\n <AutomationList\n rows={list.automations}\n grouped={false}\n title={c.title}\n intro={c.intro}\n now={c.now}\n starts={c.startList}\n query={c.query}\n onQueryChange={c.setQuery}\n onOpen={(id) => c.go({ automation: id })}\n onNew={(start) => c.go({ automation: NEW, ...(start ? { start: start.id } : {}) })}\n onToggle={c.toggle}\n canToggle={c.canToggle}\n canCreate={c.canCreate}\n canDelete={c.canDelete}\n {...(c.remove ? { onDelete: (row) => c.remove(row.id) } : {})}\n {...(c.describe ? { onDescribe: c.describe, describePrice: c.describePrice, describeChips: c.describeChips } : {})}\n />\n )}\n {ready && c.onRequest ? <div className=\"px-4 pb-4\">{slot(c.onRequest, { automations: list.automations })}</div> : null}\n {ready && c.extras ? <div className=\"px-4 pb-4\">{slot(c.extras, { automations: list.automations })}</div> : null}\n </div>\n );\n return c.render([{ label: c.title }], c.title, false, body);\n}\n\n/* ---- one automation ---------------------------------------------------- */\n\nfunction OnePage({ c, id, runId, tab }) {\n const { hooks, go, list } = c;\n const a = hooks.useAutomation(id, c.every ? { every: c.every } : undefined);\n const run = hooks.useAutomationRun(runId);\n const root = { label: c.title, href: c.hrefOf?.(LIST), onPress: () => go({}) };\n const crumb = [root, { label: a.automation?.name ?? \"Automation\" }];\n const toList = () => go({});\n\n if (a.unavailable) return c.render(crumb, \"Automation\", false, <Said>{c.unavailableWords}</Said>);\n if (!a.automation || !a.definition) {\n const gone = a.code === \"not_found\";\n return c.render(\n crumb,\n \"Automation\",\n false,\n a.error ? <Said onBack={toList}>{gone ? \"That automation is not there any more.\" : \"That automation could not be opened.\"}</Said> : <Said>Opening…</Said>,\n );\n }\n\n const au = a.automation;\n const legacy = au.source === \"legacy\";\n const tabList = tabsOf(c.extraTabs, au);\n // The app's own tab pressed (tabsOf keeps only the keys and labels the strip needs).\n const extra = tab !== FLOW && tab !== RUNS && tabList.some((t) => t.key === tab) ? (c.extraTabs ?? []).find((t) => t && t.key === tab) ?? null : null;\n const here = (t) => (t === FLOW ? { automation: id } : { automation: id, tab: t });\n const tabs = <Tabs value={extra ? tab : tab === RUNS ? RUNS : FLOW} onChange={(t) => go(here(t))} aria-label=\"This automation\" tabs={tabList} className=\"border-b-0\" />;\n const ctx = { automation: au, definition: a.definition, tab, runId, go };\n\n let body;\n let fill = true;\n if (runId) {\n fill = Boolean(run.detail);\n if (!run.detail) {\n body = run.error ? <Said onBack={() => go(here(RUNS))} back=\"Back to the runs\">That run could not be opened.</Said> : <Said>Opening the run…</Said>;\n } else {\n body = (\n <>\n <div className={FILL}>\n <RunView\n def={run.definition ?? a.definition}\n versionNote={run.definition ? null : `This run used version ${run.run?.version}. The steps drawn are the current ones.`}\n detail={run.detail}\n runs={a.runs}\n apps={c.apps}\n tz={au.tz}\n actions={c.actions}\n ownedByApp={c.ownedByApp}\n tabs={tabs}\n name={au.name}\n onBack={() => go(backFrom({ automation: id, run: runId }, c.origin.current))}\n onPickRun={(r) => go({ automation: id, run: r })}\n onRunAgain={async () => {\n const r = await a.test({ dry: run.run?.dry === true });\n if (r.ok) go({ automation: id, run: r.data.run_id });\n }}\n onControl={run.control}\n controlBusy={run.busy}\n onKeepFix={run.keepFix}\n onDropFix={run.dropFix}\n />\n </div>\n {slot(c.afterRun, { ...ctx, run: run.detail.run })}\n </>\n );\n }\n } else if (extra) {\n fill = false;\n body = (\n <div>\n <div className=\"px-4 pt-3\">{tabs}</div>\n {extra.render?.(ctx) ?? null}\n </div>\n );\n } else if (tab === RUNS) {\n fill = false;\n body = (\n <div>\n <div className=\"px-4 pt-3\">{tabs}</div>\n <RunsList runs={a.runs} tz={au.tz} onOpen={(r) => go({ automation: id, run: r })} />\n </div>\n );\n } else {\n body = (\n <div className={FILL}>\n <AutomationEditor\n key={id}\n automationId={id}\n status={au.status}\n liveVersion={au.live_version}\n draftVersion={au.draft_version}\n baseline={a.definition}\n versions={a.versions}\n runs={a.runs}\n estimate={a.estimate}\n tests={a.tests}\n apps={c.apps}\n actions={c.actions}\n runnable={c.runnable}\n ownedByApp={c.ownedByApp}\n triggers={list.triggers}\n selfKey={au.key}\n hook={a.hook}\n onRotateHook={a.rotateHook}\n source={au.source}\n canEdit={!legacy}\n canDelete={c.canDelete}\n compareTo={a.live ? { version: a.live.version, definition: a.live.definition } : null}\n tabs={tabs}\n onBack={toList}\n onSave={a.save}\n onKeep={a.keep}\n onLive={a.setLive}\n onTest={async (inputs) => {\n const r = await a.test({ inputs });\n if (r.ok) go({ automation: id, run: r.data.run_id });\n return r;\n }}\n onOpenRun={(r) => go({ automation: id, run: r })}\n suggestions={a.suggestions}\n onKeepSuggestion={a.keepSuggestion}\n onDropSuggestion={a.dropSuggestion}\n onFocus={c.onFocus}\n {...(c.remove\n ? {\n onDelete: async () => {\n const r = await c.remove(id, au.name);\n if (r.ok) toList();\n return r;\n },\n }\n : {})}\n />\n </div>\n );\n }\n\n const above = slot(c.aboveOne, { ...ctx, run: run.detail?.run ?? null });\n const label = au.name;\n const framed = (\n <div className={fill ? \"flex h-full min-h-0 flex-col\" : undefined}>\n {legacy && c.legacyWords ? (\n <div className=\"px-4 pt-3\">\n <Notice tone=\"info\">{c.legacyWords}</Notice>\n </div>\n ) : null}\n {above ? <div className=\"px-4 pt-3\">{above}</div> : null}\n {body}\n </div>\n );\n return c.render(crumb, label, fill, framed);\n}\n\n/* ---- a new one --------------------------------------------------------- */\n\nfunction NewPage({ c, start }) {\n const { go, list } = c;\n const taken = new Set(list.automations.map((x) => x.key));\n const picked = c.startList.find((s) => s.id === start);\n const crumb = [{ label: c.title, href: c.hrefOf?.(LIST), onPress: () => go({}) }, { label: \"New automation\" }];\n const body = (\n <div className=\"flex h-full min-h-0 flex-col\">\n <AutomationEditor\n key={start ?? \"blank\"}\n automationId={null}\n status=\"new\"\n baseline={picked ? picked.build(taken) : blankAutomation(\"\", keyFromName(\"New automation\", taken))}\n apps={c.apps}\n actions={c.actions}\n runnable={c.runnable}\n ownedByApp={c.ownedByApp}\n triggers={list.triggers}\n onFocus={c.onFocus}\n onBack={() => go({})}\n onSave={async (def) => {\n const r = await list.create({ ...def, key: keyFromName(def.name, taken) });\n if (r.ok) go({ automation: r.data.id });\n return r;\n }}\n />\n </div>\n );\n return c.render(crumb, \"New automation\", true, body);\n}\n\n/* ---- the screen -------------------------------------------------------- */\n\nexport function AutomationsScreen({\n hooks,\n place: placeProp,\n onPlace,\n query: queryProp,\n onQueryChange,\n title = \"Automations\",\n intro = \"What this app does on its own. Open one to see its steps, change them, test it and switch it on.\",\n starts,\n actions,\n apps,\n runnable,\n ownedByApp = true,\n allowance = null,\n onRequest,\n extras,\n extraTabs,\n aboveOne,\n afterRun,\n frame,\n hrefOf,\n onDescribe,\n describePrice,\n describeChips,\n onToggled,\n canToggle = true,\n canCreate = true,\n canDelete = true,\n onFocus,\n every,\n legacyWords = LEGACY_WORDS,\n unavailableWords = UNAVAILABLE_WORDS,\n now,\n}) {\n const problem = hooksProblem(hooks);\n if (problem) return <Notice tone=\"danger\">{problem}</Notice>;\n return (\n <Screen\n {...{ hooks, placeProp, onPlace, queryProp, onQueryChange, title, intro, starts, actions, apps, runnable, ownedByApp, allowance, onRequest, extras, extraTabs, aboveOne, afterRun, frame, hrefOf, onDescribe, describePrice, describeChips, onToggled, canToggle, canCreate, canDelete, onFocus, every, legacyWords, unavailableWords, now }}\n />\n );\n}\n\nfunction Screen(p) {\n const { hooks, placeProp, onPlace, queryProp, onQueryChange } = p;\n // The place: the app's (a URL, say) when it passes one, else held here.\n const controlled = placeProp !== undefined && placeProp !== null;\n const [held, setHeld] = useState(LIST);\n const place = normalizePlace(controlled ? placeProp : held);\n const at = useRef(place);\n at.current = place;\n // Where a run was opened from, so Back returns there.\n const origin = useRef(null);\n const go = useCallback(\n (next) => {\n const to = normalizePlace(next);\n origin.current = originAfter(at.current, to, origin.current);\n at.current = to;\n if (!controlled) setHeld(to);\n onPlace?.(to);\n },\n [controlled, onPlace],\n );\n // The words in the list's search box outlive the automation that was opened.\n const [typed, setTyped] = useState(\"\");\n const query = typeof queryProp === \"string\" ? queryProp : typed;\n const setQuery = useCallback(\n (t) => {\n setTyped(t);\n onQueryChange?.(t);\n },\n [onQueryChange],\n );\n\n const list = hooks.useAutomations(p.every ? { every: p.every } : undefined);\n const platform = useMemo(() => startsFromPlatform(list.starts), [list.starts]);\n const startList = useMemo(() => combineStarts(p.starts, platform, STARTING_POINTS), [p.starts, platform]);\n\n const { deleteAutomation, restoreAutomation } = hooks;\n const reload = list.reload;\n // A delete that asks first (the blocks do) and can be taken back for a while.\n const remove = useMemo(() => {\n if (typeof deleteAutomation !== \"function\") return null;\n return async (id, name = \"\") => {\n const r = await deleteAutomation(id);\n if (!r.ok) return { ok: false, error: r.error || \"That did not work.\", code: r.code };\n const done = r.data && typeof r.data === \"object\" ? r.data : {};\n reload?.();\n const said = deletedWords({ id, ...done }, name);\n toast(said.title, {\n ...(said.detail ? { description: said.detail } : {}),\n duration: UNDO_MS,\n ...(said.undo && typeof restoreAutomation === \"function\"\n ? {\n action: {\n label: \"Undo\",\n onClick: async () => {\n const back = await restoreAutomation(done.id ?? id);\n if (back.ok) reload?.();\n else toast.error(back.error || \"It could not be brought back.\");\n },\n },\n }\n : {}),\n });\n return { ok: true };\n };\n }, [deleteAutomation, restoreAutomation, reload]);\n\n const { setStatus } = list;\n const { onToggled } = p;\n const toggle = useCallback(\n async (row, on) => {\n const r = await setStatus(row.id, on);\n if (!r.ok) throw new Error(r.error || \"That did not work.\");\n await onToggled?.(row, on);\n },\n [setStatus, onToggled],\n );\n\n const { onDescribe } = p;\n const describe = onDescribe === \"platform\" ? (ask) => list.describe(ask) : typeof onDescribe === \"function\" ? onDescribe : null;\n\n const render = (crumb, label, fill, children) => (p.frame ?? defaultFrame)({ crumb, label, fill, children });\n const c = {\n ...p,\n list,\n startList,\n go,\n origin,\n query,\n setQuery,\n remove,\n toggle,\n describe,\n render,\n };\n\n if (place.automation === NEW) return <NewPage c={c} start={place.start} />;\n if (place.automation) return <OnePage key={place.automation} c={c} id={place.automation} runId={place.run} tab={place.tab} />;\n return <ListPage c={c} />;\n}\n"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"path": "registry/blocks/automations-screen/screen-model.js",
|
|
31
|
+
"type": "registry:component",
|
|
32
|
+
"target": "@components/makersclaw/blocks/automations-screen/screen-model.js",
|
|
33
|
+
"content": "// The pure part of AutomationsScreen: where the member is (the place), how\n// the place travels in a URL, where Back goes, what a delete says, which\n// starting points are offered, which tabs an automation has. No React, no\n// network: a test runs every function here.\n\n/** The `automation` value of a screen that makes a new one. */\nexport const NEW = \"new\";\n/** The two places inside one automation that every automation has. */\nexport const FLOW = \"flow\";\nexport const RUNS = \"runs\";\n\nconst text = (v) => (typeof v === \"string\" && v.trim() ? v.trim() : null);\nconst list = (v) => (Array.isArray(v) ? v : []);\n\n/**\n * A place, made well-formed. The list is `{ automation: null }`. One\n * automation is `{ automation: <id>, tab, run }`: `tab` is `flow` (default),\n * `runs`, or the key of an extra tab; `run` is a run's id. A new one is\n * `{ automation: \"new\", start }`, `start` being the starting point's id.\n */\nexport function normalizePlace(p) {\n const o = p && typeof p === \"object\" ? p : {};\n const automation = text(o.automation);\n if (!automation) return { automation: null, tab: FLOW, run: null, start: null };\n if (automation === NEW) return { automation: NEW, tab: FLOW, run: null, start: text(o.start) };\n return { automation, tab: text(o.tab) ?? FLOW, run: text(o.run), start: null };\n}\n\n/** Which screen a place is: list · new · one · run. */\nexport function whereOf(place) {\n const p = normalizePlace(place);\n if (!p.automation) return \"list\";\n if (p.automation === NEW) return \"new\";\n return p.run ? \"run\" : \"one\";\n}\n\nexport const samePlace = (a, b) => JSON.stringify(normalizePlace(a)) === JSON.stringify(normalizePlace(b));\n\n/** A place from the page's query string: `placeFromParams(useSearchParams())`,\n * a URLSearchParams, or a function name → value. The names are `automation`,\n * `tab`, `run`, `start`. */\nexport function placeFromParams(params) {\n const get = typeof params === \"function\" ? params : (name) => (params && typeof params.get === \"function\" ? params.get(name) : null);\n return normalizePlace({ automation: get(\"automation\"), tab: get(\"tab\"), run: get(\"run\"), start: get(\"start\") });\n}\n\n/** The query parameters of a place, only what is set: the list is `{}`. Spread\n * them into your own (`{ view: \"automations\", ...paramsOfPlace(place) }`). */\nexport function paramsOfPlace(place) {\n const p = normalizePlace(place);\n if (!p.automation) return {};\n return {\n automation: p.automation,\n ...(p.tab !== FLOW && !p.run ? { tab: p.tab } : {}),\n ...(p.run ? { run: p.run } : {}),\n ...(p.start ? { start: p.start } : {}),\n };\n}\n\n/**\n * Where the member came from when a run opened: remembered so that Back from\n * a run returns to the tab it was opened from (the Runs list), not always to\n * the flow. `from` and `to` are the places before and after a move; `origin`\n * is what was remembered. Choosing another run from inside a run keeps it.\n */\nexport function originAfter(from, to, origin = null) {\n const a = normalizePlace(from);\n const b = normalizePlace(to);\n if (!b.run) return null;\n if (a.run) return origin;\n return a.automation === b.automation ? a : null;\n}\n\n/** The place Back from a run goes to: the tab the run was opened from, else the flow. */\nexport function backFrom(place, origin = null) {\n const p = normalizePlace(place);\n const o = origin ? normalizePlace(origin) : null;\n if (o && o.automation === p.automation && !o.run && o.tab !== FLOW) return { automation: p.automation, tab: o.tab, run: null, start: null };\n return { automation: p.automation, tab: FLOW, run: null, start: null };\n}\n\n/**\n * The words after a delete: a title, what followed it (the automations that\n * started when it finished and now never will), and whether Undo is offered.\n * `done` is the platform's answer `{ id, name, followers, undo }`.\n */\nexport function deletedWords(done, fallbackName = \"\") {\n const name = text(done?.name) ?? text(fallbackName);\n const followers = list(done?.followers).filter((f) => f && text(f.name));\n return {\n title: name ? `Deleted “${name}”.` : \"Deleted.\",\n detail: followers.length\n ? `${followers.length} ${followers.length === 1 ? \"automation\" : \"automations\"} followed it and will no longer start: ${followers.map((f) => f.name).join(\", \")}.`\n : \"\",\n undo: Boolean(done?.undo) && Boolean(text(done?.id)),\n };\n}\n\n/** The starting points offered under New: the app's own first, then the platform's\n * (already turned into the list's shape), else the built-in ones. One of each id. */\nexport function combineStarts(app, platform, builtIn) {\n const seen = new Set();\n const out = [];\n for (const s of [...list(app), ...(list(platform).length ? list(platform) : list(builtIn))]) {\n if (!s || typeof s.id !== \"string\" || seen.has(s.id)) continue;\n seen.add(s.id);\n out.push(s);\n }\n return out;\n}\n\n/** The tabs of one automation: Flow, Runs, then the app's own that apply to it. */\nexport function tabsOf(extras, automation) {\n const more = [];\n for (const t of list(extras)) {\n if (!t || typeof t.key !== \"string\" || t.key === FLOW || t.key === RUNS || typeof t.label !== \"string\") continue;\n if (more.some((x) => x.key === t.key)) continue;\n if (typeof t.when === \"function\" && !t.when(automation)) continue;\n more.push(t);\n }\n return [{ key: FLOW, label: \"Flow\" }, { key: RUNS, label: \"Runs\" }, ...more.map((t) => ({ key: t.key, label: t.label }))];\n}\n\n/** What `hooks` must hold. A string naming what is missing, else null. */\nexport function hooksProblem(hooks) {\n if (!hooks || typeof hooks !== \"object\") return \"AutomationsScreen needs hooks={{ useAutomations, useAutomation, useAutomationRun }} (from lib/hooks).\";\n const missing = [\"useAutomations\", \"useAutomation\", \"useAutomationRun\"].filter((k) => typeof hooks[k] !== \"function\");\n return missing.length ? `AutomationsScreen: hooks is missing ${missing.join(\", \")}.` : null;\n}\n"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"path": "registry/blocks/automations-screen/README.md",
|
|
37
|
+
"type": "registry:file",
|
|
38
|
+
"target": "@components/makersclaw/blocks/automations-screen/README.md",
|
|
39
|
+
"content": "# AutomationsScreen\n\nThe whole Automations page of an app, as ONE block. It answers \"how do I give\nmy app an Automations page?\" with one component and a handful of props, so a\nfix to the page reaches every app through `update` and no app carries a copy.\n\nThe places a member goes:\n\n- **The list** — each automation as a row that opens whole, an on/off switch,\n **New automation** (the starting points, **Describe it**), the week's\n **allowance shown once** above the rows, and two slots under them: jobs that\n run **on request** and the app's own **extras**.\n- **New** — a new automation in the editor, from a starting point or blank. It\n is not saved until **Create**.\n- **One automation** — its flow on a canvas, edited, tested, kept and switched\n on; its **Runs**; and any tabs the app adds.\n- **One run** — step by step, with the run's controls (pause, retry, skip…)\n and the agent's fixes.\n\nIt composes `automation-list`, `automation-editor`, `run-view` and `cost`, and\ndecides only what they leave open:\n\n- **Which place is showing, and how it travels in a page address.** The place\n is `{ automation, tab, run, start }`; `placeFromParams` and `paramsOfPlace`\n turn it into and out of the query string. Hold it yourself (`place` +\n `onPlace`) or leave it to the block.\n- **Back to where the member WAS.** Back from an automation is the list with\n the words that were in its search box. Back from a run is the Runs tab it was\n opened from (or the flow, when it was opened from a test).\n- **What each editor is handed**: the platform's `triggers`, the automation's\n `selfKey`, its webhook (`hook`, `onRotateHook`), its `tests`, `compareTo`\n (what \"What changed\" compares an agent's draft with), the app's declared\n `actions`, and the save with `baseVersion` (a save names the version the copy\n began on, so an agent's newer draft is never buried).\n- **A delete that asks first and can be undone.** The row's menu and the\n editor's menu ask (`Deletes \"<name>\". Its runs stay in history.`); then a\n toast says what was deleted, which automations followed it and will no longer\n start, and offers **Undo** for a few seconds.\n- **Describe it** — to the platform (`onDescribe=\"platform\"`), or handed on to\n the agent beside the page (`onDescribe={(ask) => …}`).\n- **The app's older schedule.** A row from before the engine (`source:\n \"legacy\"`) opens read-only, with a line saying why.\n\n```jsx\nimport { AutomationsScreen, placeFromParams, paramsOfPlace } from \"../components/blocks/automations-screen\"; // a Makersclaw app\nimport { AutomationsScreen, placeFromParams, paramsOfPlace } from \"@/components/makersclaw/blocks/automations-screen\"; // a shadcn project\n```\n\n`makersclaw add automations-screen` lays `automation-list`, `automation-editor`,\n`run-view`, `cost` and `automation-model` (and what they need) with it. A\nclient component. It fills its parent's height (give it a parent with one, or a\n`frame`), and each block inside decides its own layout from its own width.\n\n## Props\n\n### Required\n\n- `hooks` — the engine's hooks and doors, from `lib/hooks`: `{ useAutomations,\n useAutomation, useAutomationRun, deleteAutomation, restoreAutomation }`. A\n block may not import `lib/`, so they come in here. The first three are\n required (without them the block draws a plain notice naming what is\n missing); the two doors are optional (without `deleteAutomation` there is no\n Delete; without `restoreAutomation` no Undo). Pass a module-level object so\n it is the same on every render.\n\n### Where it is\n\n- `place`, `onPlace(place)` — the place, controlled. Leave both out and the\n block holds it. `place` is `{ automation: null | \"new\" | <id>, tab: \"flow\" |\n \"runs\" | <an extra tab's key>, run: <run id> | null, start: <starting point\n id> | null }`; anything missing is the list. Always called back with a\n complete place.\n- `placeFromParams(searchParams)` → a place. Takes a `URLSearchParams`, a\n `useSearchParams()` result or a function name → value; reads `automation`,\n `tab`, `run`, `start`.\n- `paramsOfPlace(place)` → the query parameters of a place, only what is set\n (the list is `{}`); spread them into your own: `{ view: \"automations\",\n ...paramsOfPlace(place) }`.\n- `query`, `onQueryChange(text)` — the list's search box. Held by the block\n unless you pass them (to keep it across a reload, say).\n\n### The list\n\n- `title` (\"Automations\"), `intro` — the list's heading and its line.\n- `allowance` — `{ used, of, words? }`: the week's credits, drawn once above\n the list as a meter (`cost`'s `Allowance`). From `useJobs().budget`\n (`week_used_credits`, `week_cap_credits`). `null` (default): none. `words` is\n your own line (\"4,180 of 10,000 credits this week\").\n- `onRequest`, `extras` — nodes, or functions `({ automations }) => node`, drawn\n under the rows once they have loaded (`onRequest` first). `onRequest` is for\n jobs a press or an ask starts and no schedule runs; `extras` is anything else\n of the app's.\n- `starts` — the app's own starting points: `[{ id, name, blurb, glyph, needs,\n build(takenKeys) → a whole definition }]`, offered first under New. The\n platform's come after them (or, where it has none, the template's five).\n- `onDescribe` — `\"platform\"` (the platform drafts it, one model call, and the\n new automation opens), a function `(ask) => Promise<{ ok: true } | { ok:\n false, error }>` (the sentence is handed on, to an agent beside the page say;\n the box empties), or nothing (no Describe it). `describePrice` (the words on\n the Draft it button; `\"\"` for none), `describeChips`.\n- `onToggled(row, on)` — async, after a switch was saved. Keep a setting of the\n app's own in step with it; a throw is shown above the rows.\n- `canToggle`, `canCreate`, `canDelete` — all true by default.\n- `now` — the clock, for a test.\n\n### One automation\n\n- `actions` — the app's declared actions, for the action step: `[{ name,\n description }]` (`manifest.actions` without those marked `agent: false`).\n- `apps`, `runnable`, `ownedByApp` (default true) — as the editor's.\n- `extraTabs` — tabs the app adds beside Flow and Runs: `[{ key, label,\n when?(automation) → bool, render({ automation, definition, tab, runId, go })\n → node }]`. `when` decides which automations get the tab (omit: all). `flow`\n and `runs` are not yours.\n- `aboveOne` — a node, or `({ automation, definition, tab, runId, run, go }) =>\n node`: drawn above an automation (a connection it needs, a notice).\n- `afterRun` — the same, under a run (the app's own account of it). `run` is the\n run.\n- `legacyWords` — the line over a read-only older schedule (`false`: none).\n- Run again, in a run: after a test it is a test; after a real run it is a real\n run (it spends, and it acts). The run-view block says which on the button.\n- `onFocus({ selected, unsaved, baseVersion })` — the editor's, for an agent\n pane beside the page that must know what the member is looking at.\n- `every` — how often the hooks look again, in ms (default theirs: 30 s, 5 s\n while one runs). Lower it while an agent beside the page works.\n\n### The frame\n\n- `frame({ crumb, label, fill, children })` — a plain function (no hooks in it)\n that wraps each place in the app's chrome and returns it. `crumb` is\n `[{ label, href?, onPress? }]`: the ancestors (with `onPress`, and `href` when\n you give `hrefOf`) then the page you are on. `label` names the content region;\n `fill` is true where the body owns its own scrolling (the editor, a run).\n **The first crumb is always the list itself** (the block's `title`; on the\n list it is the only crumb), and its `onPress` goes back to the list with\n the member's search kept: use `onPress` when it is there, `href` only when\n you gave `hrefOf`. An app that draws its own root crumb (\"Morning checks\"\n above the page) drops that first one: `crumb.slice(1)`. Default: the kit's `AppLayout` filling its parent. An app draws its top bar\n here and returns `<AppLayout …>`:\n\n ```jsx\n frame={({ crumb, label, fill, children }) => (\n <>\n <TopBar icon=\"zap\" crumb={crumb.map((c) => ({ label: c.label, href: c.href }))} />\n <AppLayout contentLabel={label} contentMode={fill ? \"fill\" : undefined}>{children}</AppLayout>\n </>\n )}\n ```\n- `hrefOf(place)` → a string. When given, ancestor crumbs carry an `href`.\n- `unavailableWords` — the line where this Makersclaw has no engine yet.\n\n## Example\n\nThe whole page of an app whose address carries the place:\n\n```jsx\n\"use client\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { useAutomation, useAutomationRun, useAutomations, deleteAutomation, restoreAutomation, useJobs } from \"../lib/hooks\";\nimport { AutomationsScreen, paramsOfPlace, placeFromParams } from \"../components/blocks/automations-screen\";\nimport manifest from \"../makersclaw.json\";\n\nconst HOOKS = { useAutomations, useAutomation, useAutomationRun, deleteAutomation, restoreAutomation };\nconst ACTIONS = (manifest.actions ?? []).filter((a) => a.agent !== false).map((a) => ({ name: a.name, description: a.description }));\n\nexport default function Automations() {\n const router = useRouter();\n const sp = useSearchParams();\n const { budget } = useJobs();\n return (\n <AutomationsScreen\n hooks={HOOKS}\n place={placeFromParams(sp)}\n onPlace={(place) => router.push(`/s/today?${new URLSearchParams({ view: \"automations\", ...paramsOfPlace(place) })}`, { scroll: false })}\n actions={ACTIONS}\n allowance={budget ? { used: budget.week_used_credits, of: budget.week_cap_credits } : null}\n onDescribe=\"platform\"\n onRequest={<p className=\"text-sm text-muted-foreground\">The first read starts when a mailbox connects.</p>}\n />\n );\n}\n```\n\n## States\n\nDrawn in the demo (a state picker and a width picker for 1280, 800, 368 and\n320 px): many automations (one with a very long name), one, none (the first-run\nscreen with starting points and Describe it), loading (a skeleton, never the\nfirst-run screen), the list failing to load (a notice with Try again), a\nplatform without the engine, an app's older read-only schedule, an automation\nthat will not open or is gone (a line and a way back), a run that will not open,\na test run, a run opened from the Runs tab, an app tab, a delete with its\nUndo, and a delete the platform refuses (an app's own job).\n\n## Who may do what\n\nThe platform decides; a hidden button is never the guard. Any member who opens\nthe app edits and runs its automations. A delete is the owner's or an admin's;\na refusal comes back in the platform's own words and is shown as it is.\n\n## Never\n\n- Never import `lib/` here: the hooks are the `hooks` prop.\n- Never a run that starts on its own: a run starts only on Test run or Run again.\n- Never a second Allowance: it is drawn once, on the list.\n- Never the raw words of a failure: a list that cannot load says so in one plain\n sentence; the blocks inside show the platform's plain words.\n"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"categories": [
|
|
43
|
+
"automation",
|
|
44
|
+
"automations",
|
|
45
|
+
"page",
|
|
46
|
+
"screen",
|
|
47
|
+
"workflow",
|
|
48
|
+
"builder",
|
|
49
|
+
"jobs",
|
|
50
|
+
"schedule",
|
|
51
|
+
"runs",
|
|
52
|
+
"run",
|
|
53
|
+
"editor",
|
|
54
|
+
"list",
|
|
55
|
+
"describe it",
|
|
56
|
+
"starting points",
|
|
57
|
+
"allowance",
|
|
58
|
+
"delete",
|
|
59
|
+
"undo",
|
|
60
|
+
"n8n",
|
|
61
|
+
"make",
|
|
62
|
+
"zapier"
|
|
63
|
+
],
|
|
64
|
+
"docs": "Installed to components/makersclaw/blocks/automations-screen. Import: import { AutomationsScreen, deletedWords, normalizePlace, paramsOfPlace, placeFromParams } from \"@/components/makersclaw/blocks/automations-screen\". Props and an example: components/makersclaw/blocks/automations-screen/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
65
|
+
"meta": {
|
|
66
|
+
"makersclaw": {
|
|
67
|
+
"version": "0.8.2",
|
|
68
|
+
"useWhen": "An app has jobs that run on the automations engine and needs its Automations page: the list, the editor, the runs and a run, in the app's theme. One component, one source, so a fix to this page reaches every app through update. The app adds only what is its own: the allowance, jobs a press or an ask starts, extra tabs on an automation, a notice above one, its own starting points and its declared actions."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/r/board.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Board",
|
|
6
6
|
"description": "Records as cards in columns, one per stage; drag a card to another column (mouse, touch, or the card's menu from the keyboard) and the stage is saved. Optional column icons, a per-column total (\"13pt\"), a category colour on each card's edge, and a stacked layout of folding sections with compact rows.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"docs": "Installed to components/makersclaw/blocks/board. Import: import { Board } from \"@/components/makersclaw/blocks/board\". Props and an example: components/makersclaw/blocks/board/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
49
49
|
"meta": {
|
|
50
50
|
"makersclaw": {
|
|
51
|
-
"version": "0.8.
|
|
51
|
+
"version": "0.8.2",
|
|
52
52
|
"useWhen": "A process where each record sits in one stage and moves forward: a pipeline, a hiring funnel, a sprint, a content calendar by status, outreach by reply state. Not for records with no stages (a table) or items placed on a timeline."
|
|
53
53
|
}
|
|
54
54
|
}
|
package/r/calendar-view.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "Calendar",
|
|
6
6
|
"description": "Tasks on a calendar, like Google Calendar, Cron or Notion Calendar: month, week, day and agenda views over task-model's work items. Month shows all-day tasks and spans as tinted bars across their days (stacked in lanes, \"2 more\" past three) and timed ones as a dot and a time; week and day show an all-day strip over an hour grid where timed tasks are blocks placed by their times, overlapping ones side by side, with a line at now. Drag an item to another day or hour (it snaps to the quarter hour and the day columns as it moves), drag its bottom or end to change how long it runs, click an empty day or hour (or drag across hours) to type a new task in place, drag tasks without a date in from an unscheduled tray (or drop one there to take its date off). Arrow keys move a focused block, Shift+arrows resize it, Delete removes with Undo; T, J/K, M/W/D/A and C for today, next/previous, the views and a new task. Search and filters from the shared ViewToolbar, a colour per project or status, a real phone layout (a dotted month with the tapped day listed, a three-day week, a sheet for the tray). An available-time layer (closed hours muted, open slots as boxes you pick, from `availability`, `slots` or an async `getSlots`, with pure `freeSlots` / `availabilityOn` helpers) is what a booking page builds on. One async onChange saves every change; failures roll back with a notice.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/task-model.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"docs": "Installed to components/makersclaw/blocks/calendar-view. Import: import { CalendarView, CALENDAR_MODES, MODE_LABELS, DAY_MINUTES, UNSCHEDULE, snapMinutes, durationOf, eventOf, eventsOf, unscheduledOf, layoutTimed, layoutLanes, moveEventPatch, resizeEventPatch, placeEventPatch, timedPatch, dayPatch, allDayPatch, extendPatch, rangeFor, stepAnchor, rangeLabel, titleFor, hourLabel, minutesLabel, eventTimeLabel, availabilityOn, closedOn, freeSlots, normalizeSlots } from \"@/components/makersclaw/blocks/calendar-view\". Props and an example: components/makersclaw/blocks/calendar-view/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
99
99
|
"meta": {
|
|
100
100
|
"makersclaw": {
|
|
101
|
-
"version": "0.8.
|
|
101
|
+
"version": "0.8.2",
|
|
102
102
|
"useWhen": "A screen where dated work or events are seen by day and moved around: a team's deadlines, a content calendar, a launch plan, shifts, a personal schedule, meetings on tasks, or the slot picker of a booking page (modes week/day, slots from getSlots). Not for dependencies over a long timeline (gantt), for undated work (task-list, board), or for a single date field (the kit's DatePicker)."
|
|
103
103
|
}
|
|
104
104
|
}
|