makersclaw 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- 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 +4 -4
- 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 +5 -5
- package/r/chat-pane.json +84 -0
- package/r/chat.json +3 -3
- 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 +18 -6
- 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 +3 -3
- package/r/flow-editor.json +79 -0
- package/r/footer.json +3 -3
- 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 +4 -4
- 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 +4 -4
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +3 -3
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- 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 +4089 -1510
- 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 +3 -3
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +3 -3
- 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 +4 -4
- package/r/workflow-view.json +55 -0
- package/registry/blocks/activity-log/README.md +112 -0
- package/registry/blocks/activity-log/activity-log.jsx +351 -0
- package/registry/blocks/activity-log/block.json +33 -0
- package/registry/blocks/activity-log/demo.jsx +48 -0
- package/registry/blocks/activity-log/index.js +4 -0
- package/registry/blocks/agent-changes/README.md +94 -0
- package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
- package/registry/blocks/agent-changes/block.json +28 -0
- package/registry/blocks/agent-changes/demo.jsx +39 -0
- package/registry/blocks/agent-changes/index.js +3 -0
- package/registry/blocks/agent-chat/README.md +145 -0
- package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
- package/registry/blocks/agent-chat/block.json +30 -0
- package/registry/blocks/agent-chat/demo.jsx +165 -0
- package/registry/blocks/agent-chat/index.js +3 -0
- package/registry/blocks/agent-column/README.md +197 -0
- package/registry/blocks/agent-column/agent-column.jsx +492 -0
- package/registry/blocks/agent-column/block.json +67 -0
- package/registry/blocks/agent-column/column-picker.jsx +163 -0
- package/registry/blocks/agent-column/demo.jsx +50 -0
- package/registry/blocks/agent-column/index.js +7 -0
- package/registry/blocks/agent-column/model.js +164 -0
- package/registry/blocks/agent-column/receipt.jsx +483 -0
- package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
- package/registry/blocks/agent-dock/README.md +210 -0
- package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
- package/registry/blocks/agent-dock/block.json +47 -0
- package/registry/blocks/agent-dock/demo.jsx +153 -0
- package/registry/blocks/agent-dock/dock-model.js +118 -0
- package/registry/blocks/agent-dock/index.js +5 -0
- package/registry/blocks/agent-kit/README.md +73 -0
- package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
- package/registry/blocks/agent-kit/block.json +10 -0
- package/registry/blocks/agent-kit/demo.jsx +35 -0
- package/registry/blocks/agent-kit/index.js +3 -0
- package/registry/blocks/agent-marks/README.md +80 -0
- package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
- package/registry/blocks/agent-marks/block.json +27 -0
- package/registry/blocks/agent-marks/demo.jsx +49 -0
- package/registry/blocks/agent-marks/index.js +3 -0
- package/registry/blocks/agent-pane/README.md +225 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
- package/registry/blocks/agent-pane/block.json +41 -0
- package/registry/blocks/agent-pane/composer.jsx +6 -0
- package/registry/blocks/agent-pane/demo.jsx +233 -0
- package/registry/blocks/agent-pane/dock-context.js +10 -0
- package/registry/blocks/agent-pane/index.js +4 -0
- package/registry/blocks/agent-pane/prose.jsx +84 -0
- package/registry/blocks/agent-pane/questions.jsx +134 -0
- package/registry/blocks/agent-pane/transcript.jsx +268 -0
- package/registry/blocks/agent-pane/work.jsx +368 -0
- package/registry/blocks/ai-action/README.md +94 -0
- package/registry/blocks/ai-action/ai-action.jsx +188 -0
- package/registry/blocks/ai-action/block.json +34 -0
- package/registry/blocks/ai-action/demo.jsx +93 -0
- package/registry/blocks/ai-action/index.js +3 -0
- package/registry/blocks/annotate/README.md +14 -11
- 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/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +4 -1
- package/registry/blocks/chat/demo.jsx +2 -2
- 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-canvas/README.md +15 -8
- 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/footer/README.md +3 -2
- 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/mind-map/README.md +4 -2
- 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/org-chart/README.md +4 -2
- 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/page-builder/README.md +1 -1
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/pdf-viewer/README.md +4 -1
- 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/sidebar/README.md +23 -13
- package/registry/blocks/sketch-canvas/README.md +15 -9
- 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/whiteboard/README.md +20 -14
- 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/cli.mjs +2 -2
- package/src/install.mjs +100 -22
- package/src/registry.mjs +37 -3
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +909 -26
- package/templates/next/files/app/shell.jsx +54 -37
- 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 +1043 -12
- 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 +3 -1
- package/templates/next/files/components/ui/layout.jsx +382 -0
- 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 +244 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1694 -6
- package/templates/next/files/lib/platform.js +611 -14
- package/templates/next/template.json +22 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "agent-column",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "AgentColumn",
|
|
6
|
+
"description": "A column an agent fills (or suggests) for every record, cell by cell: each answer shows its state in words (queued, waits for another column, looking up, found, not found, two answers, can't fill, failed, out of date, checked or typed by a person) and how sure it is as a mark that differs in shape (filled sure, half likely, dashed guess, a tick for a person). Its detail shows the source, the words on the page with the answer marked in them, how it was found, both answers when sources disagree, what was tried when nothing was found, and what a person can do: accept, pick one of two, type their own, ask again with a hint, keep not found, or keep and drop a suggestion. With a column header that carries live progress, a legend, and a picker that chooses columns with their cost per record. Works in data-table cells, a spreadsheet column type, or a list of its own. Grown in Data Enrichment 0.5.0: the receipt (an answer opened as a claim with its citation: each source a clipping with the deciding words underlined, two clippings to pick from, what was tried crossed off), the one-line answer and trust mark a dense grid draws, and a column heading drawn as a job (its progress, its cost per row, its menu).",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/agent-marks.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/agent-column/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/agent-column/index.js",
|
|
17
|
+
"content": "// The AgentColumn block — `makersclaw add agent-column`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { AnswerCell, AnswerDetail, ColumnHeader, SureMark, SureLegend, Source } from \"./agent-column\";\nexport { AgentMark, AnswerLine, Clipping, JobHeader, Receipt, ReceiptActions, TriedList, TrustMark } from \"./receipt\";\nexport { ColumnPicker } from \"./column-picker\";\nexport { answerState, answerWords, needsCheck, summarizeAnswers, progressWords, markWords, decidingWords, plainWords, shortUrl, CONFIDENCE } from \"./model\";\nexport { answerColumnType } from \"./spreadsheet-type\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/agent-column/agent-column.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/agent-column/agent-column.jsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { Button, Icon, Input, cx } from \"../../ui\";\nimport { CONFIDENCE, answerState, answerWords, markWords, needsCheck, progressWords, shortUrl } from \"./model\";\n\n// AgentColumn — the pieces of a column an agent fills (or suggests) for\n// every record: the answer as a cell, how sure it is as a mark, the\n// column's progress under its name, and the answer's detail with what a\n// person can do about it. Props in, callbacks out; the block never\n// fetches. The answer's shape is in model.js.\n\n/* ------------------------------ the mark ------------------------------ */\n\n// How sure, drawn so it differs in shape (never colour alone): filled =\n// sure, half = likely, dashed ring = guess, a tick = a person accepted\n// or typed it.\nexport function SureMark({ confidence, checked = false, typed = false, className }) {\n const label = typed ? \"typed by you\" : checked ? \"checked by you\" : CONFIDENCE[confidence]?.label;\n if (!label) return null;\n if (checked || typed)\n return (\n <span role=\"img\" aria-label={label} className={cx(\"inline-grid size-3 shrink-0 place-items-center text-primary\", className)}>\n <Icon name=\"check\" size={12} />\n </span>\n );\n return (\n <span role=\"img\" aria-label={label} className={cx(\"relative inline-block size-2.5 shrink-0 rounded-full\", className, MARK[confidence])}>\n {confidence === \"likely\" ? <span aria-hidden=\"true\" className=\"absolute inset-y-0 left-0 w-1/2 bg-foreground\" /> : null}\n </span>\n );\n}\n\nconst MARK = {\n sure: \"bg-foreground\",\n likely: \"overflow-hidden border border-foreground\",\n guess: \"border border-dashed border-foreground\",\n};\n\n/** The key to the marks: \"● sure ◐ likely ◌ guess\". */\nexport function SureLegend({ className }) {\n return (\n <span aria-label=\"How sure\" className={cx(\"flex flex-wrap items-center gap-x-3.5 gap-y-1 text-xs text-muted-foreground\", className)}>\n {[\"sure\", \"likely\", \"guess\"].map((c) => (\n <span key={c} className=\"inline-flex items-center gap-1.5\">\n <SureMark confidence={c} />\n <span className={c === \"guess\" ? \"italic\" : undefined}>{c}</span>\n </span>\n ))}\n </span>\n );\n}\n\n/* ------------------------------ the cell ------------------------------ */\n\nconst MONO = new Set([\"link\", \"email\", \"number\", \"domain\"]);\n\n/**\n * One answer, as a cell: the value and its detail, or the state in words.\n *\n * answer the answer (model.js), or null for \"not asked yet\"\n * type how the value reads: text · link · email · number · domain (mono)\n * onOpen makes the cell a button that opens its detail\n * onRetry a failed answer's \"Try again\" in the cell\n * lines the value's lines before it clips (default 2)\n */\nexport function AnswerCell({ answer, type = \"text\", onOpen, onRetry, lines = 2, className, label }) {\n const st = answerState(answer);\n const mono = MONO.has(type);\n const body = (() => {\n switch (st) {\n case \"empty\":\n return <Quiet>not asked yet</Quiet>;\n case \"queued\":\n return <Quiet>{answer?.value ? <Old answer={answer} /> : \"Queued\"}</Quiet>;\n case \"waiting\":\n return <Quiet>{answerWords(answer)}</Quiet>;\n case \"blocked\":\n return (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-muted-foreground\">Can’t fill</div>\n <div className=\"text-xs text-muted-foreground\">needs {answer.blockedBy.join(\" and \")}</div>\n </div>\n );\n case \"working\":\n return (\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2 font-medium text-primary\">\n <span aria-hidden=\"true\" className=\"size-1.5 animate-pulse rounded-full bg-primary motion-reduce:animate-none\" />\n {answer.value ? \"Writing\" : \"Looking up\"}\n </div>\n {answer.looking ? <div className=\"truncate text-xs text-muted-foreground\">{answer.looking}</div> : null}\n </div>\n );\n case \"two_answers\":\n return (\n <div className=\"min-w-0\">\n <div className={cx(\"flex flex-wrap items-baseline gap-x-1.5\", mono && \"font-mono\")}>\n <Icon name=\"git-branch\" size={13} className=\"self-center text-warning\" />\n <span className=\"font-medium\">{answer.value}</span>\n <span className=\"font-sans text-xs text-muted-foreground\">or</span>\n <span className=\"font-medium\">{answer.other?.value}</span>\n </div>\n <div className=\"text-xs font-medium text-warning\">two answers</div>\n </div>\n );\n case \"not_found\":\n return (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-warning\">Not found</div>\n {answer.detail ? <div className=\"truncate text-xs text-muted-foreground\">{answer.detail}</div> : null}\n </div>\n );\n case \"failed\":\n return (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-destructive\">Failed</div>\n {answer.detail || answer.note ? <div className=\"truncate text-xs text-muted-foreground\">{answer.detail || answer.note}</div> : null}\n </div>\n );\n case \"stale\":\n return (\n <div className=\"min-w-0\">\n <div className=\"text-xs italic text-muted-foreground\">out of date</div>\n <Clamp lines={lines} className=\"text-muted-foreground\">{answer.value}</Clamp>\n </div>\n );\n default:\n return <Found answer={answer} mono={mono} lines={lines} />;\n }\n })();\n const tint = st === \"two_answers\" || (needsCheck(answer) && answer?.flag) ? \"bg-warning/10\" : undefined;\n const inner = <div className={cx(\"min-w-0 text-sm\", tint && \"-mx-2 -my-1 rounded-sm px-2 py-1\", tint, className)}>{body}</div>;\n const retry = st === \"failed\" && onRetry ? (\n <Button size=\"sm\" variant=\"link\" onClick={onRetry} className=\"mt-1 text-xs\">\n Try again\n </Button>\n ) : null;\n if (!onOpen || st === \"empty\")\n return (\n <div>\n {inner}\n {retry}\n </div>\n );\n return (\n <div>\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={label}\n className=\"block w-full min-w-0 rounded-sm text-left outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {inner}\n </button>\n {retry}\n </div>\n );\n}\n\nfunction Quiet({ children }) {\n return <div className=\"text-xs text-muted-foreground\">{children}</div>;\n}\n\nfunction Old({ answer }) {\n return (\n <span>\n Queued · <span className=\"italic\">{answer.value}</span>\n </span>\n );\n}\n\nfunction Clamp({ lines, className, children }) {\n return (\n <div className={cx(\"min-w-0 overflow-hidden\", className)} style={{ display: \"-webkit-box\", WebkitLineClamp: lines, WebkitBoxOrient: \"vertical\" }}>\n {children}\n </div>\n );\n}\n\nfunction Found({ answer, mono, lines }) {\n const st = answerState(answer);\n const guess = answer.confidence === \"guess\" && st === \"found\";\n const flagLine = answer.flag === \"may_not_fit\" && !answer.checked ? \"may not fit\" : null;\n return (\n <div className=\"flex min-w-0 gap-2\">\n <SureMark confidence={answer.confidence} checked={st === \"checked\"} typed={st === \"typed\"} className=\"mt-1.5\" />\n <div className=\"min-w-0\">\n <Clamp lines={answer.detail ? 1 : lines} className={cx(mono ? \"font-mono text-xs leading-5\" : \"\", guess && \"italic\", answer.flag === \"left\" && \"font-medium\")}>\n {answer.value}\n </Clamp>\n {answer.detail ? <div className=\"truncate text-xs text-muted-foreground\">{answer.detail}</div> : null}\n {flagLine ? <div className=\"text-xs font-medium text-warning\">{flagLine}</div> : null}\n </div>\n </div>\n );\n}\n\n/* ------------------------------ the header ------------------------------ */\n\n/**\n * A column's name and its progress: \"Headcount / 20 of 22 · 3 to check\".\n * `progress` is summarizeAnswers(); `agent` false draws a person's own\n * column (its name and \"yours\").\n */\nexport function ColumnHeader({ name, progress, agent = true, note, className }) {\n const words = progress ? progressWords(progress) : null;\n const [head, ...rest] = words ? words.split(\" · \") : [];\n return (\n <div className={cx(\"min-w-0\", className)}>\n <div className=\"truncate text-sm font-semibold text-foreground\">{name}</div>\n <div className=\"truncate font-mono text-xs font-normal text-muted-foreground\">\n {agent ? (\n <>\n {head}\n {rest.map((p) => (\n <span key={p} className={p.endsWith(\"to check\") ? \"text-warning\" : undefined}>\n {\" · \"}\n {p}\n </span>\n ))}\n </>\n ) : (\n (note ?? \"yours\")\n )}\n </div>\n </div>\n );\n}\n\n/* ------------------------------ the detail ------------------------------ */\n\n/**\n * One answer in full, and what a person can do about it.\n *\n * answer the answer (model.js)\n * type how the value reads: text · link · email · number · domain (mono)\n * title the record (\"Klarwerk\"); subtitle, a line under it\n * question the column's name (\"Headcount\")\n * needs [{ name, answer }] the answers this one read (shown as \"It needed\")\n * history [{ at, words, credits }] earlier asks, oldest first\n * askCost credits an ask again costs (\"40\")\n * placeholder for \"type your own\"; hintPlaceholder for the hint\n * onAccept() accept a found answer or a guess\n * onPick(\"one\" | \"other\") choose one of two answers\n * onType(value) a person's own value\n * onAskAgain(hint)\n * onKeepNotFound()\n * onKeep() / onDrop() a suggestion's keep and drop\n * busy a call is running (disables the actions)\n * children more of your own under the actions (a row's choices)\n */\nexport function AnswerDetail({\n answer,\n type = \"text\",\n title,\n subtitle,\n question,\n needs = [],\n history = [],\n askCost,\n placeholder = \"Your answer\",\n hintPlaceholder = \"a hint, if you have one\",\n onAccept,\n onPick,\n onType,\n onAskAgain,\n onKeepNotFound,\n onKeep,\n onDrop,\n busy = false,\n note,\n children,\n}) {\n const st = answerState(answer);\n const [own, setOwn] = useState(\"\");\n const [hint, setHint] = useState(answer?.hint ?? \"\");\n const ownId = useId();\n const hintId = useId();\n const typeWord = st === \"not_found\" ? \"Type who it is\" : st === \"two_answers\" ? \"Or type your own\" : \"Or type the one you know\";\n return (\n <div className=\"grid gap-5\">\n <header className=\"grid gap-1\">\n {title ? <h3 className=\"font-display text-3xl leading-tight\">{title}</h3> : null}\n {subtitle ? <p className=\"text-sm text-muted-foreground\">{subtitle}</p> : null}\n </header>\n\n {st === \"two_answers\" ? (\n <section className=\"grid gap-3\">\n <p className=\"text-sm font-medium text-warning\">Two answers: the sources disagree</p>\n <Choice value={answer.value} detail={answer.detail} source={answer.source} onUse={onPick ? () => onPick(\"one\") : null} busy={busy} />\n <Choice value={answer.other?.value} detail={answer.other?.detail} source={answer.other?.source} onUse={onPick ? () => onPick(\"other\") : null} busy={busy} />\n </section>\n ) : st === \"not_found\" ? (\n <section className=\"grid gap-3\">\n <p className=\"flex items-baseline gap-2\">\n <span className=\"font-display text-2xl italic text-warning\">Not found</span>\n {history.length ? <span className=\"text-sm text-muted-foreground\">· asked {history.length + 1 === 2 ? \"twice\" : `${history.length + 1} times`}</span> : null}\n </p>\n {answer.note ? <p className=\"text-sm\">{answer.note}</p> : null}\n {answer.tried?.length ? (\n <div className=\"grid gap-2\">\n <h4 className=\"text-xs font-medium text-muted-foreground\">What it read</h4>\n {answer.tried.map((t, i) => (\n <Source key={i} source={t} />\n ))}\n </div>\n ) : null}\n </section>\n ) : st === \"blocked\" || st === \"waiting\" || st === \"queued\" || st === \"working\" || st === \"empty\" ? (\n <section className=\"grid gap-2\">\n <p className=\"font-display text-2xl italic\">{st === \"working\" ? \"Looking up\" : answerWords(answer)}</p>\n {st === \"blocked\" ? <p className=\"text-sm text-muted-foreground\">It needs {answer.blockedBy.join(\" and \")}, which wasn’t found. Type it there and this fills from it.</p> : null}\n {answer?.looking ? <p className=\"text-sm text-muted-foreground\">{answer.looking}</p> : null}\n </section>\n ) : (\n <section className=\"grid gap-3\">\n <p className={cx(\"break-words text-xl\", MONO.has(type) ? \"font-mono text-base\" : \"font-medium\", answer.confidence === \"guess\" && st === \"found\" && \"italic\")}>{answer.value}</p>\n {answer.detail ? <p className=\"text-sm text-muted-foreground\">{answer.detail}</p> : null}\n <p className=\"flex items-center gap-2 text-sm\">\n <SureMark confidence={answer.confidence} checked={st === \"checked\"} typed={st === \"typed\"} />\n <span className=\"font-medium capitalize\">{st === \"checked\" || st === \"typed\" ? answerWords(answer) : CONFIDENCE[answer.confidence]?.label ?? \"found\"}</span>\n {st === \"found\" && CONFIDENCE[answer.confidence] ? <span className=\"text-muted-foreground\">· {CONFIDENCE[answer.confidence].words}</span> : null}\n </p>\n {answer.source?.url || answer.source?.evidence ? <Source source={answer.source} mark={answer.value} /> : null}\n {st === \"stale\" ? <p className=\"text-sm text-muted-foreground\">Out of date: its question or its row changed. It keeps this until a new answer lands.</p> : null}\n </section>\n )}\n\n {needs.length ? (\n <section className=\"grid gap-2\">\n <h4 className=\"text-xs font-medium text-muted-foreground\">It needed</h4>\n <ul className=\"grid gap-1.5 text-sm\">\n {needs.map((n) => (\n <li key={n.name} className=\"flex items-center gap-2\">\n {n.answer ? <SureMark confidence={n.answer.confidence} checked={answerState(n.answer) === \"checked\"} typed={answerState(n.answer) === \"typed\"} /> : null}\n <span>\n {n.name}\n {n.answer?.confidence ? <span className=\"text-muted-foreground\"> · {n.answer.confidence}</span> : null}\n {n.answer?.value ? <span className=\"text-muted-foreground\"> · {n.answer.value}</span> : null}\n </span>\n </li>\n ))}\n </ul>\n </section>\n ) : null}\n\n {answer?.note && st !== \"not_found\" ? (\n <p className=\"text-sm\">\n <span className=\"font-medium\">How it was found.</span> {answer.note}\n </p>\n ) : null}\n\n {history.length ? (\n <section className=\"grid gap-2\">\n <h4 className=\"text-xs font-medium text-muted-foreground\">Asked</h4>\n <ul className=\"grid gap-1 text-sm\">\n {history.map((h, i) => (\n <li key={i} className=\"flex gap-3\">\n <span className=\"w-14 shrink-0 text-muted-foreground\">{h.at}</span>\n <span className=\"min-w-0 flex-1\">{h.words}</span>\n {h.credits != null ? <span data-nums className=\"font-mono text-xs text-muted-foreground\">{h.credits}</span> : null}\n </li>\n ))}\n </ul>\n </section>\n ) : null}\n\n {note ? <p className=\"text-sm text-muted-foreground\">{note}</p> : null}\n\n {st === \"suggested\" && (onKeep || onDrop) ? (\n <div className=\"flex flex-wrap gap-2\">\n {onKeep ? (\n <Button variant=\"primary\" onClick={onKeep} disabled={busy}>\n Keep it\n </Button>\n ) : null}\n {onDrop ? (\n <Button onClick={onDrop} disabled={busy}>\n Drop it\n </Button>\n ) : null}\n </div>\n ) : null}\n\n {(st === \"found\" || st === \"stale\") && onAccept && needsCheck(answer) ? (\n <Button variant=\"primary\" onClick={onAccept} disabled={busy} className=\"justify-self-start\">\n Accept it\n </Button>\n ) : null}\n\n {onType && st !== \"working\" ? (\n <form\n className=\"grid gap-1.5\"\n onSubmit={(e) => {\n e.preventDefault();\n if (own.trim()) onType(own.trim());\n }}\n >\n <label htmlFor={ownId} className=\"text-sm font-medium\">\n {typeWord}\n </label>\n <div className=\"flex gap-2\">\n <Input id={ownId} value={own} onChange={(e) => setOwn(e.target.value)} placeholder={placeholder} className=\"min-w-0 flex-1\" />\n <Button type=\"submit\" disabled={busy || !own.trim()}>\n Save\n </Button>\n </div>\n </form>\n ) : null}\n\n {onAskAgain && st !== \"working\" && st !== \"typed\" ? (\n <form\n className=\"grid gap-1.5\"\n onSubmit={(e) => {\n e.preventDefault();\n onAskAgain(hint.trim());\n }}\n >\n <label htmlFor={hintId} className=\"text-sm font-medium\">\n Or ask again, with a hint\n </label>\n <div className=\"flex gap-2\">\n <Input id={hintId} value={hint} onChange={(e) => setHint(e.target.value)} placeholder={hintPlaceholder} className=\"min-w-0 flex-1\" />\n <Button type=\"submit\" disabled={busy}>\n Ask again{askCost != null ? <span data-nums className=\"font-mono text-xs\"> · {askCost}</span> : null}\n </Button>\n </div>\n </form>\n ) : null}\n\n {st === \"not_found\" && onKeepNotFound ? (\n <Button onClick={onKeepNotFound} disabled={busy} className=\"justify-self-start\">\n Keep “not found”\n </Button>\n ) : null}\n\n {children}\n </div>\n );\n}\n\nfunction Choice({ value, detail, source, onUse, busy }) {\n return (\n <div className=\"grid gap-2 rounded-lg border border-border p-4\">\n <div className=\"flex items-baseline gap-3\">\n <span className=\"font-display text-4xl leading-none\">{value}</span>\n {detail ? <span className=\"text-sm text-muted-foreground\">{detail}</span> : null}\n </div>\n {source ? <Source source={source} mark={value} /> : null}\n {onUse ? (\n <Button variant=\"secondary\" size=\"sm\" onClick={onUse} disabled={busy} className=\"justify-self-start\">\n Use {value}\n </Button>\n ) : null}\n </div>\n );\n}\n\n/** A source: its title and address, and the words on it in the display\n * face, the answer marked where it appears. */\nexport function Source({ source, mark, className }) {\n if (!source) return null;\n const words = markWords(source.evidence, mark);\n return (\n <div className={cx(\"grid gap-0.5 border-l-2 border-border pl-3\", className)}>\n <div className=\"flex min-w-0 flex-wrap items-baseline gap-x-2 text-sm\">\n {source.title ? <span className=\"font-medium\">{source.title}</span> : null}\n {source.url ? (\n <a href={/^https?:/i.test(source.url) ? source.url : `https://${source.url}`} target=\"_blank\" rel=\"noreferrer\" className=\"min-w-0 truncate font-mono text-xs text-primary underline-offset-2 hover:underline\">\n {shortUrl(source.url)}\n </a>\n ) : null}\n </div>\n {source.evidence ? (\n <blockquote className=\"font-display text-lg leading-snug\">\n “\n {words.map((w, i) =>\n w.marked ? (\n <mark key={i} className=\"rounded-sm bg-primary/15 px-0.5 text-foreground\">\n {w.text}\n </mark>\n ) : (\n <span key={i}>{w.text}</span>\n ),\n )}\n ”\n </blockquote>\n ) : null}\n </div>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/agent-column/receipt.jsx",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "@components/makersclaw/blocks/agent-column/receipt.jsx",
|
|
29
|
+
"content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { Button, Input, cx } from \"../../ui\";\nimport { AgentMark } from \"../agent-marks\";\nimport { CONFIDENCE, answerState, decidingWords, shortUrl } from \"./model\";\n\n// The receipt: an answer as a claim with its citation (grown in Data\n// Enrichment 0.5.0, the v2 design's signature). In a dense grid the\n// answer shrinks to one line, its value and its trust as a mark and a\n// word (AnswerLine, TrustMark); opened, it is drawn big: the value, how\n// sure, and each source as a clipping (the page's title, its address and\n// date, and the words on it with the deciding ones underlined). Two\n// answers are two clippings to pick from; \"not found\" is a receipt too,\n// every page the agent tried crossed off. And a column's heading as a job\n// (JobHeader): its name, how far it got, what a row costs, its menu.\n//\n// Every state is a shape and a word, never colour alone. Colours are the\n// theme's slots: chart-1 sure, chart-2 likely, chart-3 guess, chart-4\n// sent, chart-5 typed by a person, success a person's check, warning\n// two answers or a flag, info looking up, destructive failed; the rest\n// muted. Props in, callbacks out. Tokens only.\n\n/* ------------------------------ the marks ------------------------------ */\n\n// The agent's mark is the platform's one (the agent-marks block): a\n// filled diamond. Re-exported so a screen that took AgentMark from here\n// keeps it.\nexport { AgentMark };\n\nconst Dot = ({ kind }) =>\n kind === \"sure\" ? (\n <span aria-hidden=\"true\" className=\"inline-block size-2 shrink-0 rounded-full bg-current\" />\n ) : kind === \"likely\" ? (\n <span aria-hidden=\"true\" className=\"relative inline-block size-2 shrink-0 overflow-hidden rounded-full border-[1.5px] border-current\">\n <span className=\"absolute inset-y-0 left-0 w-1/2 bg-current\" />\n </span>\n ) : kind === \"guess\" ? (\n <span aria-hidden=\"true\" className=\"inline-block size-[9px] shrink-0 rounded-full border-[1.5px] border-dashed border-current\" />\n ) : kind === \"two\" ? (\n <span aria-hidden=\"true\" className=\"relative inline-block size-[9px] shrink-0 border-[1.5px] border-current\">\n <span className=\"absolute inset-y-0 left-1/2 w-[1.5px] -translate-x-1/2 bg-current\" />\n </span>\n ) : kind === \"wait\" ? (\n <span aria-hidden=\"true\" className=\"inline-block size-2 shrink-0 rounded-full border-[1.5px] border-current border-t-transparent\" />\n ) : kind === \"none\" ? (\n <span aria-hidden=\"true\" className=\"relative inline-block size-[9px] shrink-0 overflow-hidden rounded-full border-[1.5px] border-current\">\n <span className=\"absolute left-1/2 top-[-2px] h-[13px] w-[1.5px] -translate-x-1/2 rotate-45 bg-current\" />\n </span>\n ) : kind === \"looking\" ? (\n <span aria-hidden=\"true\" className=\"relative inline-block h-[3px] w-[18px] shrink-0 overflow-hidden rounded-full border border-current\">\n <span className=\"absolute inset-y-0 left-0 w-1/3 animate-pulse bg-current motion-reduce:animate-none\" />\n </span>\n ) : kind === \"failed\" ? (\n <span aria-hidden=\"true\" className=\"text-[10px] leading-none\">▲</span>\n ) : kind === \"typed\" ? (\n <span aria-hidden=\"true\" className=\"text-[11px] leading-none\">✎</span>\n ) : kind === \"sent\" ? (\n <span aria-hidden=\"true\" className=\"text-[11px] leading-none\">↗</span>\n ) : kind === \"stale\" ? (\n <span aria-hidden=\"true\" className=\"text-[11px] leading-none\">↻</span>\n ) : null;\n\nconst TONE = {\n sure: \"text-chart-1\",\n likely: \"text-chart-2\",\n guess: \"text-chart-3\",\n two: \"text-warning\",\n warn: \"text-warning\",\n looking: \"text-info\",\n failed: \"text-destructive\",\n typed: \"text-chart-5\",\n sent: \"text-chart-4\",\n ok: \"text-success\",\n quiet: \"text-muted-foreground\",\n};\n\n/**\n * A trust mark and its word, in the state's tone: sure ●, likely ◐,\n * guess ◌ (never shown as verified), two answers ▣, looking up ▬, waits\n * ◔, not found ⊘, failed ▲, typed ✎, sent ↗, out of date ↻.\n *\n * kind sure · likely · guess · two · warn · looking · wait · none ·\n * failed · typed · sent · stale · quiet\n * word the word beside it (\"\" for the mark alone, which still names\n * itself to a screen reader)\n * check a person's tick after it\n */\nexport function TrustMark({ kind, word, check = false, className }) {\n const glyph = { warn: \"two\", quiet: null }[kind] ?? kind;\n const tone = TONE[kind] ?? TONE.quiet;\n const said = word ?? CONFIDENCE[kind]?.label ?? \"\";\n const aria = [said || CONFIDENCE[kind]?.label || kind, check ? \"checked\" : \"\"].filter(Boolean).join(\", \");\n return (\n <span className={cx(\"relative inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-[11px]\", tone, className)}>\n {glyph ? <Dot kind={glyph === \"wait\" || glyph === \"none\" ? glyph : glyph} /> : null}\n {said ? <span>{said}</span> : <span className=\"sr-only\">{aria}</span>}\n {check ? (\n <span className=\"text-[11px] font-bold text-success\" aria-label=\"checked\">\n ✓\n </span>\n ) : null}\n </span>\n );\n}\n\nconst sureKind = (c) => (c === \"sure\" || c === \"likely\" || c === \"guess\" ? c : \"quiet\");\n\n/* ------------------------------ the line ------------------------------ */\n\n/**\n * One answer on one line, as a dense grid draws it: the value at the\n * start, its trust at the end (a person's check after it); a state with\n * no value says the state in words.\n *\n * answer the answer (model.js), or null for not asked yet\n * type text · link · email · number · domain\n * onRetry a failed answer's \"Try again\"\n * queuedWord what a queued answer says (\"queued\", \"back in the queue\")\n */\nexport function AnswerLine({ answer, onRetry, queuedWord = \"queued\", className }) {\n const st = answerState(answer);\n const a = answer ?? {};\n const value = (v, struck) => <span className={cx(\"min-w-0 flex-[0_1_auto] truncate\", struck && \"text-muted-foreground line-through decoration-muted-foreground\")}>{v}</span>;\n const row = (children) => <span className={cx(\"flex w-full min-w-0 items-center gap-1.5\", className)}>{children}</span>;\n const end = (node) => <span className=\"ml-auto flex shrink-0 items-center gap-1\">{node}</span>;\n switch (st) {\n case \"empty\":\n return row(<span className=\"text-input\">—</span>);\n case \"queued\":\n return row(<TrustMark kind=\"quiet\" word={a.value ? `${queuedWord} · ${a.value}` : queuedWord} />);\n case \"waiting\":\n return row(<TrustMark kind=\"wait\" word={a.waitsFor?.length ? `waits for ${a.waitsFor.join(\", \")}` : \"waits for the row\"} className=\"text-muted-foreground\" />);\n case \"blocked\":\n return row(<TrustMark kind=\"wait\" word={`can't fill · needs ${(a.blockedBy ?? []).join(\" and \")}`} className=\"text-muted-foreground\" />);\n case \"working\":\n return row(<TrustMark kind=\"looking\" word={a.value ? \"writing\" : a.looking ? `looking up · ${a.looking}` : \"looking up\"} className=\"min-w-0 truncate\" />);\n case \"not_found\":\n return row(<TrustMark kind=\"none\" word={a.checked ? \"not found, kept empty\" : \"not found\"} check={a.checked} className=\"text-muted-foreground\" />);\n case \"failed\":\n return row(\n <>\n <TrustMark kind=\"failed\" word={`failed${a.detail || a.note ? ` · ${a.detail || a.note}` : \"\"}`} className=\"min-w-0 truncate\" />\n {onRetry ? (\n <span className=\"ml-auto shrink-0\">\n <Button variant=\"link\" size=\"inherit\" onClick={(e) => (e.stopPropagation(), onRetry())} className=\"text-xs\">\n Try again\n </Button>\n </span>\n ) : null}\n </>,\n );\n case \"two_answers\":\n return row(\n <>\n {value(`${a.value} · ${a.other?.value ?? \"\"}`)}\n {end(<TrustMark kind=\"two\" word=\"2 answers\" />)}\n </>,\n );\n case \"stale\":\n return row(\n <>\n {value(a.value, true)}\n {end(<TrustMark kind=\"stale\" word=\"out of date\" className=\"text-muted-foreground\" />)}\n </>,\n );\n case \"typed\":\n return row(\n <>\n {value(a.value)}\n {end(<TrustMark kind=\"typed\" word=\"typed\" />)}\n </>,\n );\n default: {\n if (a.flag && !a.checked) return row(<TrustMark kind=\"warn\" word={a.flag === \"left\" ? `${a.value}${a.movedTo ? ` · ${a.movedTo}` : \"\"}` : `${a.value ?? \"\"}${a.flag === \"may_not_fit\" ? \": may not fit\" : \"\"}`} className=\"min-w-0 truncate\" />);\n const checked = st === \"checked\";\n const kind = sureKind(a.confidence);\n return row(\n <>\n {value(a.value)}\n {end(<TrustMark kind={kind} word={checked && kind !== \"guess\" ? \"\" : CONFIDENCE[kind]?.label ?? \"\"} check={checked} />)}\n </>,\n );\n }\n }\n}\n\n/* ------------------------------ the heading ------------------------------ */\n\n/**\n * A column's heading as a job: the agent's mark and its name, its menu,\n * then a bar of how far it got, the words (\"138 of 480 · 2 to check\")\n * and what a row costs. A person's own column says where it came from.\n *\n * name \"Headcount\"\n * agent false: a person's own column\n * source for a person's column, the line under its name (\"yours ·\n * summit-leads.csv\"; default \"yours\")\n * progress \"21 of 22 · 3 to check\"; part 0 to 1 for the bar\n * cost \"40 cr/row\"\n * menu a node at the end of the first line (a kit Menu)\n */\nexport function JobHeader({ name, agent = true, source, progress, part, cost, menu, className }) {\n return (\n <div className={cx(\"grid min-w-0 gap-1.5\", className)}>\n <div className={cx(\"flex min-w-0 items-center gap-1.5\", agent ? \"font-semibold\" : \"font-medium\")}>\n {agent ? <AgentMark /> : null}\n <span className=\"min-w-0 flex-1 truncate\">{name}</span>\n {menu ?? null}\n </div>\n <div className={cx(\"flex min-w-0 items-center gap-1.5 whitespace-nowrap text-[11px] font-normal\", agent ? \"text-muted-foreground\" : \"text-chart-5\")}>\n {agent ? (\n <>\n <span aria-hidden=\"true\" className=\"relative inline-block h-[3px] w-[30px] shrink-0 overflow-hidden rounded-full bg-muted\">\n <span className=\"absolute inset-y-0 left-0 bg-primary\" style={{ width: `${Math.round(Math.max(0, Math.min(1, part ?? 0)) * 100)}%` }} />\n </span>\n <span className=\"min-w-0 truncate\">{progress}</span>\n {cost ? <span className=\"ml-auto shrink-0\">{cost}</span> : null}\n </>\n ) : (\n <span className=\"truncate\">{source ?? \"yours\"}</span>\n )}\n </div>\n </div>\n );\n}\n\n/* ------------------------------ the receipt ------------------------------ */\n\n/** The words on the page, the deciding ones underlined: `[[…]]` in the\n * words marks them, else the answer's value where it appears. */\nfunction Quote({ words, mark }) {\n const parts = decidingWords(words, mark);\n return (\n <q className=\"block leading-normal [quotes:none] before:content-['\\201C'] after:content-['\\201D']\">\n {parts.map((p, i) =>\n p.marked ? (\n <mark key={i} className=\"bg-accent px-px text-accent-foreground underline decoration-primary decoration-2 underline-offset-[3px]\">\n {p.text}\n </mark>\n ) : (\n <span key={i}>{p.text}</span>\n ),\n )}\n </q>\n );\n}\n\n/**\n * A source as a clipping: its trust, the page's title, an action at the\n * end (Pick 85), the address and when it was read, the words on it with\n * the deciding ones underlined, and a footnote.\n *\n * source { url, title, evidence, read } (read: \"read 22 Sep\")\n * trust sure · likely · guess (the dot before the title), or null\n * mark the value to underline when the words carry no [[…]]\n * title overrides source.title (\"62 people · likely\")\n * action a node at the head's end\n * footnote a muted line under the words\n * picked drawn as the one chosen\n */\nexport function Clipping({ source, trust, mark, title, action, footnote, picked = false, children, className }) {\n const s = source ?? {};\n const url = s.url ? shortUrl(s.url) : null;\n return (\n <div className={cx(\"grid gap-1.5 rounded-[var(--radius)] border bg-card px-3 py-2.5 text-card-foreground\", picked ? \"border-primary ring-1 ring-primary\" : \"border-border\", className)}>\n <div className=\"flex min-w-0 items-center gap-2\">\n {trust ? <TrustMark kind={sureKind(trust)} word=\"\" /> : null}\n <b className=\"min-w-0 flex-1 truncate font-semibold\">{title ?? s.title ?? url ?? \"A page\"}</b>\n {action ?? null}\n </div>\n {url || s.read ? (\n <div className=\"min-w-0 truncate text-xs text-muted-foreground\">\n {url ? (\n <a href={/^https?:/i.test(s.url) ? s.url : `https://${s.url}`} target=\"_blank\" rel=\"noreferrer\" className=\"underline-offset-2 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring\">\n {url}\n </a>\n ) : null}\n {url && s.read ? \" · \" : null}\n {s.read ?? null}\n </div>\n ) : null}\n {s.evidence ? <Quote words={s.evidence} mark={mark} /> : null}\n {footnote ? <div className=\"text-xs text-muted-foreground\">{footnote}</div> : null}\n {children}\n </div>\n );\n}\n\n/** What the agent tried, each page crossed off, with why it gave up. */\nexport function TriedList({ tried = [], className }) {\n if (!tried.length) return null;\n return (\n <ul className={cx(\"grid gap-1 text-xs\", className)}>\n {tried.map((t, i) => (\n <li key={i} className=\"flex min-w-0 items-baseline gap-2\">\n <s className=\"min-w-0 truncate text-muted-foreground\">{t.title || shortUrl(t.url) || \"a page\"}</s>\n {t.evidence ? <span className=\"min-w-0 truncate\">{t.evidence}</span> : null}\n </li>\n ))}\n </ul>\n );\n}\n\n/**\n * The receipt: one answer opened, drawn big. Its head (the record, the\n * question, the row), the value and its state in words, then the\n * clippings: one for a found answer, two to pick from for two answers,\n * the page that says a person left and who holds the role now, or what\n * was tried for not found.\n *\n * answer the answer (model.js)\n * title the record (\"Klarwerk\"); question (\"Headcount\"); where (\"row 2\")\n * onPick(which) \"one\" | \"other\": Pick on each of two answers\n * pickWords(value) the Pick button's words (default \"Pick <value>\")\n * picked which of two was picked (\"one\" | \"other\"), drawn chosen\n * read the date the pages were read (\"read 22 Sep\")\n * by who found it, a node (\"Found by the agent · 22 Sep, 10:31\")\n * valueLabel overrides the big value (\"Head of Product, or Product Lead\")\n * stateWords overrides the state's words beside it\n * leftTitle for a person who left: the title over who holds the role now\n * children your actions, under the receipt\n */\nexport function Receipt({ answer, title, question, where, onPick, pickWords, picked, read, by, valueLabel, stateWords, leftTitle = \"Now in the role\", busy = false, children, className }) {\n const st = answerState(answer);\n const a = answer ?? {};\n const two = st === \"two_answers\" || (a.other?.value && picked);\n const left = a.flag === \"left\";\n const kind = left || st === \"two_answers\" ? \"two\" : st === \"not_found\" ? \"none\" : st === \"failed\" ? \"failed\" : st === \"typed\" ? \"typed\" : st === \"working\" ? \"looking\" : sureKind(a.confidence);\n const big =\n valueLabel ??\n (st === \"two_answers\" ? `${a.value} or ${a.other?.value ?? \"\"}` : st === \"not_found\" ? \"Not found\" : st === \"failed\" ? \"Failed\" : st === \"working\" ? \"Looking up\" : st === \"empty\" ? \"Not asked yet\" : a.value);\n const words =\n stateWords ??\n (left\n ? \"left the company · needs you\"\n : st === \"two_answers\"\n ? \"two answers · the sources disagree\"\n : st === \"checked\"\n ? `${CONFIDENCE[a.confidence]?.label ?? \"found\"} · checked`\n : st === \"typed\"\n ? \"typed by a person · never overwritten\"\n : st === \"not_found\"\n ? a.checked\n ? \"kept empty\"\n : \"what it tried, crossed off\"\n : st === \"failed\"\n ? \"a page didn't answer\"\n : CONFIDENCE[a.confidence]\n ? `${CONFIDENCE[a.confidence].label} · ${CONFIDENCE[a.confidence].words}`\n : \"\");\n const pickLabel = (v) => (pickWords ? pickWords(v) : `Pick ${v}`);\n const withRead = (s) => (s ? { ...s, read: s.read ?? read } : s);\n return (\n <div className={cx(\"grid min-w-0 gap-3\", className)}>\n {title || question ? (\n <div className=\"flex min-w-0 flex-wrap items-baseline gap-x-1.5\">\n {title ? <b className=\"text-sm font-semibold\">{title}</b> : null}\n {question || where ? <span className=\"text-muted-foreground\">· {[question, where].filter(Boolean).join(\" · \")}</span> : null}\n </div>\n ) : null}\n <div className=\"grid gap-1\">\n <span className={cx(\"break-words font-semibold leading-tight\", String(big ?? \"\").length > 24 ? \"text-xl\" : \"text-2xl\", a.confidence === \"guess\" && st === \"found\" && \"italic\")}>{big}</span>\n {words ? <TrustMark kind={kind} word={words} check={st === \"checked\"} className=\"text-xs\" /> : null}\n {a.detail && !two && st !== \"failed\" ? <span className=\"text-xs text-muted-foreground\">{a.detail}</span> : null}\n </div>\n {two && !left ? (\n <>\n <Clipping\n source={withRead(a.source)}\n trust={a.confidence ?? \"likely\"}\n mark={a.value}\n picked={picked === \"one\"}\n action={onPick ? (picked === \"one\" ? <Button size=\"sm\" variant=\"primary\" disabled>Picked</Button> : <Button size=\"sm\" onClick={() => onPick(\"one\")} disabled={busy}>{pickLabel(a.value)}</Button>) : null}\n footnote={a.detail}\n />\n <Clipping\n source={withRead(a.other?.source)}\n trust={a.other?.confidence ?? a.confidence ?? \"likely\"}\n mark={a.other?.value}\n picked={picked === \"other\"}\n action={onPick ? (picked === \"other\" ? <Button size=\"sm\" variant=\"primary\" disabled>Picked</Button> : <Button size=\"sm\" onClick={() => onPick(\"other\")} disabled={busy}>{pickLabel(a.other?.value)}</Button>) : null}\n footnote={a.other?.detail}\n />\n </>\n ) : left ? (\n <>\n {a.source ? <Clipping source={withRead(a.source)} trust={a.confidence ?? \"sure\"} mark={a.value} footnote={a.note} /> : null}\n {a.other?.value ? (\n <>\n <span className=\"text-[11px] uppercase tracking-[0.04em] text-muted-foreground\">{leftTitle}</span>\n <Clipping source={withRead(a.other.source)} trust=\"sure\" title={`${a.other.value}${a.other.detail ? `, ${a.other.detail}` : \"\"}`} mark={a.other.value} />\n </>\n ) : null}\n </>\n ) : st === \"not_found\" || st === \"failed\" ? (\n <Clipping source={{ title: st === \"failed\" ? \"What it read\" : \"What it tried\", read }} title={st === \"failed\" ? \"What it read\" : \"What it tried\"} footnote={a.note}>\n <TriedList tried={a.tried?.length ? a.tried : a.source?.url ? [{ title: a.source.title, url: a.source.url, evidence: a.detail }] : []} />\n </Clipping>\n ) : a.source?.url || a.source?.evidence ? (\n <Clipping source={withRead(a.source)} trust={a.confidence} mark={a.value} footnote={st === \"stale\" ? \"Out of date: its question or its row changed since. It keeps this until a new answer lands.\" : undefined} />\n ) : null}\n {a.note && st !== \"not_found\" && st !== \"failed\" && !left ? (\n <div className=\"text-xs\">\n <span className=\"text-[11px] uppercase tracking-[0.04em] text-muted-foreground\">How it was found</span>\n <br />\n {a.note}\n </div>\n ) : null}\n {by ? <div className=\"flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground\">{by}</div> : null}\n {children}\n </div>\n );\n}\n\n/**\n * What a person can do with an answer: type their own (theirs, never\n * overwritten), or ask again with a hint, its price said.\n *\n * onType(value) · typePlaceholder\n * onAskAgain(hint) · hintPlaceholder · askCost (\"about 40 credits\")\n * askNote a line under it (\"The old answers go into this answer's history.\")\n * askLabel the button's words (default \"Ask again\")\n * busy\n */\nexport function ReceiptActions({ onType, typePlaceholder = \"Your answer\", onAskAgain, hintPlaceholder = \"Ask again with a hint\", askCost, askNote, askLabel = \"Ask again\", hint: initialHint = \"\", busy = false, className }) {\n const [typing, setTyping] = useState(false);\n const [own, setOwn] = useState(\"\");\n const [hint, setHint] = useState(initialHint ?? \"\");\n const ownId = useId();\n const hintId = useId();\n return (\n <div className={cx(\"grid gap-2\", className)}>\n {onType ? (\n typing ? (\n <form\n className=\"relative flex gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n if (own.trim()) onType(own.trim());\n }}\n >\n <label htmlFor={ownId} className=\"sr-only\">\n Your own answer\n </label>\n <Input id={ownId} value={own} onChange={(e) => setOwn(e.target.value)} placeholder={typePlaceholder} className=\"min-w-0 flex-1\" autoFocus />\n <Button type=\"submit\" size=\"sm\" disabled={busy || !own.trim()}>\n Save\n </Button>\n </form>\n ) : (\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button size=\"sm\" onClick={() => setTyping(true)} disabled={busy}>\n Type my own\n </Button>\n <span className=\"text-xs text-muted-foreground\">yours, never overwritten</span>\n </div>\n )\n ) : null}\n {onAskAgain ? (\n <form\n className=\"relative grid gap-1\"\n onSubmit={(e) => {\n e.preventDefault();\n onAskAgain(hint.trim());\n }}\n >\n <div className=\"flex gap-2\">\n <label htmlFor={hintId} className=\"sr-only\">\n A hint for the agent\n </label>\n <Input id={hintId} value={hint} onChange={(e) => setHint(e.target.value)} placeholder={hintPlaceholder} className=\"min-w-0 flex-1\" />\n <Button type=\"submit\" size=\"sm\" disabled={busy}>\n <AgentMark />\n {askLabel}\n </Button>\n </div>\n {askCost || askNote ? <span className=\"text-xs text-muted-foreground\">{[askCost, askNote].filter(Boolean).join(\". \")}</span> : null}\n </form>\n ) : null}\n </div>\n );\n}\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "registry/blocks/agent-column/column-picker.jsx",
|
|
33
|
+
"type": "registry:component",
|
|
34
|
+
"target": "@components/makersclaw/blocks/agent-column/column-picker.jsx",
|
|
35
|
+
"content": "\"use client\";\n\nimport { useId, useMemo, useState } from \"react\";\nimport { Button, Checkbox, Field, Input, Select, Tabs, TabPanel, Textarea, cx } from \"../../ui\";\n\n// ColumnPicker — choose the columns an agent will fill: common ones from\n// a list the app passes in (each with its cost per record), or the\n// person's own question with the kind of answer. It says what they cost\n// together before anything starts. It saves nothing: onSubmit gets the\n// choice.\n//\n// recipes [{ key, name, description, credits, input?: \"role\" | \"instructions\",\n// defaultRole?, needs?: [\"Domain\"], checked? }]\n// rows how many records the columns would fill (for the total)\n// heading the common tab's label (\"Common, for companies\")\n// onSubmit([{ recipe, name, ask?, example?, answerType?, options? }])\n// submitLabel(n) the button's words (\"Add 6 columns\")\n// answerTypes [{ value, label }] for your own question\n// ownCredits what your own question costs per record\n\nconst ANSWER_TYPES = [\n { value: \"yes_no\", label: \"Yes or no\" },\n { value: \"number\", label: \"A number\" },\n { value: \"text\", label: \"A short text\" },\n { value: \"link\", label: \"A link\" },\n { value: \"choice\", label: \"One of a few choices\" },\n];\n\nexport function ColumnPicker({\n recipes,\n rows = 0,\n heading = \"Common\",\n onSubmit,\n submitLabel = (n) => (n === 1 ? \"Add 1 column\" : `Add ${n} columns`),\n answerTypes = ANSWER_TYPES,\n ownCredits = 50,\n busy = false,\n unit = \"credits\",\n noun = \"row\",\n}) {\n const id = useId();\n const [tab, setTab] = useState(\"common\");\n const [picked, setPicked] = useState(() => Object.fromEntries(recipes.filter((r) => r.checked).map((r) => [r.key, true])));\n const [roles, setRoles] = useState(() => Object.fromEntries(recipes.filter((r) => r.input === \"role\").map((r) => [r.key, r.defaultRole ?? \"\"])));\n const [how, setHow] = useState(() => Object.fromEntries(recipes.filter((r) => r.input === \"instructions\").map((r) => [r.key, { ask: r.defaultAsk ?? \"\", example: r.defaultExample ?? \"\" }])));\n const [own, setOwn] = useState({ question: \"\", name: \"\", answerType: \"yes_no\", options: \"\" });\n\n const chosen = useMemo(() => {\n const out = recipes\n .filter((r) => picked[r.key])\n .map((r) => ({\n recipe: r.key,\n name: r.input === \"role\" ? roles[r.key]?.trim() || r.name : r.name,\n ask: r.input === \"role\" ? roles[r.key]?.trim() : r.input === \"instructions\" ? how[r.key]?.ask?.trim() : undefined,\n example: r.input === \"instructions\" ? how[r.key]?.example?.trim() : undefined,\n credits: r.credits,\n }));\n if (own.question.trim())\n out.push({\n recipe: \"question\",\n name: own.name.trim() || own.question.trim().slice(0, 40),\n ask: own.question.trim(),\n answerType: own.answerType,\n options: own.answerType === \"choice\" ? own.options.split(\",\").map((s) => s.trim()).filter(Boolean) : [],\n credits: ownCredits,\n });\n return out;\n }, [recipes, picked, roles, how, own, ownCredits]);\n const perRow = chosen.reduce((n, c) => n + (c.credits ?? 0), 0);\n const missingRole = recipes.some((r) => picked[r.key] && r.input === \"role\" && !roles[r.key]?.trim());\n\n return (\n <div className=\"grid gap-4\">\n <Tabs\n aria-label=\"Which columns\"\n id={id}\n value={tab}\n onChange={setTab}\n tabs={[\n { key: \"common\", label: heading },\n { key: \"own\", label: \"Your own question\" },\n ]}\n />\n <TabPanel tab=\"common\" value={tab} id={id} className=\"pt-0\">\n <ul className=\"grid divide-y divide-border\">\n {recipes.map((r) => {\n const on = Boolean(picked[r.key]);\n return (\n <li key={r.key} className=\"grid gap-2 py-2\">\n <div className=\"flex items-start gap-3\">\n <Checkbox\n id={`${id}-${r.key}`}\n checked={on}\n onChange={(e) => setPicked((p) => ({ ...p, [r.key]: e.target.checked }))}\n aria-label={r.name}\n className=\"min-h-0 pt-0.5\"\n />\n <label htmlFor={`${id}-${r.key}`} className=\"min-w-0 flex-1 cursor-pointer\">\n <span className=\"block text-sm font-medium\">{r.name}</span>\n {r.description ? <span className=\"block text-xs text-muted-foreground\">{r.description}</span> : null}\n </label>\n <span data-nums className=\"shrink-0 font-mono text-xs text-muted-foreground\">\n {r.credits} / {noun}\n </span>\n </div>\n {on && r.input === \"role\" ? (\n <Field id={`${id}-${r.key}-role`} label=\"Role\" className=\"ml-7\">\n <Input id={`${id}-${r.key}-role`} value={roles[r.key] ?? \"\"} onChange={(e) => setRoles((s) => ({ ...s, [r.key]: e.target.value }))} placeholder=\"Head of Compliance\" />\n </Field>\n ) : null}\n {on && r.input === \"instructions\" ? (\n <div className=\"ml-7 grid gap-3\">\n <Field id={`${id}-${r.key}-how`} label=\"How to write it\">\n <Textarea id={`${id}-${r.key}-how`} rows={3} value={how[r.key]?.ask ?? \"\"} onChange={(e) => setHow((s) => ({ ...s, [r.key]: { ...s[r.key], ask: e.target.value } }))} />\n </Field>\n <Field id={`${id}-${r.key}-ex`} label=\"A line of yours, as an example\">\n <Textarea id={`${id}-${r.key}-ex`} rows={2} value={how[r.key]?.example ?? \"\"} onChange={(e) => setHow((s) => ({ ...s, [r.key]: { ...s[r.key], example: e.target.value } }))} />\n </Field>\n </div>\n ) : null}\n </li>\n );\n })}\n </ul>\n </TabPanel>\n <TabPanel tab=\"own\" value={tab} id={id} className=\"grid gap-3 pt-0\">\n <Field id={`${id}-q`} label=\"Your question\" hint=\"The agent answers it for every row, from the public web.\">\n <Textarea id={`${id}-q`} rows={2} value={own.question} onChange={(e) => setOwn((s) => ({ ...s, question: e.target.value }))} placeholder=\"Do they sell to banks?\" />\n </Field>\n <Field id={`${id}-n`} label=\"Column name\">\n <Input id={`${id}-n`} value={own.name} onChange={(e) => setOwn((s) => ({ ...s, name: e.target.value }))} placeholder=\"Sells to banks\" />\n </Field>\n <Field id={`${id}-t`} label=\"The kind of answer\">\n <Select id={`${id}-t`} value={own.answerType} onChange={(e) => setOwn((s) => ({ ...s, answerType: e.target.value }))} options={answerTypes} />\n </Field>\n {own.answerType === \"choice\" ? (\n <Field id={`${id}-o`} label=\"The choices\" hint=\"Separated by commas.\">\n <Input id={`${id}-o`} value={own.options} onChange={(e) => setOwn((s) => ({ ...s, options: e.target.value }))} placeholder=\"Banks, Fintechs, Both, Neither\" />\n </Field>\n ) : null}\n <p data-nums className=\"font-mono text-xs text-muted-foreground\">\n {ownCredits} {unit} / {noun}\n </p>\n </TabPanel>\n <div className=\"grid gap-2 border-t border-border pt-3\">\n <p className=\"text-sm\">\n <span className=\"font-medium\">\n {chosen.length} {chosen.length === 1 ? \"column\" : \"columns\"}\n </span>{\" \"}\n · <span data-nums className=\"font-mono\">{perRow.toLocaleString(\"en-US\")}</span> {unit} a {noun}\n </p>\n {rows ? (\n <p className=\"text-xs text-muted-foreground\">\n All {rows.toLocaleString(\"en-US\")} {rows === 1 ? noun : `${noun}s`}: about <span data-nums className=\"font-mono\">{(perRow * rows).toLocaleString(\"en-US\")}</span>. Nothing is spent until you start a fill.\n </p>\n ) : null}\n <Button variant=\"primary\" disabled={busy || chosen.length === 0 || missingRole} loading={busy} onClick={() => onSubmit?.(chosen)} className={cx(\"justify-self-start\")}>\n {submitLabel(chosen.length)}\n </Button>\n </div>\n </div>\n );\n}\n"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "registry/blocks/agent-column/spreadsheet-type.jsx",
|
|
39
|
+
"type": "registry:component",
|
|
40
|
+
"target": "@components/makersclaw/blocks/agent-column/spreadsheet-type.jsx",
|
|
41
|
+
"content": "\"use client\";\n\nimport { AnswerCell } from \"./agent-column\";\nimport { answerWords } from \"./model\";\n\n// A column type for the spreadsheet block (and base-view, which passes\n// `types` through): the value is an answer, drawn by AnswerCell. A person\n// who types in the cell sets their own value (setBy \"person\"), which the\n// app saves and the agent never overwrites.\n//\n// <Spreadsheet types={{ answer: answerColumnType }}\n// columns={[{ key: \"headcount\", name: \"Headcount\", type: \"answer\", answerType: \"number\" }]} … />\nexport const answerColumnType = {\n label: \"Agent answer\",\n icon: \"sparkles\",\n width: 200,\n editor: \"text\",\n empty: (v) => v == null || (v.value == null && v.status == null),\n parse: (text) => ({ value: { status: \"found\", value: String(text ?? \"\").trim(), setBy: \"person\" } }),\n toText: (v) => (v?.value == null ? \"\" : String(v.value)),\n format: (v) => (v?.value != null ? String(v.value) : answerWords(v)),\n compare: (a, b) => String(a?.value ?? \"\").localeCompare(String(b?.value ?? \"\"), undefined, { numeric: true }),\n render: (v, column) => <AnswerCell answer={v} type={column?.answerType ?? \"text\"} lines={1} />,\n};\n"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"path": "registry/blocks/agent-column/model.js",
|
|
45
|
+
"type": "registry:component",
|
|
46
|
+
"target": "@components/makersclaw/blocks/agent-column/model.js",
|
|
47
|
+
"content": "// agent-column — the pure half: what state an answer is in, what it says\n// in words, whether a person should look at it, and a column's progress.\n// Server-safe (no React), so a server surface can count the same way the\n// screen draws.\n//\n// An answer is one cell an agent fills (or suggests) for one record:\n//\n// {\n// id, status, // queued | working | found | not_found | two_answers | failed | stale | suggested\n// value, detail, link, // the answer (\"62\"), a line under it (\"51–200 on LinkedIn\"), a URL\n// confidence, // sure | likely | guess\n// source: { url, title, evidence }, // where it came from, and the words on the page\n// other: { value, detail, link, source: { url, title, evidence } }, // the second answer\n// note, // how it was found, or what was tried\n// tried: [{ title, url, evidence }], // what it read (not found)\n// flag, // may_not_fit | left (or your own word)\n// setBy, // agent | person — a person's value is never overwritten\n// checked, // a person accepted it\n// looking, // what the agent is reading now (working)\n// waitsFor: [\"Domain\"], // columns it waits for (queued)\n// blockedBy: [\"Head of Compliance\"], // columns it needs that weren't found (queued)\n// waitsForRow, // it reads the whole record and goes last\n// }\n//\n// A missing answer (null) is \"not asked yet\".\n\nexport const CONFIDENCE = {\n sure: { label: \"sure\", words: \"their own page says it, or two sources agree\" },\n likely: { label: \"likely\", words: \"one good source\" },\n guess: { label: \"guess\", words: \"worked out, not found written\" },\n};\n\n/** The state an answer shows, one word the screen can switch on. */\nexport function answerState(a) {\n if (!a) return \"empty\";\n if (a.setBy === \"person\") return \"typed\";\n const s = a.status;\n if (s === \"queued\") {\n if (a.blockedBy?.length) return \"blocked\";\n if (a.waitsFor?.length || a.waitsForRow) return \"waiting\";\n return \"queued\";\n }\n if (s === \"found\" && a.checked) return \"checked\";\n return s || \"empty\";\n}\n\nconst list = (names) => (names.length <= 1 ? names[0] ?? \"\" : `${names.slice(0, -1).join(\", \")} and ${names[names.length - 1]}`);\n\n/** What the answer's state says, in plain words (\"Waits for Domain\"). */\nexport function answerWords(a) {\n switch (answerState(a)) {\n case \"empty\":\n return \"not asked yet\";\n case \"queued\":\n return \"Queued\";\n case \"waiting\":\n return a.waitsFor?.length ? `Waits for ${list(a.waitsFor)}` : \"Waits for the rest of the row\";\n case \"blocked\":\n return \"Can't fill\";\n case \"working\":\n return \"Looking up\";\n case \"not_found\":\n return \"Not found\";\n case \"two_answers\":\n return \"two answers\";\n case \"failed\":\n return \"Failed\";\n case \"stale\":\n return \"out of date\";\n case \"suggested\":\n return \"suggested\";\n case \"checked\":\n return \"checked by you\";\n case \"typed\":\n return \"typed by you\";\n default:\n return CONFIDENCE[a.confidence]?.label ?? \"found\";\n }\n}\n\n/** A person should look at it: nobody accepted it, and it is not found,\n * two answers, failed, a guess, a flag, or a suggestion. */\nexport function needsCheck(a) {\n if (!a || a.checked || a.setBy === \"person\") return false;\n if ([\"not_found\", \"two_answers\", \"failed\", \"suggested\"].includes(a.status)) return true;\n return a.status === \"found\" && (a.confidence === \"guess\" || Boolean(a.flag));\n}\n\n/** A column's progress over its answers (null ones count as not asked). */\nexport function summarizeAnswers(answers, total = answers.length) {\n const out = { total, done: 0, toCheck: 0, waiting: 0, looking: 0, queued: 0, stale: 0, blocked: 0 };\n for (const a of answers) {\n const st = answerState(a);\n if ([\"found\", \"checked\", \"typed\", \"not_found\", \"two_answers\", \"failed\"].includes(st)) out.done += 1;\n if (needsCheck(a)) out.toCheck += 1;\n if (st === \"waiting\") out.waiting += 1;\n if (st === \"working\") out.looking += 1;\n if (st === \"queued\") out.queued += 1;\n if (st === \"stale\") out.stale += 1;\n if (st === \"blocked\") out.blocked += 1;\n }\n return out;\n}\n\n/** The progress line under a column's name: \"138 of 480 · 3 to check\". */\nexport function progressWords(p, { lookingWord = \"looking\" } = {}) {\n const parts = [`${p.done.toLocaleString(\"en-US\")} of ${p.total.toLocaleString(\"en-US\")}`];\n if (p.toCheck) parts.push(`${p.toCheck} to check`);\n if (p.looking) parts.push(`${p.looking} ${lookingWord}`);\n if (p.waiting) parts.push(`${p.waiting} waiting`);\n if (p.stale) parts.push(`${p.stale} out of date`);\n return parts.join(\" · \");\n}\n\n/** Split `text` around the occurrences of `mark` (the answer inside its\n * quote), for highlighting: [{ text, marked }]. */\nexport function markWords(text, mark) {\n const t = String(text ?? \"\");\n const m = String(mark ?? \"\").trim();\n if (!t || !m || m.length > 60) return [{ text: t, marked: false }];\n const lower = t.toLowerCase();\n const needle = m.toLowerCase();\n const out = [];\n let at = 0;\n for (;;) {\n const i = lower.indexOf(needle, at);\n if (i < 0) break;\n if (i > at) out.push({ text: t.slice(at, i), marked: false });\n out.push({ text: t.slice(i, i + m.length), marked: true });\n at = i + m.length;\n }\n if (at < t.length) out.push({ text: t.slice(at), marked: false });\n return out.length ? out : [{ text: t, marked: false }];\n}\n\n/** A URL without its scheme and \"www.\", for reading (\"klarwerk.de/careers\"). */\nexport function shortUrl(url) {\n return String(url ?? \"\")\n .replace(/^https?:\\/\\//i, \"\")\n .replace(/^www\\./i, \"\")\n .replace(/\\/$/, \"\");\n}\n\n/** The words on a page with the deciding ones marked: `[[…]]` in the\n * words marks them (the agent marks what decides the answer), else the\n * answer's value where it appears (markWords). → [{ text, marked }] */\nexport function decidingWords(text, mark) {\n const t = String(text ?? \"\");\n if (!/\\[\\[[^\\]]+\\]\\]/.test(t)) return markWords(t, mark);\n const out = [];\n let at = 0;\n for (const m of t.matchAll(/\\[\\[([^\\]]+)\\]\\]/g)) {\n if (m.index > at) out.push({ text: t.slice(at, m.index), marked: false });\n out.push({ text: m[1], marked: true });\n at = m.index + m[0].length;\n }\n if (at < t.length) out.push({ text: t.slice(at), marked: false });\n return out;\n}\n\n/** The words with any `[[…]]` marks taken out, for a line of plain text. */\nexport function plainWords(text) {\n return String(text ?? \"\").replace(/\\[\\[([^\\]]+)\\]\\]/g, \"$1\");\n}\n"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"path": "registry/blocks/agent-column/README.md",
|
|
51
|
+
"type": "registry:file",
|
|
52
|
+
"target": "@components/makersclaw/blocks/agent-column/README.md",
|
|
53
|
+
"content": "# AgentColumn\n\nA column an agent fills for every record, cell by cell, and a person\nchecks. Each answer says what state it is in (queued, waits for another\ncolumn, looking up, found, not found, two answers, can't fill, failed,\nout of date, checked or typed by a person) and how sure the agent is,\nas a mark that differs in shape, never colour alone: filled is **sure**,\nhalf is **likely**, a dashed ring is a **guess**, a tick is a person's.\nIts detail shows where the answer came from, the words on the page with\nthe answer marked in them, how it was found, both answers when two\nsources disagree, what was tried when nothing was found, and what a\nperson can do about it.\n\nBorn in Data Enrichment (0.5.0), moved into the template with the app\nkit (D15), written for any app whose agent fills or suggests values one\nrecord at a time: a CRM filling in a contact, a base whose agent fills a\nfield or proposes a cleaner value, a research list. For one field on the\nrecord you have open, use `suggestion`; for one source outside a column,\n`source-receipt` (a clipping from this block).\n\n```jsx\nimport { AnswerCell, AnswerDetail, ColumnHeader, ColumnPicker, SureLegend, summarizeAnswers } from \"../components/blocks/agent-column\"; // a Makersclaw app (\"./blocks/agent-column\" from components/)\nimport { AnswerCell } from \"@/components/makersclaw/blocks/agent-column\"; // a shadcn project\n```\n\nClient components, plus server-safe helpers (`model.js`: `answerState`,\n`answerWords`, `needsCheck`, `summarizeAnswers`, `progressWords`,\n`markWords`, `shortUrl`). No npm packages. It requires `agent-marks`\n(laid with it), whose diamond is the agent's mark here too.\n\n## The answer\n\nOne plain object per cell. Map your rows into it; the block never reads\nyour database.\n\n```js\n{\n id: \"c-42\",\n status: \"found\", // queued · working · found · not_found · two_answers · failed · stale · suggested\n value: \"62\", // the answer\n detail: \"51–200 on LinkedIn\", // a line under it\n link: \"https://pennantpay.com\", // a URL, when the answer is one\n confidence: \"likely\", // sure · likely · guess\n source: { url: \"pennantpay.com/team\", title: \"Their team page\", evidence: \"62 people\" },\n other: { value: \"70\", detail: \"in their own words\", source: { url, title, evidence } }, // two_answers\n note: \"Read their LinkedIn page and their team page.\", // how it was found; what was tried\n tried: [{ title, url, evidence }], // what it read, for \"not found\"\n flag: \"may_not_fit\", // your own word for \"look at this\" on a found answer\n setBy: \"agent\", // \"person\": typed by a person, never overwritten\n checked: false, // a person accepted it\n looking: \"reading klarwerk.de/team\", // while working\n waitsFor: [\"Domain\"], // queued, waiting for other columns\n blockedBy: [\"Head of Compliance\"], // queued, needs a column that wasn't found\n waitsForRow: false, // queued, reads the whole record and goes last\n hint: \"try their careers page\", // the person's words when asking again\n}\n```\n\n`null` is \"not asked yet\". `needsCheck(answer)` is the one rule for \"a\nperson should look at this\": not accepted, and not found, two answers,\nfailed, a guess, flagged, or a suggestion.\n\n## The pieces\n\n- **`AnswerCell`** `{ answer, type, onOpen, onRetry, lines, label }` —\n the cell. `type` text · link · email · number · domain (the last four\n in the mono face). `onOpen` makes it a button (open your detail);\n `onRetry` adds \"Try again\" to a failed one. Two answers and flagged\n answers get a warning tint. Use it in `data-table`'s `cell(row)`, in a\n list, or through `answerColumnType` in a spreadsheet.\n- **`SureMark`** `{ confidence, checked, typed }` and **`SureLegend`** —\n the mark alone (`role=\"img\"` with its word), and the key to the marks.\n- **`ColumnHeader`** `{ name, progress, agent, note }` — the name and the\n live line under it (\"138 of 480 · 3 to check · 1 waiting\", \"to check\"\n in the warning tone). `progress` is `summarizeAnswers(answers, total)`.\n `agent={false}` for a person's own column (\"yours\", or `note`).\n- **`AnswerDetail`** — the answer in full, for your `Sheet`: `answer`,\n `type`, `title` and `subtitle` (the record), `needs` (`[{ name, answer\n }]`, what it read), `history` (`[{ at, words, credits }]`), `askCost`,\n `placeholder`, `hintPlaceholder`, `note`, `busy`, `children` (your own\n choices under it: a record's Keep / Skip / Remove). The actions show\n when you pass their callback and the state allows them: `onAccept()`,\n `onPick(\"one\" | \"other\")`, `onType(value)`, `onAskAgain(hint)`,\n `onKeepNotFound()`, `onKeep()` / `onDrop()` (a suggestion).\n- **`Source`** `{ source, mark }` — a source's title and address, and its\n words in the display face with `mark` highlighted where it appears.\n- **`ColumnPicker`** — choose the columns to add: `recipes` (`[{ key,\n name, description, credits, input?: \"role\" | \"instructions\", defaultRole,\n defaultAsk, defaultExample, checked }]`), `rows` (for the total),\n `heading`, `noun`, `unit`, `ownCredits`, `answerTypes`, `busy`,\n `submitLabel(n)`, `onSubmit(chosen)` with `[{ recipe, name, ask,\n example, answerType, options, credits }]`. It also offers \"Your own\n question\" with the kind of answer.\n- **`answerColumnType`** — the spreadsheet type (`types={{ answer:\n answerColumnType }}`, a column `{ type: \"answer\", answerType }`). Typing\n in the cell makes the value the person's (`setBy: \"person\"`).\n\n## Example\n\n```jsx\n\"use client\";\nimport { DataTable } from \"./blocks/data-table\";\nimport { AnswerCell, ColumnHeader, summarizeAnswers } from \"./blocks/agent-column\";\n\nexport default function Companies({ rows, open }) {\n const headcount = rows.map((r) => r.answers.headcount);\n return (\n <DataTable\n rows={rows}\n columns={[\n { key: \"name\", header: \"Company\" },\n {\n key: \"headcount\",\n header: <ColumnHeader name=\"Headcount\" progress={summarizeAnswers(headcount, rows.length)} />,\n cell: (r) => <AnswerCell answer={r.answers.headcount} type=\"number\" onOpen={() => open(r, \"headcount\")} label={`Headcount of ${r.name}`} />,\n },\n ]}\n />\n );\n}\n```\n\n## The receipt (0.5.0, from Data Enrichment's v2 design)\n\nA second way to draw the same answers, for a dense grid and a panel\nbeside it. Colours are the theme's slots, each with a shape and a word:\n`chart-1` sure (filled dot), `chart-2` likely (half), `chart-3` guess\n(dashed ring, never shown as verified), `success` a person's check (✓),\n`chart-5` typed by a person (✎), `warning` two answers or a flag (split\nsquare), `info` looking up (a bar), `destructive` failed (▲), muted for\nqueued, waiting, can't fill, not found and out of date.\n\n```jsx\nimport { AnswerLine, JobHeader, Receipt, ReceiptActions, Clipping, TrustMark } from \"./blocks/agent-column\";\n```\n\n- **`AnswerLine`** `{ answer, onRetry, queuedWord }` — one line for a\n 32px cell: the value at the start, its trust at the end (\"pennantpay.com\n ● sure\", \"62 ◐ ✓\", \"85 · 70 ▣ 2 answers\", \"⊘ not found\", \"◌ can't\n fill · needs Head of Compliance\", \"▲ failed · didn't respond Try\n again\"). A flagged answer (`may_not_fit`, `left`) reads in the warning\n tone.\n- **`TrustMark`** `{ kind, word, check }` — the mark and its word:\n `sure` · `likely` · `guess` · `two` · `warn` · `looking` · `wait` ·\n `none` · `failed` · `typed` · `sent` · `stale` · `quiet`.\n- **`AgentMark`** — the platform's agent mark (a filled diamond, from\n `agent-marks`), re-exported here for what the agent did.\n- **`JobHeader`** `{ name, agent, source, progress, part, cost, menu }` —\n a column heading as a job: the mark and the name with its menu, then a\n bar (`part` 0 to 1), the words (\"21 of 22 · 3 to check\") and the price\n (\"40 cr/row\"). `agent={false}` draws a person's column, `source` the line\n under it (\"yours · summit-leads.csv\"; default \"yours\").\n- **`Receipt`** `{ answer, title, question, where, onPick, pickWords,\n picked, read, by, valueLabel, stateWords, leftTitle, busy, children }`\n — the answer opened: its head, the value big with its state in words,\n then the sources as clippings. Two answers are two clippings with Pick\n on each; a person who left shows the page that says so and, under\n `leftTitle`, who holds the role now; not found and failed list what was\n tried, crossed off. `children` go under it (your actions).\n- **`Clipping`** `{ source: { url, title, evidence, read }, trust, mark,\n title, action, footnote, picked }` — one source: the title, the address\n and date, the words on the page with the deciding words underlined in\n the accent. Mark them in the words with `[[…]]` (\"[[51–200\n employees]] · [[85]] members\"); without marks the answer's value is\n underlined where it appears (`decidingWords`).\n- **`TriedList`** `{ tried }` — the pages read, crossed off.\n- **`ReceiptActions`** `{ onType, typePlaceholder, onAskAgain,\n hintPlaceholder, askCost, askNote, askLabel, hint, busy }` — \"Type my\n own\" (yours, never overwritten) and \"Ask again\" with a hint, its price\n said under it.\n\n```jsx\n<Receipt answer={a} title=\"Klarwerk\" question=\"Headcount\" where=\"row 2\" read=\"read 22 Sep\"\n onPick={(which) => pick(a.id, which)}\n by={<><AgentMark /> Found by the agent · 22 Sep, 10:31</>}>\n <ReceiptActions onType={(v) => type(a.id, v)} onAskAgain={(h) => askAgain(a.id, h)} askCost=\"Asking again costs about 40 credits\" />\n</Receipt>\n```\n\n## For a second app\n\n- **Suggested values on cells** (a base's cleaner value, a CRM's filled-in\n title): give the answer `status: \"suggested\"`, the proposed `value`,\n its `source`, and pass `onKeep` / `onDrop` to `AnswerDetail`; `needsCheck`\n counts it. When a person changed the value since, mark it `stale`.\n- **A person's value wins**: map a typed value to `setBy: \"person\"`; the\n cell shows a tick and `AnswerDetail` offers no ask-again over it.\n- **Waiting and can't-fill** are yours to compute (which of your columns\n an answer reads), passed as `waitsFor` / `blockedBy` names.\n- The block draws no toast and no undo: your callbacks save, and your\n toast offers Undo.\n\n## What it will not do\n\n- Fetch, save or start the agent. Callbacks out; `useJob` is yours.\n- Decide what is sure. The agent says; the mark draws it.\n- Colour a state alone: every state has its words, every mark its shape.\n"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"categories": [
|
|
57
|
+
"enrichment",
|
|
58
|
+
"column",
|
|
59
|
+
"cell",
|
|
60
|
+
"answer",
|
|
61
|
+
"source",
|
|
62
|
+
"citation",
|
|
63
|
+
"confidence",
|
|
64
|
+
"suggestion",
|
|
65
|
+
"proposal",
|
|
66
|
+
"fill",
|
|
67
|
+
"research",
|
|
68
|
+
"table",
|
|
69
|
+
"spreadsheet",
|
|
70
|
+
"keep",
|
|
71
|
+
"drop",
|
|
72
|
+
"receipt",
|
|
73
|
+
"clipping",
|
|
74
|
+
"trust",
|
|
75
|
+
"job",
|
|
76
|
+
"heading"
|
|
77
|
+
],
|
|
78
|
+
"docs": "Installed to components/makersclaw/blocks/agent-column. Import: import { AnswerCell, AnswerDetail, ColumnHeader, SureMark, SureLegend, Source, AgentMark, AnswerLine, Clipping, JobHeader, Receipt, ReceiptActions, TriedList, TrustMark, ColumnPicker, answerColumnType, answerState, answerWords, needsCheck, summarizeAnswers, progressWords, markWords, decidingWords, plainWords, shortUrl, CONFIDENCE } from \"@/components/makersclaw/blocks/agent-column\". Props and an example: components/makersclaw/blocks/agent-column/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
79
|
+
"meta": {
|
|
80
|
+
"makersclaw": {
|
|
81
|
+
"version": "0.8.1",
|
|
82
|
+
"useWhen": "An app whose agent fills or proposes values record by record and a person checks them: enrichment of a list, a CRM filling in a contact, a base whose agent fills a field or suggests a cleaner value, a research table. Not for a single agent reply (job-panel) or a person's own typing (spreadsheet, record-form)."
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "agent-dock",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "AgentDock",
|
|
6
|
+
"description": "Where an app's agent lives, as the desk's agent sidebar does: a column on the right beside the work, under the top bar, with the content reflowing beside it. A quarter of the body until the member drags the divider (never under 280 px, never over half), the width and the open state remembered in this browser per app; the divider is a separator a keyboard can move (arrows nudge, Home goes back to a quarter, End to the widest). Where the body is under 900 px (a container measure, not the window) the dock becomes a sheet laid over the app from the right, folded by Escape, and the chat does not remount on the way. It is the host only: the pane inside draws its own head (title, New chat, Chats, close) and its empty state (past chats listed, suggestions, the box); the dock tells the pane how to close. The pane stays mounted once opened, so a turn in flight and a draft survive folding. It sits in the `dock` slot of the kit's AppLayout. useAgentDock() holds the open state for the app's Ask button, remembered per app, and puts the cursor in the box when the member opens it. openAgent({ ask }) is how every \"have the agent do it\" button works (X9): it opens the dock with the agent's first line already sent in a new chat (or put in the box), so the agent asks only for what it is missing, in the chat, and no form comes first.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/agent-pane.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/agent-dock/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/agent-dock/index.js",
|
|
17
|
+
"content": "// The AgentDock block — `makersclaw add agent-dock`. Fixed: compose it\n// through its props (README.md beside this file). The model's helpers are\n// plain functions; they work on the server too.\nexport { AgentDock, openAgent, useAgentDock } from \"./agent-dock\";\nexport { COLUMN_AT, DEFAULT_SHARE, MIN_W, OPEN_EVENT, STEP, carriesLine, clampWidth, columnWidth, dockKeys, dragged, hasRoom, maxWidth, nudged, openRequest, storedWidth } from \"./dock-model\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/agent-dock/agent-dock.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/agent-dock/agent-dock.jsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cx } from \"../../ui\";\nimport { AgentDockContext, AgentPane } from \"../agent-pane\";\nimport { COLUMN_AT, MIN_W, OPEN_EVENT, carriesLine, columnWidth, dockKeys, dragged, hasRoom, nudged, openRequest, storedWidth } from \"./dock-model\";\n\n// AgentDock — where the app's agent lives: a column on the right, beside the\n// work, like the desk's own agent sidebar (2026-10-12). The founder: \"why are\n// we still using this weak ai sidebar component?\" The pane was the desk's\n// sidebar, but every app invented its own host for it (a modal sheet that\n// dimmed the work, an inspector tab), so none felt like the sidebar. This is\n// the host, once.\n//\n// const dock = useAgentDock({ storageKey: \"crm\" }); // dock.open, dock.toggle\n// <AppLayout dock={<AgentDock dock={dock}><AskPanel /></AgentDock>} …>…</AppLayout>\n// <Button onClick={dock.toggle}>Ask</Button>\n//\n// It draws the head-less shell around the pane: the column (a quarter of the\n// body until dragged, never under MIN_W, never over half), the divider that\n// is dragged or nudged with the arrow keys (width kept in this browser, per\n// app), the work reflowing beside it, and a sheet laid over the app where the\n// body has no room for both. The pane inside draws its own head (title, New\n// chat, Chats, close); the dock only tells it how to close, through\n// AgentDockContext. The chat stays mounted once opened, so a turn in flight\n// and a draft survive folding the column.\n//\n// A button that says the agent will do the work calls openAgent(\"Draft a\n// project plan for me\") (X9): the dock opens and the pane sends that line in\n// a new chat, and the agent asks only for what it is missing. Never a form\n// first.\n\nconst readKey = (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n};\nconst writeKey = (key, value) => {\n try {\n if (value === null) localStorage.removeItem(key);\n else localStorage.setItem(key, value);\n } catch {\n /* private mode, blocked storage */\n }\n};\n\n/**\n * Whether the dock is open, remembered in this browser per app.\n *\n * storageKey the app (a short word: \"crm\"); the open state and the\n * width are kept under it\n * defaultOpen until the member has chosen (default false)\n * listen hear openAgent() calls from anywhere on the page (default\n * true; the one dock in the shell is the one that listens)\n *\n * Returns { open, setOpen(bool), toggle(), openAgent(ask), storageKey,\n * asked, start, startTaken }. `asked` counts the member's opens: the dock\n * puts the cursor in the box when it rises, never on a page load that\n * restores an open dock. `start` is the starting line an openAgent call\n * left for the pane, until the pane takes it (`startTaken`).\n */\nexport function useAgentDock({ storageKey = \"app\", defaultOpen = false, listen = true } = {}) {\n const [open, setOpenState] = useState(defaultOpen);\n const [asked, setAsked] = useState(0);\n const [start, setStart] = useState(null);\n const now = useRef(open);\n now.current = open;\n // The stored choice is read after the first paint, so the server's and the\n // browser's first render agree.\n useEffect(() => {\n const v = readKey(dockKeys(storageKey).open);\n if (v === \"1\") setOpenState(true);\n else if (v === \"0\") setOpenState(false);\n }, [storageKey]);\n const setOpen = useCallback(\n (next) => {\n const v = Boolean(typeof next === \"function\" ? next(now.current) : next);\n now.current = v;\n setOpenState(v);\n if (v) setAsked((n) => n + 1);\n writeKey(dockKeys(storageKey).open, v ? \"1\" : \"0\");\n },\n [storageKey],\n );\n const toggle = useCallback(() => setOpen(!now.current), [setOpen]);\n // \"Have the agent do it\": open, and leave the pane its first line.\n const openWith = useCallback(\n (request) => {\n setOpen(true);\n if (carriesLine(request)) setStart(request);\n },\n [setOpen],\n );\n const openHere = useCallback((input) => openWith(openRequest(input)), [openWith]);\n const startTaken = useCallback((nonce) => setStart((s) => (s && s.nonce === nonce ? null : s)), []);\n useEffect(() => {\n if (!listen || typeof window === \"undefined\") return undefined;\n const on = (e) => {\n const d = e.detail;\n if (!d || (d.storageKey && d.storageKey !== storageKey)) return;\n d.handled = true;\n openWith(d);\n };\n window.addEventListener(OPEN_EVENT, on);\n return () => window.removeEventListener(OPEN_EVENT, on);\n }, [listen, storageKey, openWith]);\n return { open, setOpen, toggle, openAgent: openHere, storageKey, asked, start, startTaken };\n}\n\n/**\n * \"Have the agent do it\": open the agent with its first line already in\n * hand. Call it from anywhere on the page (a button on a screen, a card's\n * action); the app's one dock hears it.\n *\n * openAgent(\"Draft a project plan for me\")\n * openAgent({ ask: \"Draft a project plan for …\", send: false })\n *\n * The line is sent as the member's first words in a new chat, and the agent\n * asks for only what it is missing, in the chat; it then does the work with\n * the app's own actions. `send: false` puts the line in the box instead, to\n * finish. `fresh: false` carries on in the open chat. `storageKey` aims at\n * one app's dock. → true when a dock took it; false when the page has none\n * (the caller's own fallback, e.g. a form, may run then).\n *\n * With no line (`openAgent()`, or `{ ask: \"\" }`) it only opens the dock: it\n * sends nothing and starts no chat. That is the one way to open the agent\n * from a screen far from the shell (a \"Talk to the agent\" link, an empty\n * state). Ask in the top bar toggles; this only ever opens.\n */\nexport function openAgent(input) {\n if (typeof window === \"undefined\") return false;\n const detail = { ...openRequest(input), handled: false };\n window.dispatchEvent(new CustomEvent(OPEN_EVENT, { detail }));\n return detail.handled;\n}\n\n/**\n * dock what useAgentDock() returns: the open state, its setter and\n * the storage key. Or pass the parts:\n * open, onOpenChange the dock is yours to open and close (controlled)\n * storageKey where the width is kept (default \"app\", or dock's)\n * label the column's name for a screen reader (default \"Agent\")\n * columnAt the body width in px from which it is a column beside the\n * work; under it, a sheet over the app (default 900)\n * children the pane: your own component over AgentPane (the app's\n * yes card, its title). Without children the dock draws an\n * AgentPane from chat and the props below\n * chat, title, suggestions, empty, placeholder, yes, mark, onOpen, recent\n * the AgentPane's own (see agent-pane)\n * className the column's own layout. Never colour.\n */\nexport function AgentDock({\n dock,\n open: openProp,\n onOpenChange,\n storageKey: keyProp,\n label = \"Agent\",\n columnAt = COLUMN_AT,\n children,\n chat,\n title,\n suggestions,\n empty,\n placeholder,\n yes,\n mark,\n onOpen,\n recent,\n className,\n}) {\n const storageKey = keyProp ?? dock?.storageKey ?? \"app\";\n // Without a dock from the shell the column keeps its own state (and hears\n // openAgent); with one, the shell's hook is the one that listens.\n const own = useAgentDock({ storageKey, listen: !dock });\n const open = Boolean(openProp ?? dock?.open ?? own.open);\n const setOpen = onOpenChange ?? dock?.setOpen ?? own.setOpen;\n const asked = (dock ?? own).asked;\n const start = (dock ?? own).start ?? null;\n const startTaken = (dock ?? own).startTaken;\n // A controlled dock (open + onOpenChange, no dock) is told when openAgent\n // asks it to open.\n const ownAsked = useRef(own.asked);\n useEffect(() => {\n if (ownAsked.current === own.asked) return;\n ownAsked.current = own.asked;\n if (!dock && openProp !== undefined) onOpenChange?.(true);\n }, [own.asked, dock, openProp, onOpenChange]);\n\n const asideRef = useRef(null);\n const bodyRef = useRef(null);\n const [body, setBody] = useState(0);\n const [width, setWidth] = useState(null);\n const widthRef = useRef(null);\n const [mounted, setMounted] = useState(open);\n const column = hasRoom(body, columnAt);\n\n useEffect(() => {\n if (open) setMounted(true);\n }, [open]);\n\n // The width this browser kept for this app.\n useEffect(() => {\n const w = storedWidth(readKey(dockKeys(storageKey).width));\n widthRef.current = w;\n setWidth(w);\n }, [storageKey]);\n const keepWidth = useCallback(\n (w) => {\n widthRef.current = w;\n setWidth(w);\n writeKey(dockKeys(storageKey).width, w === null ? null : String(w));\n },\n [storageKey],\n );\n\n // The body it sits beside (the frame's row), measured, never the window.\n useEffect(() => {\n const el = asideRef.current?.parentElement;\n if (!el) return undefined;\n bodyRef.current = el;\n const measure = () => setBody(el.clientWidth);\n measure();\n if (typeof ResizeObserver === \"undefined\") return undefined;\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n // The member opened it: the cursor goes to the box. The pane mounts on the\n // same render that opens the column, so the box may not exist for a frame or\n // two: try on each frame until it does (and give up after a second). What\n // had focus (the Ask button) is remembered, and folding the dock gives focus\n // back to it.\n const firstAsk = useRef(asked);\n const opener = useRef(null);\n useEffect(() => {\n if (asked === firstAsk.current || !open) return undefined;\n firstAsk.current = asked;\n const here = document.activeElement;\n if (here && here !== document.body && !asideRef.current?.contains(here)) opener.current = here;\n let id = 0;\n let tries = 0;\n const attempt = () => {\n const box = asideRef.current?.querySelector(\"textarea:not([disabled])\");\n if (box) {\n box.focus({ preventScroll: true });\n if (document.activeElement === box) return;\n }\n if (++tries < 60) id = requestAnimationFrame(attempt);\n };\n id = requestAnimationFrame(attempt);\n return () => cancelAnimationFrame(id);\n }, [asked, open]);\n const wasOpen = useRef(open);\n useEffect(() => {\n const was = wasOpen.current;\n wasOpen.current = open;\n if (!was || open) return;\n const back = opener.current;\n opener.current = null;\n const active = document.activeElement;\n // Only when focus was inside the dock (or lost): never steal it from\n // somewhere the member has since gone.\n if (back && back.isConnected && (!active || active === document.body || asideRef.current?.contains(active))) back.focus({ preventScroll: true });\n }, [open]);\n\n // A sheet lays over the app, so Escape folds it.\n useEffect(() => {\n if (!open || column) return undefined;\n const onKey = (e) => {\n if (e.key === \"Escape\" && !e.defaultPrevented) setOpen(false);\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, column, setOpen]);\n\n const bodyWidth = () => bodyRef.current?.clientWidth ?? body;\n const onPointerDown = (e) => {\n if (e.button !== 0) return;\n e.preventDefault();\n const b = bodyWidth();\n const startWidth = asideRef.current?.offsetWidth ?? columnWidth(widthRef.current, b);\n const startX = e.clientX;\n try {\n e.currentTarget.setPointerCapture(e.pointerId);\n } catch {\n /* a pointer the runtime does not know */\n }\n const onMove = (ev) => {\n const w = dragged(startWidth, startX, ev.clientX, b);\n widthRef.current = w;\n setWidth(w);\n };\n const onUp = () => {\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"pointerup\", onUp);\n window.removeEventListener(\"pointercancel\", onUp);\n keepWidth(widthRef.current);\n };\n window.addEventListener(\"pointermove\", onMove);\n window.addEventListener(\"pointerup\", onUp);\n window.addEventListener(\"pointercancel\", onUp);\n };\n const onKeyDown = (e) => {\n const b = bodyWidth();\n const next = nudged(e.key, asideRef.current?.offsetWidth ?? columnWidth(widthRef.current, b), b);\n if (next === undefined) return;\n e.preventDefault();\n keepWidth(next);\n };\n\n const ctx = useMemo(() => ({ close: () => setOpen(false), mode: column ? \"column\" : \"sheet\", start, taken: startTaken }), [setOpen, column, start, startTaken]);\n const pane =\n children ??\n (chat ? (\n <AgentPane chat={chat} title={title} suggestions={suggestions} empty={empty} placeholder={placeholder} yes={yes} mark={mark} onOpen={onOpen} recent={recent} />\n ) : null);\n\n return (\n <AgentDockContext.Provider value={ctx}>\n {open && column ? (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize the agent column\"\n aria-valuemin={MIN_W}\n aria-valuenow={width ?? undefined}\n title=\"Drag to resize · ← → to nudge · Home for a quarter\"\n tabIndex={0}\n onPointerDown={onPointerDown}\n onKeyDown={onKeyDown}\n className=\"group relative z-10 -mx-[3px] w-1.5 shrink-0 cursor-col-resize touch-none outline-none print:hidden\"\n >\n <span aria-hidden=\"true\" className=\"absolute inset-y-0 left-[2px] right-[2px] rounded-sm transition-colors group-hover:bg-border group-focus-visible:bg-ring group-active:bg-ring motion-reduce:transition-none\" />\n </div>\n ) : null}\n <aside\n ref={asideRef}\n aria-label={label}\n data-agent-dock={column ? \"column\" : \"sheet\"}\n data-open={open ? \"\" : undefined}\n style={column ? { width: \"clamp(280px, var(--agent-dock-w, 25%), 50%)\", \"--agent-dock-w\": width !== null ? `${width}px` : undefined } : undefined}\n className={cx(\n open ? \"flex\" : \"hidden\",\n \"min-h-0 min-w-0 flex-col border-l border-border bg-background text-foreground print:hidden\",\n column ? \"shrink-0\" : \"fixed inset-y-0 right-0 z-40 w-[min(100%,26rem)] shadow-xl\",\n className,\n )}\n >\n <div className=\"flex min-h-0 flex-1 flex-col\">{mounted ? pane : null}</div>\n </aside>\n </AgentDockContext.Provider>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/agent-dock/dock-model.js",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "@components/makersclaw/blocks/agent-dock/dock-model.js",
|
|
29
|
+
"content": "// agent-dock: the numbers and the keys, as plain functions (they work on the\n// server too, and test/agent-dock.test.mjs holds them).\n//\n// The dock is a column beside the work. Its width is the member's to drag:\n// never under MIN_W, never over half the body it sits in, a quarter of it\n// until dragged. Below `columnAt` px of body the column has no room and the\n// dock becomes a sheet laid over the app.\n\n/** The narrowest the column goes, px. */\nexport const MIN_W = 280;\n/** One press of an arrow key on the divider, px. */\nexport const STEP = 16;\n/** The body width from which the dock is a column beside the work (the\n * desk's own agent column uses 900). */\nexport const COLUMN_AT = 900;\n/** The share of the body the column takes until it is dragged. */\nexport const DEFAULT_SHARE = 0.25;\n\n/** The widest the column goes in a body this wide: half of it, never under MIN_W. */\nexport function maxWidth(body) {\n const b = Number(body);\n return Math.max(MIN_W, Number.isFinite(b) && b > 0 ? Math.floor(b / 2) : MIN_W);\n}\n\n/** A width kept inside its bounds, whole px. */\nexport function clampWidth(width, body) {\n const w = Number(width);\n if (!Number.isFinite(w)) return MIN_W;\n return Math.min(maxWidth(body), Math.max(MIN_W, Math.round(w)));\n}\n\n/** What a stored width is worth: a whole number of px at least MIN_W, else\n * null (the default). The CSS clamps it again whatever was stored. */\nexport function storedWidth(raw) {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const n = Number(raw);\n return Number.isFinite(n) && n >= MIN_W ? Math.round(n) : null;\n}\n\n/** The width the column has now: the dragged one, else a quarter of the\n * body, kept inside the same bounds the CSS uses. */\nexport function columnWidth(width, body) {\n const b = Number(body) > 0 ? Number(body) : 0;\n return clampWidth(width ?? b * DEFAULT_SHARE, b);\n}\n\n/** Is there room for a column beside the work? An unmeasured body (0) is\n * taken to have room: the first paint is never a sheet by mistake. */\nexport function hasRoom(body, columnAt = COLUMN_AT) {\n const b = Number(body);\n return !(Number.isFinite(b) && b > 0) || b >= columnAt;\n}\n\n/** The divider dragged: the pointer moving left widens the column. */\nexport function dragged(startWidth, startX, x, body) {\n return clampWidth(Number(startWidth) - (Number(x) - Number(startX)), body);\n}\n\n/** The divider's keys. Left widens, Right narrows, End opens it to the\n * widest, Home goes back to the default (null). `undefined` = not ours. */\nexport function nudged(key, now, body) {\n if (key === \"ArrowLeft\") return clampWidth(Number(now) + STEP, body);\n if (key === \"ArrowRight\") return clampWidth(Number(now) - STEP, body);\n if (key === \"End\") return maxWidth(body);\n if (key === \"Home\") return null;\n return undefined;\n}\n\n/** The two keys this browser keeps for one app's dock. */\nexport function dockKeys(storageKey) {\n const k = String(storageKey || \"app\").replace(/[^\\w.-]+/g, \"-\");\n return { open: `mc.dock.${k}.open`, width: `mc.dock.${k}.width` };\n}\n\n// ---- \"have the agent do it\" (X9) -------------------------------------------\n//\n// A button that says the agent will do the work does not open a form. It\n// opens the dock with the agent's first line already sent (or in the box),\n// and the agent asks only for what it is missing, in the chat. openAgent()\n// (agent-dock.jsx) is the call; this is its message.\n\n/** The window event openAgent() dispatches; every useAgentDock hears it. */\nexport const OPEN_EVENT = \"mc:agent-dock:open\";\n\nlet seq = 0;\n\n/**\n * What openAgent was asked, made whole. The argument is a line (\"Draft a\n * project plan for me\") or an object:\n *\n * ask the starting line\n * send true (the default): send it as the member's first words.\n * false: put it in the box for the member to finish\n * fresh true (the default): in a new chat, so it is its own thing\n * and not a follow-up. false: carry on in the open one\n * storageKey only the dock of that app (default: any dock on the page)\n *\n * → { ask, send, fresh, storageKey, nonce }. The line is kept as given (a\n * trailing space is where the member goes on typing); the pane trims what it\n * sends. The nonce is new every call: a pane takes a nonce once.\n */\nexport function openRequest(input, now = Date.now()) {\n const o = typeof input === \"string\" ? { ask: input } : (input ?? {});\n return {\n ask: String(o.ask ?? \"\"),\n send: o.send !== false,\n fresh: o.fresh !== false,\n storageKey: o.storageKey ? String(o.storageKey) : null,\n nonce: `${now}-${++seq}`,\n };\n}\n\n/** Does an openAgent call carry a line for the pane? With no line\n * (`openAgent()`, `{ ask: \"\" }`, a blank string) the dock only opens: nothing\n * is sent and no chat is started. */\nexport function carriesLine(request) {\n return Boolean(String(request?.ask ?? \"\").trim());\n}\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "registry/blocks/agent-dock/README.md",
|
|
33
|
+
"type": "registry:file",
|
|
34
|
+
"target": "@components/makersclaw/blocks/agent-dock/README.md",
|
|
35
|
+
"content": "# AgentDock\n\nWhere an app's agent lives (0.8.1): a column on the right, beside the work,\nlike the desk's own agent sidebar.\n\nThe founder, testing an app: \"why are we still using this weak ai sidebar\ncomponent? I thought we had created the original agent sidebar as component\nin the template\". We had: `agent-pane` is the desk's sidebar. What was\nmissing was its host. Every app invented one: a modal `Sheet` that dimmed\nthe work, an inspector tab, a card. So no two apps felt alike, and none felt\nlike the sidebar. This is the host, once.\n\n- A **column beside the content**, under the top bar and right of the rail.\n The work reflows beside it. Nothing is dimmed.\n- **Resizable.** A quarter of the body until dragged, never under 280 px,\n never over half. The divider is a separator a keyboard can move: arrows\n nudge it, Home goes back to a quarter, End to the widest.\n- **Remembered.** The open state and the width are kept in this browser, per\n app (`storageKey`), in `localStorage` with a `try/catch`; with no storage\n it still works.\n- **A sheet where there is no room.** Where the body is under `columnAt`\n (900 px; a measure of the frame's own width, never the window) the dock is\n a sheet laid over the app from the right. Escape folds it. The chat is not\n remounted on the way: a turn in flight survives a resize.\n- **The pane keeps its head.** The `agent-pane` inside draws title, New chat,\n Chats and the close; its empty state lists the past chats (or \"No chats\n yet\"), then the app's suggestions, then the box: the desk's message box\n (the `chat-pane` block's), with the words on top and + to attach and a\n round send under them. The dock only tells the pane how to close, and\n hands it a starting line when a \"have the agent do it\" button asked for one.\n- **Stays mounted once opened**, so a draft and a running turn survive\n folding the column.\n\n```jsx\nimport { AgentDock, useAgentDock } from \"../components/blocks/agent-dock\"; // a Makersclaw app (\"./blocks/agent-dock\" from components/)\nimport { AgentDock, useAgentDock } from \"@/components/makersclaw/blocks/agent-dock\"; // a shadcn project\n```\n\nA client component. It needs the kit's `AppLayout` with its `dock` slot\n(0.8.1), or any flex row it is the last child of.\n\n## Adopting it in a shell (a few lines)\n\nThe shell is the app's one `AppLayout`. Give it the dock, and make every Ask\nbutton open it: the top bar's own Ask does that when the shell wraps the\nlayout in `AskProvider` (blocks/top-bar).\n\n```jsx\n\"use client\";\nimport { AppLayout } from \"../components/ui\";\nimport { AgentDock, useAgentDock } from \"../components/blocks/agent-dock\";\nimport { AskProvider } from \"../components/blocks/top-bar\";\nimport { AskPanel } from \"../components/ask-panel\"; // the app's own pane over AgentPane\n\nexport default function Shell({ children, budget }) {\n const dock = useAgentDock({ storageKey: \"crm\" }); // { open, setOpen, toggle }\n return (\n <AskProvider dock={dock} allowance={{ used: budget.used, cap: budget.cap }} findEvent=\"crm:find\">\n <AppLayout\n navigation={<Rail />}\n dock={\n <AgentDock dock={dock} label=\"Ask the agent\">\n <AskPanel /> {/* useAgentPane({ row }) inside, AgentPane (not bare) */}\n </AgentDock>\n }\n >\n {children} {/* each screen draws its own <TopBar crumb=… action=…/> */}\n </AppLayout>\n </AskProvider>\n );\n}\n```\n\nDelete the shell's old `<Sheet>` for the agent, and drop `bare` from the pane\n(the dock's pane has its own head).\n\n**One dock per app, in the shell.** A record's own pane (a meeting, a\nperson) is this same dock with a chat scoped to the open record: make the\n`AskPanel` read the record the page has open and pass it as\n`useAgentPane({ row })`. The thread follows the record; the column stays\nwhere it is. Do not add a second column.\n\nAlso set `\"agent_pane\": true` in the package's blueprint (see `agent-pane`)\nso the desk draws no second sidebar for the window.\n\n## Just open it\n\n**To open the dock and send nothing, call `openAgent()` with no line.** One\nway, from anywhere on the page:\n\n```jsx\nimport { openAgent } from \"../components/blocks/agent-dock\";\n\n<Button variant=\"link\" onClick={() => openAgent()}>Talk to the agent</Button>\n```\n\n- `openAgent()` and `openAgent({ ask: \"\" })` open the dock (column or sheet),\n put the cursor in the box, send nothing and start no chat. It returns\n `true` when a dock took it.\n- It only ever **opens**. The top bar's Ask **toggles** (it also closes);\n `useAsk()` is for reading whether the agent is open (`open`) and for that\n toggle, not for opening it from a screen.\n- From the shell, where `useAgentDock()` is held, `dock.setOpen(true)` is the\n same thing, and `dock.openAgent()` too. A screen far from the shell uses the\n exported `openAgent()`: do not write your own event, ref or context for it.\n- With a line, it is the next section: the dock opens and the pane sends it.\n\n## Have the agent do it\n\n**Every button that says the agent will do the work calls `openAgent`.** It\nnever opens a form, a route or a sheet first. The founder, on a project\nplanner whose \"Have the agent draft it\" first asked for a name, a code and\ntwo dates: \"the agent should be able to create these\".\n\n```jsx\nimport { openAgent } from \"../components/blocks/agent-dock\";\n\n<Button variant=\"primary\" icon=\"sparkles\" onClick={() => openAgent(\"Draft a project plan for me\")}>\n Have the agent draft it\n</Button>\n```\n\n`openAgent(\"…\")` opens the dock and hands the pane its first line. The pane\nstarts a new chat and sends the line as the member's first words; the agent\nasks only for what it is missing (the name, the target date), in the chat\nas a question the member answers there; then it creates the record through\nthe app's own actions. A form stays only for \"type it yourself\".\n\n- `openAgent(\"Draft a project plan for me\")`, or an object:\n- `ask`: the starting line. Say what the member wants done, in their words.\n Left out or empty, the dock only opens (\"Just open it\", above).\n- `send`: `true` (the default) sends it. `false` puts it in the box for the\n member to finish: `openAgent({ ask: \"Draft a project plan for \", send: false })`.\n- `fresh`: `true` (the default) is a new chat, so it is its own ask and not a\n follow-up to the last one. `false` carries on in the chat that is open.\n- `storageKey`: only that app's dock (default: any dock on the page, and an\n app has one).\n- It returns `true` when a dock took it and `false` when the page has none.\n Do not use the `false` to open a form: an app that has an agent has the dock\n in its shell (AppLayout's `dock` slot). `npm run check` (rule `agentfirst`)\n refuses a button whose words say the agent will do the work (\"Have the\n agent draft it\", \"Let the agent plan it\") when it submits a form, follows a\n link or only changes a step; it must call `openAgent` or ask the agent\n (`ask`, `chat.send`). A choice labelled that way inside a form fails too.\n\nFrom the shell, where `useAgentDock()` is held, `dock.openAgent(ask)` does the\nsame without the page event. A screen far from the shell uses the exported\n`openAgent`; a line the member is shown first (a chip, an ask bar) uses\n`chat.send` as before.\n\nThe agent's side: its brief names the app's create actions in\n`references/actions.md`, and the draft action takes its inputs as optional\n(`name`, `start`, `target`), so a line like \"Draft a project plan for me\"\nreaches it with nothing and it asks. Write the line so it carries what the\nscreen already knows (\"Draft a project plan for the Pinewood launch\").\n\n## Props\n\n- `dock`: what `useAgentDock()` returns (`open`, `setOpen`, `storageKey`,\n `asked`, `start`, `startTaken`). Or pass the parts:\n- `open`, `onOpenChange`: the dock is yours to open and close.\n- `storageKey`: where the width is kept (default the dock's, else `\"app\"`).\n- `label`: the column's name for a screen reader. Default `\"Agent\"`.\n- `columnAt`: body width in px from which it is a column. Default `900`.\n- `children`: the pane. Your own component over `AgentPane` (the app's yes\n card, its titles). Without children the dock draws an `AgentPane` from:\n- `chat`, `title`, `suggestions`, `empty`, `placeholder`, `yes`, `mark`,\n `onOpen`, `recent`: the `AgentPane`'s own props.\n- `className`: the column's layout. Never colour.\n\n### useAgentDock({ storageKey, defaultOpen, listen })\n\nReturns `{ open, setOpen(bool | fn), toggle(), openAgent(ask), storageKey,\nasked, start, startTaken }`. `listen` (default true) is whether it hears\n`openAgent()` calls from anywhere on the page; the shell's one hook is the\none that does. The\nchoice is read from storage after the first paint (so the server's and the\nbrowser's first render agree) and written when the member changes it.\n`defaultOpen` (false) holds until they choose. `asked` counts the member's\nopens: when it rises the dock puts the cursor in the box. A restored open\ndock never steals focus. Folding the dock gives focus back to what had it\nwhen it opened (the Ask button), unless the member has gone elsewhere.\n\n### The model (plain functions)\n\n`MIN_W`, `COLUMN_AT`, `maxWidth(body)`, `clampWidth(w, body)`,\n`storedWidth(raw)`, `columnWidth(width, body)`, `hasRoom(body, columnAt)`,\n`dragged(startWidth, startX, x, body)`, `nudged(key, now, body)`,\n`dockKeys(storageKey)`, `openRequest(input)`, `OPEN_EVENT`.\n\n## Example\n\n```jsx\n// Without an app-owned pane: the dock draws the AgentPane itself.\nconst dock = useAgentDock({ storageKey: \"notes\" });\nconst chat = useAgentPane();\n<AppLayout\n dock={<AgentDock dock={dock} chat={chat} title=\"Ask about your notes\" suggestions={[\"Summarise this week\"]} />}\n>\n …\n</AppLayout>;\n```\n\n## States drawn in the demo\n\nClosed (nothing drawn but the app); open as a column at 1100 px with past\nchats, and with \"No chats yet\"; dragged and nudged; a sheet at 700 px with\nEscape; a chat answering; \"Have the agent draft it\" on the page, which opens\nthe dock with its line already sent in a new chat (and \"Start a line\" beside\nit, which puts the line in the box instead).\n"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"categories": [
|
|
39
|
+
"dock",
|
|
40
|
+
"sidebar",
|
|
41
|
+
"column",
|
|
42
|
+
"ask",
|
|
43
|
+
"chat",
|
|
44
|
+
"copilot",
|
|
45
|
+
"resize",
|
|
46
|
+
"panel",
|
|
47
|
+
"sheet",
|
|
48
|
+
"shell"
|
|
49
|
+
],
|
|
50
|
+
"docs": "Installed to components/makersclaw/blocks/agent-dock. Import: import { AgentDock, useAgentDock, openAgent, COLUMN_AT, DEFAULT_SHARE, MIN_W, OPEN_EVENT, STEP, carriesLine, clampWidth, columnWidth, dockKeys, dragged, hasRoom, maxWidth, nudged, openRequest, storedWidth } from \"@/components/makersclaw/blocks/agent-dock\". Props and an example: components/makersclaw/blocks/agent-dock/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
51
|
+
"meta": {
|
|
52
|
+
"makersclaw": {
|
|
53
|
+
"version": "0.8.1",
|
|
54
|
+
"useWhen": "Any app that has an agent the member asks from the shell: the top bar's Ask, a chip that sends a question, a record whose Ask follows the open record. One dock per app, in the shell; the record's own pane is the same dock with a chat scoped to that record (useAgentPane({ row })), never a second column. Replaces a modal Sheet or an inspector tab as the agent's place. Not for work the member commissions from a verb and watches land (job-panel), and not for a conversation that fills a whole screen (agent-pane in a page of its own)."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/r/agent-kit.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "agent-kit",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "AgentKit",
|
|
6
|
+
"description": "What the app's agent knows and what it can do, inside the app: its skills (the app's own references, each with when the agent reads it, and the skills added on top, marked Added and, where so, Not installed) and its tools (the app's declared actions, by what they do). Read only; \"Manage in Settings\" (onManage) opens the window's Settings › Skills, where skills are added and removed. It draws what useAgentKit() returns (lib/hooks.js, GET _claw/agent).",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/agent-kit/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/agent-kit/index.js",
|
|
16
|
+
"content": "// The AgentKit block — `makersclaw add agent-kit`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { AgentKit } from \"./agent-kit\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/agent-kit/agent-kit.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/agent-kit/agent-kit.jsx",
|
|
22
|
+
"content": "\"use client\";\n\n// What the app's agent knows and what it can do, inside the app: its\n// skills (the app's own references, with when it reads each, and those\n// added on top, marked added and, where so, not installed) and its tools\n// (the app's declared actions). Props in, callbacks out; read only —\n// adding or removing a skill stays in the window's Settings › Skills,\n// which onManage opens. Tokens only. Its own width decides its layout\n// (@container): the two lists sit side by side from about 768 px.\n\nimport { Badge, Button, Icon, cx } from \"../../ui\";\n\nfunction Section({ icon, title, count, children }) {\n return (\n <section aria-label={title} className=\"grid min-w-0 content-start gap-1\">\n <h3 className=\"m-0 flex min-w-0 items-center gap-2 px-3.5 pt-3 text-sm font-semibold\">\n <Icon name={icon} size={14} className=\"shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{title}</span>\n {count ? <span className=\"text-xs font-normal tabular-nums text-muted-foreground\">{count}</span> : null}\n </h3>\n {children}\n </section>\n );\n}\n\n/**\n * skills [{ name, title, when, from: \"app\" | \"added\", installed }]\n * (useAgentKit's `skills`)\n * tools [{ name, description }] (useAgentKit's `tools`)\n * onManage opens the window's Settings › Skills; leave it out for none\n * manageLabel its words (default \"Manage in Settings\")\n * title the head (default \"The agent\"); null for none\n * empty { skills, tools }: what an empty list says\n */\nexport function AgentKit({ skills = [], tools = [], onManage, manageLabel = \"Manage in Settings\", title = \"The agent\", empty, className }) {\n const known = Array.isArray(skills) ? skills : [];\n const can = Array.isArray(tools) ? tools : [];\n return (\n <section aria-label={title ?? \"The agent\"} className={cx(\"@container grid min-w-0 overflow-hidden rounded-[var(--radius)] border border-border bg-card text-card-foreground\", className)}>\n {title || onManage ? (\n <header className=\"flex min-w-0 flex-wrap items-center gap-2 border-b border-border px-3.5 py-2\">\n {title ? <b className=\"min-w-0 flex-1 truncate text-sm font-semibold\">{title}</b> : <span className=\"flex-1\" />}\n {onManage ? (\n <Button size=\"sm\" variant=\"quiet\" icon=\"settings\" onClick={onManage}>\n {manageLabel}\n </Button>\n ) : null}\n </header>\n ) : null}\n <div className=\"grid min-w-0 divide-y divide-border @3xl:grid-cols-2 @3xl:divide-x @3xl:divide-y-0\">\n <Section icon=\"lightbulb\" title=\"What it knows\" count={known.length || null}>\n {known.length ? (\n <ul className=\"m-0 grid list-none p-0 pb-1.5\">\n {known.map((s) => (\n <li key={`${s.from}-${s.name}`} className=\"grid min-w-0 gap-0.5 px-3.5 py-2\">\n <span className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1\">\n <span className=\"min-w-0 truncate text-[13px] font-medium\">{s.title || s.name}</span>\n {s.from === \"added\" ? <Badge tone=\"info\">Added</Badge> : null}\n {s.from === \"added\" && !s.installed ? <Badge tone=\"warning\">Not installed</Badge> : null}\n </span>\n {s.when ? <span className=\"line-clamp-2 text-xs text-muted-foreground\">{s.when}</span> : null}\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"m-0 px-3.5 pb-3 text-[13px] text-muted-foreground\">{empty?.skills ?? \"Nothing of its own yet: it works from the app's instructions.\"}</p>\n )}\n </Section>\n <Section icon=\"wand\" title=\"What it can do\" count={can.length || null}>\n {can.length ? (\n <ul className=\"m-0 grid list-none p-0 pb-1.5\">\n {can.map((t) => (\n <li key={t.name} className=\"grid min-w-0 gap-0.5 px-3.5 py-2\">\n <span className=\"min-w-0 text-[13px]\">{t.description || t.name}</span>\n {t.description ? <code className=\"truncate text-[11.5px] text-muted-foreground\">{t.name}</code> : null}\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"m-0 px-3.5 pb-3 text-[13px] text-muted-foreground\">{empty?.tools ?? \"No actions declared yet: it reads, and asks before it writes.\"}</p>\n )}\n </Section>\n </div>\n </section>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/agent-kit/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/agent-kit/README.md",
|
|
28
|
+
"content": "# AgentKit\n\nWhat the app's agent knows and what it can do, shown inside the app.\nPart of the agent kit (0.8.1).\n\n- **What it knows** — its skills. The app's own references first, each\n with when the agent reads it (\"Before writing a follow-up\"); then the\n skills added on top, marked **Added**, and **Not installed** where one\n is not yet.\n- **What it can do** — its tools: the app's declared actions, by what\n they do (\"Move a deal to another stage\"), with the action's name\n under it.\n\nRead only. Adding or removing a skill is the window's Settings › Skills;\n`onManage` opens it (\"Manage in Settings\").\n\n```jsx\nimport { AgentKit } from \"../components/blocks/agent-kit\"; // a Makersclaw app\nimport { AgentKit } from \"@/components/makersclaw/blocks/agent-kit\"; // a shadcn project\n```\n\nA client component. Its own width decides its layout: the two lists sit\nside by side from about 768 px, one over the other below.\n\n## Props\n\n- `skills` — useAgentKit's `skills`: `[{ name, title, when, from: \"app\"\n | \"added\", installed }]`.\n- `tools` — useAgentKit's `tools`: `[{ name, description }]` (only the\n actions the agent may run; one declared `\"agent\": false` is left out).\n- `onManage` — opens the window's Settings › Skills. Leave it out for no\n button. `manageLabel` — its words (default \"Manage in Settings\").\n- `title` — the head (default \"The agent\"); `null` for none.\n- `empty` — `{ skills, tools }`: what an empty list says.\n- `className` — layout around it. Never colour.\n\n## useAgentKit (lib/hooks.js)\n\n```js\nconst { skills, tools } = useAgentKit();\n```\n\nIt reads `GET _claw/agent` once: `own_skills` (`{ name, title, when }`,\nfrom the app's `references/*.md`), `skills` (added on top, `{ name,\ninstalled }`) and `actions` (`{ name, description, agent }`, the\ndeclared actions). On a claw from before `own_skills` and `actions` it\nanswers empty lists: the block says so plainly, never an error.\n\n## Who may do what\n\nAny member reads it. Adding or removing a skill: the window's Settings ›\nSkills, on the desk, by whoever may change the app there.\n\n## Never\n\n- Never an Add or Remove of a skill here: that is the desk's.\n- Never the agent's instructions (its brief) word for word: they are\n for the agent. Say what it knows by its skills' titles.\n- Never a tool the app does not declare: the list is the declared\n actions, nothing written by hand.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useAgentKit } from \"../lib/hooks\";\nimport { AgentKit } from \"../components/blocks/agent-kit\";\n\nexport default function AboutTheAgent({ openSettings }) {\n const { skills, tools } = useAgentKit();\n return <AgentKit title=\"What the CRM's agent knows\" skills={skills} tools={tools} onManage={openSettings} />;\n}\n```\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"skills",
|
|
33
|
+
"tools",
|
|
34
|
+
"actions",
|
|
35
|
+
"capabilities",
|
|
36
|
+
"knowledge",
|
|
37
|
+
"about",
|
|
38
|
+
"settings",
|
|
39
|
+
"help",
|
|
40
|
+
"kit"
|
|
41
|
+
],
|
|
42
|
+
"docs": "Installed to components/makersclaw/blocks/agent-kit. Import: import { AgentKit } from \"@/components/makersclaw/blocks/agent-kit\". Props and an example: components/makersclaw/blocks/agent-kit/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
43
|
+
"meta": {
|
|
44
|
+
"makersclaw": {
|
|
45
|
+
"version": "0.8.1",
|
|
46
|
+
"useWhen": "An app shows its member what its agent is for and may do: an About the agent page, a side card beside the agent's chat, a settings screen's Agent section, a help panel (\"What can I ask?\"). For the agent's runs use run-history; for its schedule use schedule-card; for the chat itself use agent-chat."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "agent-marks",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "AgentMarks",
|
|
6
|
+
"description": "The platform's two marks and its byline: AgentMark, a filled diamond for the agent's work (it used tools, read and wrote, it may cost more than one call); CallMark, a sparkle in a dashed ring for one model call (a Summarize, a Shorter); and Byline, who did a thing in words (\"By the agent, on Priya's ask · 11:20\", \"By one model call\", \"By Owen\"). The two differ in shape, never colour alone, and each names itself to a screen reader when no words stand beside it.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/agent-marks/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/agent-marks/index.js",
|
|
16
|
+
"content": "// The AgentMarks block — `makersclaw add agent-marks`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { AgentMark, CallMark, Byline } from \"./agent-marks\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/agent-marks/agent-marks.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/agent-marks/agent-marks.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { Icon, cx } from \"../../ui\";\n\n// The two marks every app draws, one of each for the whole platform\n// (the app kit, D15). Before the kit the\n// redesigns drew the agent in six different glyphs; now:\n//\n// AgentMark a filled diamond: the agent's work. It used tools, read\n// and wrote, and it may cost more than one call.\n// CallMark a sparkle in a dashed ring: ONE model call, no\n// conversation and no tools (\"Shorter · 1 cr\").\n//\n// The difference is shape (filled vs dashed), never colour alone. Byline\n// says who did a thing in words: \"By the agent, on Priya's ask · 11:20\".\n// No data, no door: props in.\n\nconst PX = { xs: 10, sm: 12, md: 14, lg: 16 };\nconst px = (size) => (typeof size === \"number\" ? size : PX[size] ?? PX.sm);\n\n/** A mark that names itself when `label` is given, and is decoration\n * (hidden from a reader) when the words beside it already say it. */\nfunction marked(label) {\n return label ? { role: \"img\", \"aria-label\": label } : { \"aria-hidden\": true };\n}\n\n/**\n * label what a screen reader hears (\"The agent\"); leave it out when\n * the words beside the mark already say it\n * size xs · sm (default) · md · lg, or pixels\n * children words drawn beside the mark (\"Agent\")\n */\nexport function AgentMark({ label, size = \"sm\", className, children }) {\n const s = px(size);\n const mark = (\n <span {...(children ? { \"aria-hidden\": true } : marked(label))} className={cx(\"inline-flex shrink-0 text-primary\", !children && className)}>\n <Icon name=\"diamond\" size={s} className=\"fill-current\" />\n </span>\n );\n if (!children) return mark;\n return (\n <span className={cx(\"inline-flex min-w-0 items-center gap-1\", className)}>\n {mark}\n <span className=\"min-w-0\">{children}</span>\n </span>\n );\n}\n\n/**\n * label what a screen reader hears (\"One model call\"); leave it out\n * when the words beside the mark already say it\n * size xs · sm (default) · md · lg, or pixels\n * children words drawn beside the mark (\"1 call\")\n */\nexport function CallMark({ label, size = \"sm\", className, children }) {\n const s = px(size);\n const ring = s + 4;\n const mark = (\n <span\n {...(children ? { \"aria-hidden\": true } : marked(label))}\n style={{ width: ring, height: ring }}\n className={cx(\n \"inline-flex shrink-0 items-center justify-center rounded-full border border-dashed border-current text-muted-foreground\",\n !children && className,\n )}\n >\n <Icon name=\"sparkles\" size={Math.max(8, s - 3)} />\n </span>\n );\n if (!children) return mark;\n return (\n <span className={cx(\"inline-flex min-w-0 items-center gap-1\", className)}>\n {mark}\n <span className=\"min-w-0\">{children}</span>\n </span>\n );\n}\n\nconst ISO = /^\\d{4}-\\d{2}-\\d{2}T/;\n\n/** \"11:20\" for a moment today, \"12 Sep\" for another day; words as given. */\nfunction whenWords(at) {\n if (at == null || at === \"\") return null;\n const d = at instanceof Date ? at : typeof at === \"string\" && ISO.test(at) ? new Date(at) : null;\n if (!d) return { words: String(at), iso: undefined };\n if (Number.isNaN(d.getTime())) return null;\n const now = new Date();\n const today = d.toDateString() === now.toDateString();\n const words = today\n ? d.toLocaleTimeString(\"en-GB\", { hour: \"2-digit\", minute: \"2-digit\" })\n : d.toLocaleDateString(\"en-GB\", { day: \"numeric\", month: \"short\" });\n return { words, iso: d.toISOString() };\n}\n\n/** \"Priya's\", \"James'\", \"your\". */\nfunction whose(name) {\n const n = String(name).trim();\n if (/^you$/i.test(n)) return \"your\";\n return /s$/i.test(n) ? `${n}'` : `${n}'s`;\n}\n\n/**\n * Who did it, in words.\n *\n * by \"agent\" (the agent's work, its mark before it), \"call\" (one\n * model call), or a person's name\n * ask whose ask the agent worked on (\"Priya\", or \"you\"); a job's run\n * is `ask=\"run\"` → \"on a run\"\n * at words (\"11:20\"), a Date or an ISO string\n * mark false: no mark before the words (a head that already has one)\n */\nexport function Byline({ by = \"agent\", ask, at, mark = true, className }) {\n const agent = by === \"agent\";\n const call = by === \"call\";\n const who = agent ? \"By the agent\" : call ? \"By one model call\" : `By ${by}`;\n const on = (agent || call) && ask ? (ask === \"run\" ? \"on a run\" : `on ${whose(ask)} ask`) : null;\n const when = whenWords(at);\n return (\n <span className={cx(\"inline-flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs text-muted-foreground\", className)}>\n {!mark ? null : agent ? <AgentMark /> : call ? <CallMark /> : null}\n <span className=\"min-w-0 break-words\">\n {who}\n {on ? `, ${on}` : \"\"}\n {when ? (\n <>\n {\" · \"}\n <time dateTime={when.iso}>{when.words}</time>\n </>\n ) : null}\n </span>\n </span>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/agent-marks/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/agent-marks/README.md",
|
|
28
|
+
"content": "# AgentMarks\n\nTwo marks and a byline, one of each for the whole platform.\n\n- **AgentMark** — a filled diamond. The agent's work: it read, used\n tools, wrote. It may cost more than one call.\n- **CallMark** — a sparkle in a dashed ring. One model call: no\n conversation, no tools (\"Shorter · 1 cr\").\n- **Byline** — who did it, in words: \"By the agent, on Priya's ask ·\n 11:20\".\n\nBefore the kit, the store's apps drew the agent in six different glyphs.\nA member who learns the diamond in one app now reads it in every app.\n\nThe two marks differ in **shape**, filled against dashed. Never tell them\napart by colour alone, and never draw a sparkle or a robot of your own.\n\n```jsx\nimport { AgentMark, CallMark, Byline } from \"../components/blocks/agent-marks\"; // a Makersclaw app (\"./blocks/agent-marks\" from components/)\nimport { AgentMark, CallMark, Byline } from \"@/components/makersclaw/blocks/agent-marks\"; // a shadcn project\n```\n\nNo data, no hook, no door. Props in.\n\n## Props\n\n### AgentMark · CallMark\n\n- `label` — what a screen reader hears when the mark stands alone (\"The\n agent\", \"One model call\"). Leave it out when words beside the mark\n already say it: the mark is then decoration.\n- `size` — `xs` · `sm` (default, 12px) · `md` · `lg`, or pixels.\n- `children` — words drawn beside the mark (\"Agent\", \"1 call\").\n- `className` — layout around it. Never colour: the agent's mark is the\n theme's `primary`, the call's is muted.\n\n### Byline\n\n- `by` — `\"agent\"` (the default; its mark leads), `\"call\"` (one model\n call), or a person's name (\"Owen\").\n- `ask` — whose ask the agent worked on: a name (\"Priya\" → \"on Priya's\n ask\"), `\"you\"` (\"on your ask\"), or `\"run\"` for a scheduled job's run\n (\"on a run\").\n- `at` — when: words (\"11:20\"), a `Date`, or an ISO string (today reads\n as the time, another day as \"12 Sep\").\n- `mark` — `false` drops the mark before the words, for a head that\n already draws one.\n\n## Where each one goes\n\n- A value the agent wrote, in a cell or beside a field: `AgentMark` with\n `label=\"Written by the agent\"`.\n- A button that makes one model call: `CallMark` (the `ai-action` block\n draws it for you).\n- The head of a receipt, a timeline line, a draft: `Byline`.\n\n## Never\n\n- Never mark a person's own work with either mark.\n- Never use `AgentMark` for one model call, or `CallMark` for an ask: the\n member reads cost and reach from the shape.\n- Never colour a mark through `className`.\n\n## Example\n\n```jsx\nimport { AgentMark, Byline } from \"./blocks/agent-marks\";\n\nexport function WrittenBy({ note }) {\n return (\n <div className=\"grid gap-1\">\n <p className=\"flex items-center gap-1.5\">\n <AgentMark label=\"Written by the agent\" />\n {note.text}\n </p>\n <Byline by=\"agent\" ask={note.asked_by_name} at={note.created_at} />\n </div>\n );\n}\n```\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"ai",
|
|
33
|
+
"mark",
|
|
34
|
+
"badge",
|
|
35
|
+
"byline",
|
|
36
|
+
"provenance",
|
|
37
|
+
"model call",
|
|
38
|
+
"llm",
|
|
39
|
+
"who did it",
|
|
40
|
+
"author"
|
|
41
|
+
],
|
|
42
|
+
"docs": "Installed to components/makersclaw/blocks/agent-marks. Import: import { AgentMark, CallMark, Byline } from \"@/components/makersclaw/blocks/agent-marks\". Props and an example: components/makersclaw/blocks/agent-marks/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
43
|
+
"meta": {
|
|
44
|
+
"makersclaw": {
|
|
45
|
+
"version": "0.8.1",
|
|
46
|
+
"useWhen": "Anywhere a screen holds work that was not a person's own: a value the agent wrote, a line in a timeline, a draft from one model call, the head of an ask's receipt. Use these instead of drawing a sparkle or a robot of your own, so every app marks the agent the same way. The other kit blocks (ai-action, agent-changes, activity-log, needs-you) already use them."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|