pmx-canvas 0.9.0 → 0.9.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/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/CHANGELOG.md +10 -0
- package/dist/canvas/index.js +90 -90
- package/dist/canvas/mcp-app.html +96 -96
- package/dist/types/client/state/human-store.d.ts +2 -1
- package/package.json +1 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/skills/pmx-canvas/SKILL.md +1 -1
- package/skills/pmx-canvas/evals/evals.json +224 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/src/cli/commands/boards.ts +1 -0
- package/src/client/canvas/LifecyclePanel.tsx +26 -16
- package/src/client/nodes/ExtAppFrame.tsx +6 -2
- package/src/client/state/human-store.ts +13 -7
- package/src/client/state/sse-bridge.ts +30 -4
|
@@ -22,6 +22,7 @@ export declare function markYielded(nodeId: string, name: string): void;
|
|
|
22
22
|
* node wears a "took over — agent yielded" pill for a moment.
|
|
23
23
|
*/
|
|
24
24
|
export declare function takeOverNode(nodeId: string, title: string): void;
|
|
25
|
+
export declare function stopHumanPresence(): void;
|
|
25
26
|
/** Announce this tab and keep it alive; renews a held grab. Call once per connection. */
|
|
26
|
-
export declare function startHumanPresence():
|
|
27
|
+
export declare function startHumanPresence(): void;
|
|
27
28
|
export declare function resetHumanPresence(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "pmx-canvas",
|
|
4
4
|
"description": "PMX Canvas spatial workbench and MCP tools, with a native canvas panel in the GitHub Copilot app.",
|
|
5
|
-
"version": "0.9.
|
|
5
|
+
"version": "0.9.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "pskoett",
|
|
8
8
|
"url": "https://github.com/pskoett"
|
|
@@ -106,7 +106,7 @@ for the authoritative stop, viewport, persistence, reset, group-order, and captu
|
|
|
106
106
|
|
|
107
107
|
## Runtime prerequisites
|
|
108
108
|
|
|
109
|
-
PMX Canvas 0.9.
|
|
109
|
+
PMX Canvas 0.9.1 requires **Bun >=1.4.2**, including when installed through npm.
|
|
110
110
|
Check `bun --version` and ensure Bun is on the MCP host's PATH, not just your shell's;
|
|
111
111
|
use an absolute executable path if needed. Install/configure only when requested.
|
|
112
112
|
For pinned installation, MCP configuration, managed services, and disposable verification,
|
|
@@ -834,6 +834,230 @@
|
|
|
834
834
|
"type": "output_check"
|
|
835
835
|
}
|
|
836
836
|
]
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
"id": 36,
|
|
840
|
+
"name": "folder-cli-without-removed-category",
|
|
841
|
+
"prompt": "In a verified disposable workspace, use the CLI to create Release notes under Engineering/Canvas, move it to Engineering/Release, then unfile it without opening it. An old script uses board create --category Engineering/Canvas. Check that obsolete command as a negative control; do not silently accept it or create a knowledge category instead. Leave the active board and its cards alone.",
|
|
842
|
+
"expected_output": "Executes board create \"Release notes\" --folder Engineering/Canvas, captures the returned board ID, uses board folder <id> Engineering/Release and board folder <id> --clear. Reads the library after each step to verify folder and stable identity/content. In the disposable target, --category exits nonzero with the removal diagnostic and creates no board. No board open or category-set substitute occurs; activeBoardId and the original board layout remain unchanged.",
|
|
843
|
+
"assertions": [
|
|
844
|
+
{
|
|
845
|
+
"name": "uses-current-folder-commands",
|
|
846
|
+
"description": "CLI trace uses --folder on create, the returned ID with positional board folder, and --clear to unfile; successful command results are required, not merely suggested commands",
|
|
847
|
+
"type": "output_check"
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
"name": "verifies-folder-and-identity",
|
|
851
|
+
"description": "Library reads show Engineering/Canvas, then Engineering/Release, then an empty folder on the same board ID without losing its content",
|
|
852
|
+
"type": "output_check"
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
"name": "rejects-obsolete-category-control",
|
|
856
|
+
"description": "The isolated --category control exits nonzero with 'The --category flag was removed.' and leaves the library count unchanged; no category-set is used to simulate filing",
|
|
857
|
+
"type": "output_check"
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"name": "does-not-navigate-to-file",
|
|
861
|
+
"description": "Before/after reads show unchanged activeBoardId and active layout; the trace contains no board open to perform filing",
|
|
862
|
+
"type": "output_check"
|
|
863
|
+
}
|
|
864
|
+
]
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
"id": 37,
|
|
868
|
+
"name": "whole-library-map-versus-full-content-delivery",
|
|
869
|
+
"prompt": "On a disposable library, board A is active and inactive board B has pinned Markdown cards Target and Unread control. Target's decisive sentence is beyond character 280. Show the whole-board connection map, then use the actual Target text to answer its question without opening B. Use one stable consumer. After a human edits Target, refresh just that card. Do map summaries prove you read both cards in full?",
|
|
870
|
+
"expected_output": "Reads a whole-library graph with projection boards and omitted focus for orientation, then resolves B/Target and explicitly gets its full content with canvas_node {action:get, id, board:B, full:true}. Uses the sentence beyond the summary in its answer. Captures read status before and after graph/full deliveries, checks Target's delivered content revision, then fully rereads Target after the edit. Does not mark Unread control fully read from a graph/compact summary or confuse complete link delivery with full-content delivery. Active A remains unchanged.",
|
|
871
|
+
"assertions": [
|
|
872
|
+
{
|
|
873
|
+
"name": "requests-board-map-not-content-dump",
|
|
874
|
+
"description": "Trace requests canvas_query graph with projection boards and no focus (or equivalent memory-graph API), and observes board-qualified summaryOnly results rather than using a full layout as the map",
|
|
875
|
+
"type": "output_check"
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
"name": "fetches-and-uses-full-qualified-target",
|
|
879
|
+
"description": "Trace gets the resolved B/Target with full:true (or equivalent tracked full-node API); the answer uses its seeded sentence beyond 280 characters and read-status evidence records that delivered revision",
|
|
880
|
+
"type": "output_check"
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
"name": "refreshes-only-observed-stale-content",
|
|
884
|
+
"description": "After the human edit, a new full Target delivery advances its recorded read revision; status deltas show the unread control received no full-content read from map/compact discovery alone",
|
|
885
|
+
"type": "output_check"
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"name": "preserves-active-board-and-read-boundaries",
|
|
889
|
+
"description": "Before/after activeBoardId and A layout match, no open/write occurs, and full-read claims are backed by deliveries rather than summaryOnly or link-seen state",
|
|
890
|
+
"type": "output_check"
|
|
891
|
+
}
|
|
892
|
+
]
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
"id": 38,
|
|
896
|
+
"name": "durable-background-create-replay",
|
|
897
|
+
"prompt": "In an isolated workspace, keep A active while populating inactive B with two static Markdown cards, a group containing them, and a references edge. Use durable request IDs per creation and batch named references. Simulate a lost response after a committed prefix, retry, and restart the disposable daemon before retrying again. With the harness, edit one created card and delete the other on B, return to A, then replay the original requests. Do not recreate deleted work or undo later edits.",
|
|
898
|
+
"expected_output": "Uses destinationBoardId:B and stable per-operation requestId inside each batch operation's args, resolving group/edge named references. Replays identical inputs after partial completion and restart and compares original receipts/IDs. Later edit/deletion controls are harness-owned, not unsupported background CRUD. Replay returns original receipts without duplicating, overwriting or resurrecting objects. Qualified reads show B's intended surviving state and unchanged A activeBoardId/layout/viewport throughout agent background writes.",
|
|
899
|
+
"assertions": [
|
|
900
|
+
{
|
|
901
|
+
"name": "targets-each-static-creation",
|
|
902
|
+
"description": "Batch trace puts destinationBoardId and distinct logical requestId in node/group/edge args, uses valid named references, and verifies group membership and qualified edge endpoints on B",
|
|
903
|
+
"type": "output_check"
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"name": "replays-prefix-and-restart-durably",
|
|
907
|
+
"description": "Evidence includes a committed prefix with lost response, identical retry after it and after a real isolated daemon restart, stable returned IDs/receipts and no duplicate nodes/groups/edges",
|
|
908
|
+
"type": "output_check"
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
"name": "replay-is-not-reapplication",
|
|
912
|
+
"description": "After harness edit/deletion and original-input replay, qualified reads retain the later edit and deleted-card absence even though original creation receipts are returned",
|
|
913
|
+
"type": "output_check"
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
"name": "keeps-background-work-off-active-board",
|
|
917
|
+
"description": "Before/after reads around agent creation/replays show A stays active with identical layout/viewport; no agent board open, active update/remove, runtime creation or background intent is used",
|
|
918
|
+
"type": "output_check"
|
|
919
|
+
}
|
|
920
|
+
]
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
"id": 39,
|
|
924
|
+
"name": "background-create-conflicts-and-stale-active-guard",
|
|
925
|
+
"prompt": "In a disposable workspace with A active and B inactive, create a static note on B with requestId logical-note and expectedBoardId A. Test reusing logical-note with changed content and with destination A. Then have the harness switch the active board to C and send a new creation for B using stale expectedBoardId A. Do not force it through. Finally create a genuinely separate identical note with a fresh request ID after checking C. Also test a background file-node creation as an unsupported control.",
|
|
926
|
+
"expected_output": "Observes conflicts on changed-input and changed-destination reuse without mutating either destination. Stale expectedBoardId guards the actual active board, not B: the new request is refused after the switch to C. Rereads active identity before using expectedBoardId C with a fresh logical ID for the intentional duplicate. Unsupported background file creation fails without side effects. No clearing guards, board switching or blind retries to defeat rejection; C stays active.",
|
|
927
|
+
"assertions": [
|
|
928
|
+
{
|
|
929
|
+
"name": "changed-request-conflicts-without-writes",
|
|
930
|
+
"description": "Actual changed-content and changed-destination requests using logical-note fail with conflicts, and destination reads prove original content/counts are unchanged",
|
|
931
|
+
"type": "output_check"
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
"name": "stale-guard-requires-reread",
|
|
935
|
+
"description": "After the harness switch, expectedBoardId A is refused with no creation on B; trace rereads C before any fresh write and does not remove the guard or substitute B as the active identity",
|
|
936
|
+
"type": "output_check"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
"name": "new-operation-gets-new-identity",
|
|
940
|
+
"description": "The intentional identical creation uses a new requestId and expectedBoardId C, returns a distinct node ID on B and leaves C active with unchanged layout",
|
|
941
|
+
"type": "output_check"
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
"name": "unsupported-background-control-has-no-effect",
|
|
945
|
+
"description": "A file-node creation with destinationBoardId B fails as unsupported and qualified reads show no new file or active-board side effect; no active-board fallback is attempted",
|
|
946
|
+
"type": "output_check"
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
"id": 40,
|
|
952
|
+
"name": "board-qualified-native-relations",
|
|
953
|
+
"prompt": "In a disposable library A is active, and A and B both contain a card with ID shared. The harness has given A/shared, A/Navigation and B/shared explicit durable lifecycle policies and validated their current content. From A/shared add a depends-on edge to B/shared with reason 'Capacity requires measured throughput'. Add an ordinary references edge from A/Navigation to the same remote source. Read the qualified graph, change B/shared with the harness, and check which dependents become review-due. Keep A active and do not replace the edge with a board portal.",
|
|
954
|
+
"expected_output": "Resolves the two board identities and uses canvas_edge add with from:shared, to:shared, toBoardId:B, type:depends-on for dependent to prerequisite, preserving reason. Uses a references edge for Navigation. Reads graph and lifecycle qualifications with explicit board identities: the dependency is remote, not a self-loop on A. With explicit durable lifecycle policies in the fixture, the source change makes the dependent due but not the ordinary-reference control. Reads do not open B or mutate its source.",
|
|
955
|
+
"assertions": [
|
|
956
|
+
{
|
|
957
|
+
"name": "writes-qualified-remote-edge",
|
|
958
|
+
"description": "Actual edge add uses A/shared as source and toBoardId B with target shared; graph endpoints are distinct qualified keys, not bare-ID self-loops or board portal nodes",
|
|
959
|
+
"type": "output_check"
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"name": "preserves-direction-and-reason",
|
|
963
|
+
"description": "Readback shows depends-on from dependent A/shared to prerequisite B/shared with the exact supplied reason, independently of display style or label",
|
|
964
|
+
"type": "output_check"
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
"name": "distinguishes-navigation-from-validation-dependency",
|
|
968
|
+
"description": "Before/after lifecycle reads under explicit durable policies show B's semantic change makes A/shared due while the references-only Navigation control stays not due",
|
|
969
|
+
"type": "output_check"
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
"name": "reads-remote-source-without-navigation",
|
|
973
|
+
"description": "Tool trace and before/after board reads keep A active; agent does not open B, edit its source or create a portal as a substitute for the remote card relation",
|
|
974
|
+
"type": "output_check"
|
|
975
|
+
}
|
|
976
|
+
]
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"id": 41,
|
|
980
|
+
"name": "wiki-bindings-survive-renames-without-guessing",
|
|
981
|
+
"prompt": "In a disposable library, save a source Markdown card on A containing [[Research#Result|evidence]], plus code-span and fenced-code copies of that link. Research uniquely names B and Result uniquely names B's target card. Record the binding, then let the harness rename B and Result and create new objects using the old names. Separately save [[Ambiguous]] when two boards share that name, and [[Future]] before that board exists. After the harness creates Future, read the source without saving, then explicitly save it. Do not open other boards or hand-author wikiLinks metadata.",
|
|
982
|
+
"expected_output": "Saves through the native Markdown path and inspects server-produced qualified wiki bindings/backlinks. The bound aliased link continues to target B's original card after renames despite old-name replacements; code samples add no relations. Ambiguous links stay unresolved instead of choosing a board. The saved missing Future link remains unresolved on read after creation and resolves only on explicit source save. Does not fabricate binding IDs or switch active boards to resolve references.",
|
|
983
|
+
"assertions": [
|
|
984
|
+
{
|
|
985
|
+
"name": "uses-server-bindings-and-ignores-code",
|
|
986
|
+
"description": "Save/read trace shows one real qualified Research/Result wiki relation with alias evidence, server-owned data.wikiLinks, and no extra relations for code-span/fenced examples",
|
|
987
|
+
"type": "output_check"
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
"name": "identity-wins-over-renamed-labels",
|
|
991
|
+
"description": "After harness renames and old-name replacements, graph/backlink or binding readback still identifies the original B/card IDs rather than silently retargeting to replacement names",
|
|
992
|
+
"type": "output_check"
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
"name": "does-not-guess-ambiguous-board",
|
|
996
|
+
"description": "The saved Ambiguous reference remains explicitly unresolved with no target selected; trace does not rewrite the source to an arbitrary candidate",
|
|
997
|
+
"type": "output_check"
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
"name": "resolution-requires-explicit-save",
|
|
1001
|
+
"description": "Future remains unresolved after a read with the board now present, then resolves on explicit source save; A stays active and no invented wikiLinks metadata is submitted",
|
|
1002
|
+
"type": "output_check"
|
|
1003
|
+
}
|
|
1004
|
+
]
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
"id": 42,
|
|
1008
|
+
"name": "review-evidence-bound-to-checked-content-and-sources",
|
|
1009
|
+
"prompt": "On disposable board A, review Conclusion against its explicit dependency on B/Source. Read the actual content and capture lifecycle observations. Before submission let the harness change Conclusion, then in a separate trial change Source. Submit the captured stale reviews as controls. Do not just copy fresh tokens onto the old note. Recheck the evidence for a successful retain review, and also test an unavailable source, applied:false and blank evidence. In a separate harness-seeded cached-provenance control, compare observedRevision and availability to the current live source rather than treating retrieval time as freshness. Does fetching or regenerating a card alone validate it?",
|
|
1010
|
+
"expected_output": "Reads full qualified content and lifecycle-get, captures the checked content revision and qualification.observations, then supplies them as expectedContentRevision/expectedSources on canvas_node review with outcome retain, applied true and substantive evidence. Both stale trials conflict and leave validation unchanged. A successful review follows real rereads/recomparison, not token replacement, and its stored record identifies checked content/source revisions. Unavailable sources refuse validation; unapplied or empty-evidence outcomes do not renew it. Retrieval, editing and recipe refresh alone never validate.",
|
|
1011
|
+
"assertions": [
|
|
1012
|
+
{
|
|
1013
|
+
"name": "checks-content-and-observed-sources-before-review",
|
|
1014
|
+
"description": "Trace delivers full Conclusion and qualified B/Source content and lifecycle observations before sending retain/applied:true with nonempty evidence, expectedContentRevision and expectedSources from the checked versions",
|
|
1015
|
+
"type": "output_check"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"name": "stale-evidence-cannot-renew-validation",
|
|
1019
|
+
"description": "Separate harness content-change and source-change trials reject captured stale reviews with conflicts and preserve lastValidation; no unchecked new revision tokens are pasted onto the old evidence",
|
|
1020
|
+
"type": "output_check"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
"name": "successful-record-binds-rechecked-versions",
|
|
1024
|
+
"description": "After actual full rereads and evidence comparison, successful review readback records the checked contentRevision and sourceRevisions; fetching/editing/refreshing alone leaves lastValidation unchanged",
|
|
1025
|
+
"type": "output_check"
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
"name": "nonvalidating-controls-do-not-renew",
|
|
1029
|
+
"description": "Unavailable-source review is refused, and applied:false and blank-evidence controls do not advance lastValidation; observed cached-source revision/availability is not misrepresented as a fresh live check",
|
|
1030
|
+
"type": "output_check"
|
|
1031
|
+
}
|
|
1032
|
+
]
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
"id": 43,
|
|
1036
|
+
"name": "retirement-excludes-context-until-audited-restore",
|
|
1037
|
+
"prompt": "On disposable A, pin Obsolete and Keep. Give Obsolete an explicit durable lifecycle policy and validate its current content with evidence, then retire it. Check the brief, pinned context, search and direct read. Read and edit Obsolete and apply a retain review: should any of these unretire it? Finally make an unvalidated content edit, explicitly restore it with reason 'Needed for follow-up', and check its eligibility and review status. B has the same card ID as Obsolete; do not change B.",
|
|
1038
|
+
"expected_output": "Uses canvas_node review outcome retire/applied true and expectedBoardId A. Retired Obsolete is absent from briefs and pinned-context delivery while search/direct reads retain it marked retired; Keep stays delivered. Read/edit/retain do not reverse retirement. Explicit lifecycle-restore with evidence reverses retirement, makes the still-pinned card eligible again, but leaves lastValidation bound to the older checked revision so the final edited content remains review-due. Board-qualified checks confirm B's same-ID card and lifecycle history are unchanged.",
|
|
1039
|
+
"assertions": [
|
|
1040
|
+
{
|
|
1041
|
+
"name": "retirement-excludes-delivery-not-storage",
|
|
1042
|
+
"description": "After retire/applied:true, brief and pinned-context results omit A/Obsolete while Keep remains; qualified direct read/search still find Obsolete marked retired rather than deleted",
|
|
1043
|
+
"type": "output_check"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
"name": "ordinary-actions-cannot-unretire",
|
|
1047
|
+
"description": "Actual read, content edit and applied retain review leave the retirement flag set and Obsolete excluded from context delivery",
|
|
1048
|
+
"type": "output_check"
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
"name": "restore-is-audited-not-validation",
|
|
1052
|
+
"description": "After a final unvalidated semantic edit, lifecycle-restore records 'Needed for follow-up', clears retired and restores context eligibility without advancing lastValidation; the current revision remains due",
|
|
1053
|
+
"type": "output_check"
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
"name": "lifecycle-identity-is-board-qualified",
|
|
1057
|
+
"description": "Writes use expectedBoardId A; before/after reads of B's same-ID card show unchanged content, retirement and review records, with A still active",
|
|
1058
|
+
"type": "output_check"
|
|
1059
|
+
}
|
|
1060
|
+
]
|
|
837
1061
|
}
|
|
838
1062
|
]
|
|
839
1063
|
}
|
|
@@ -4,7 +4,7 @@ Use this reference when the `pmx-canvas` skill is installed but the `pmx-canvas`
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites and version
|
|
6
6
|
|
|
7
|
-
The examples pin **PMX Canvas 0.9.
|
|
7
|
+
The examples pin **PMX Canvas 0.9.1**, which requires **Bun >=1.4.2** even when
|
|
8
8
|
installed through npm: the CLI runs through Bun, not Node. Check `bun --version`
|
|
9
9
|
first. If missing or older, follow the [Bun installation instructions](https://bun.sh/docs/installation)
|
|
10
10
|
with the user's authorization. Ensure Bun is on the MCP host's PATH as well as the
|
|
@@ -17,11 +17,11 @@ in `SKILL.md`. Do not pair newer checkout guidance with an older runtime unknowi
|
|
|
17
17
|
## Install from npm
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install -g pmx-canvas@0.9.
|
|
20
|
+
npm install -g pmx-canvas@0.9.1
|
|
21
21
|
pmx-canvas --version
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Without a global install, use `bunx pmx-canvas@0.9.
|
|
24
|
+
Without a global install, use `bunx pmx-canvas@0.9.1` in place of `pmx-canvas` in
|
|
25
25
|
the commands below. Do not use unpinned `bunx pmx-canvas` for a version-specific check.
|
|
26
26
|
|
|
27
27
|
## Choose the workspace and target
|
|
@@ -91,7 +91,7 @@ discards empty env values, remove `PMX_CANVAS_URL` from its launch environment i
|
|
|
91
91
|
"mcpServers": {
|
|
92
92
|
"canvas": {
|
|
93
93
|
"command": "bunx",
|
|
94
|
-
"args": ["pmx-canvas@0.9.
|
|
94
|
+
"args": ["pmx-canvas@0.9.1", "--mcp"],
|
|
95
95
|
"env": {
|
|
96
96
|
"PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
|
|
97
97
|
"PMX_CANVAS_PORT": "14313",
|
|
@@ -135,7 +135,7 @@ Amp executes, not only on the user's laptop. Merge this into that workspace's
|
|
|
135
135
|
"amp.mcpServers": {
|
|
136
136
|
"canvas": {
|
|
137
137
|
"command": "bunx",
|
|
138
|
-
"args": ["pmx-canvas@0.9.
|
|
138
|
+
"args": ["pmx-canvas@0.9.1", "--mcp"],
|
|
139
139
|
"includeTools": ["canvas_*"],
|
|
140
140
|
"env": {
|
|
141
141
|
"PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
|
|
@@ -169,7 +169,7 @@ The agent must not run this approval itself or move the command to global settin
|
|
|
169
169
|
`--mcp-config` to bypass workspace trust. Approval authorizes execution of this MCP
|
|
170
170
|
command; it is not a new Canvas import consent or permission to mutate arbitrary boards.
|
|
171
171
|
After approval, reload MCP connections (`reload_mcp` in Amp) and require `connected`.
|
|
172
|
-
Discover `canvas` tools with `tool_search`; for the full unfiltered 0.9.
|
|
172
|
+
Discover `canvas` tools with `tool_search`; for the full unfiltered 0.9.1 server there
|
|
173
173
|
are 24 tools. If still absent, inspect the server status, command/PATH and target instead
|
|
174
174
|
of reinstalling blindly. See [Amp MCP documentation](https://ampcode.com/docs/customize/mcp).
|
|
175
175
|
|
|
@@ -102,6 +102,7 @@ cmd(
|
|
|
102
102
|
async (args) => {
|
|
103
103
|
const { positional, flags } = parseFlags(args);
|
|
104
104
|
if (flags.help || flags.h) return showCommandHelp('board create');
|
|
105
|
+
if ('category' in flags) die('The --category flag was removed.', 'Use --folder <folder> instead.');
|
|
105
106
|
const name = positional.join(' ').trim();
|
|
106
107
|
if (!name) die('Usage: pmx-canvas board create <name> [--folder <folder>]');
|
|
107
108
|
const folder = getStringFlag(flags, 'folder');
|
|
@@ -105,7 +105,7 @@ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarge
|
|
|
105
105
|
void refreshKnowledge();
|
|
106
106
|
if (!detail || needsReload) void load();
|
|
107
107
|
}, [enabled]);
|
|
108
|
-
const mutate = async (suffix: string, body: Record<string, unknown
|
|
108
|
+
const mutate = async (suffix: string, body: Record<string, unknown>, acknowledge?: () => void) => {
|
|
109
109
|
if (!ownsDialog() || busy || needsReload) return false;
|
|
110
110
|
setBusy(true);
|
|
111
111
|
setError('');
|
|
@@ -114,7 +114,7 @@ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarge
|
|
|
114
114
|
headers: { 'Content-Type': 'application/json' },
|
|
115
115
|
body: JSON.stringify({ ...body, expectedBoardId: target.boardId }),
|
|
116
116
|
});
|
|
117
|
-
if (!sameBoard()
|
|
117
|
+
if (!sameBoard()) {
|
|
118
118
|
setBusy(false);
|
|
119
119
|
return false;
|
|
120
120
|
}
|
|
@@ -125,6 +125,14 @@ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarge
|
|
|
125
125
|
);
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
|
+
// Acknowledgement belongs to the accepted write, not the later reads or
|
|
129
|
+
// the dock's lifetime. Closing/suspending the dock must not lose Undo.
|
|
130
|
+
acknowledge?.();
|
|
131
|
+
if (lifecycleTarget.value !== target) {
|
|
132
|
+
void refreshKnowledge();
|
|
133
|
+
setBusy(false);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
128
136
|
// Invalidate acknowledged preconditions without unmounting focused controls.
|
|
129
137
|
// If suspended, resume reloads before another mutation is allowed.
|
|
130
138
|
setNeedsReload(true);
|
|
@@ -144,20 +152,22 @@ function LifecycleDialog({ target }: { target: NonNullable<typeof lifecycleTarge
|
|
|
144
152
|
error)
|
|
145
153
|
)
|
|
146
154
|
return false;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
return mutate(
|
|
156
|
+
'/review',
|
|
157
|
+
{
|
|
158
|
+
outcome: reviewOutcome,
|
|
159
|
+
evidence,
|
|
160
|
+
applied: wasApplied,
|
|
161
|
+
expectedContentRevision: detail.contentRevision,
|
|
162
|
+
expectedSources: detail.qualification.observations,
|
|
163
|
+
},
|
|
164
|
+
() => {
|
|
165
|
+
setRetiredHere(wasApplied && reviewOutcome === 'retire');
|
|
166
|
+
if (wasApplied && reviewOutcome === 'retire') retirementUndo.value = target;
|
|
167
|
+
setEvidence('');
|
|
168
|
+
setApplied(false);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
161
171
|
};
|
|
162
172
|
const recipe = node?.data.provenance as { recipe?: { tool: string; input?: unknown; prompt: string } } | undefined;
|
|
163
173
|
const date = (value: string | null | undefined) => (value ? new Date(value).toLocaleString() : 'Unknown');
|
|
@@ -1016,6 +1016,9 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1016
1016
|
|
|
1017
1017
|
// Register handlers BEFORE connect
|
|
1018
1018
|
bridge.onsizechange = async ({ height }) => {
|
|
1019
|
+
const currentNode = nodes.value.get(nodeId);
|
|
1020
|
+
// Late app measurements must not undo an explicit manual card size.
|
|
1021
|
+
if (!currentNode || currentNode.data.userResized === true) return {};
|
|
1019
1022
|
if (shouldApplyExtAppSizeChange(height, expandedNodeId.value === nodeId)) {
|
|
1020
1023
|
const hostDimensions = resolveExtAppContainerDimensions(iframe.parentElement ?? iframe, {
|
|
1021
1024
|
width: node.size.width,
|
|
@@ -1023,7 +1026,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1023
1026
|
});
|
|
1024
1027
|
const inlineFrameHeight = resolveExtAppInlineFrameHeight(height, hostDimensions.height);
|
|
1025
1028
|
iframe.style.height = `${inlineFrameHeight}px`;
|
|
1026
|
-
const currentSize =
|
|
1029
|
+
const currentSize = currentNode.size;
|
|
1027
1030
|
const nodeHeight = Math.max(currentSize.height, inlineFrameHeight + AUTO_FIT_TITLEBAR_HEIGHT);
|
|
1028
1031
|
if (Math.abs(nodeHeight - currentSize.height) > 8) {
|
|
1029
1032
|
resizeNode(nodeId, { width: currentSize.width, height: nodeHeight });
|
|
@@ -1031,8 +1034,9 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
1031
1034
|
window.clearTimeout(sizePersistTimerRef.current);
|
|
1032
1035
|
}
|
|
1033
1036
|
sizePersistTimerRef.current = window.setTimeout(() => {
|
|
1034
|
-
persistLayout({ recordHistory: false });
|
|
1035
1037
|
sizePersistTimerRef.current = null;
|
|
1038
|
+
const latest = nodes.value.get(nodeId);
|
|
1039
|
+
if (latest && latest.data.userResized !== true) persistLayout({ recordHistory: false });
|
|
1036
1040
|
}, 0);
|
|
1037
1041
|
}
|
|
1038
1042
|
}
|
|
@@ -146,12 +146,23 @@ export function takeOverNode(nodeId: string, title: string): void {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
149
|
+
let leave: (() => void) | null = null;
|
|
150
|
+
|
|
151
|
+
export function stopHumanPresence(): void {
|
|
152
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
153
|
+
heartbeat = null;
|
|
154
|
+
if (pending) clearTimeout(pending);
|
|
155
|
+
pending = null;
|
|
156
|
+
if (leave) window.removeEventListener('pagehide', leave);
|
|
157
|
+
leave = null;
|
|
158
|
+
}
|
|
149
159
|
|
|
150
160
|
/** Announce this tab and keep it alive; renews a held grab. Call once per connection. */
|
|
151
|
-
export function startHumanPresence():
|
|
161
|
+
export function startHumanPresence(): void {
|
|
162
|
+
stopHumanPresence();
|
|
152
163
|
post({ cursor: lastCursor, grabbingNodeId: grabbingNodeId.value });
|
|
153
164
|
heartbeat = setInterval(() => post({ grabbingNodeId: grabbingNodeId.value }), 3000);
|
|
154
|
-
|
|
165
|
+
leave = () => {
|
|
155
166
|
try {
|
|
156
167
|
navigator.sendBeacon?.(
|
|
157
168
|
'/api/canvas/human-presence',
|
|
@@ -162,11 +173,6 @@ export function startHumanPresence(): () => void {
|
|
|
162
173
|
}
|
|
163
174
|
};
|
|
164
175
|
window.addEventListener('pagehide', leave);
|
|
165
|
-
return () => {
|
|
166
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
167
|
-
heartbeat = null;
|
|
168
|
-
window.removeEventListener('pagehide', leave);
|
|
169
|
-
};
|
|
170
176
|
}
|
|
171
177
|
|
|
172
178
|
export function resetHumanPresence(): void {
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
tabAgentKey,
|
|
59
59
|
} from './intent-bridge';
|
|
60
60
|
import { agentActivity, applyPresenceSnapshot, sessionActive } from './presence-store';
|
|
61
|
-
import { applyHumanSnapshot, humanClientId, startHumanPresence } from './human-store';
|
|
61
|
+
import { applyHumanSnapshot, humanClientId, startHumanPresence, stopHumanPresence } from './human-store';
|
|
62
62
|
import type { HumanPresenceSnapshot } from '../../shared/human-presence.js';
|
|
63
63
|
import { activityLens, applySessionReceipt, dismissSessionReceipt, refreshTimeline } from './session-store';
|
|
64
64
|
import { initSessionThemeOverride, themeOverrideActive } from './theme-override';
|
|
@@ -502,7 +502,6 @@ function handleAgentSessionEnded(data: Record<string, unknown>): void {
|
|
|
502
502
|
applySessionReceipt(data);
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
-
let stopHumanPresence: (() => void) | null = null;
|
|
506
505
|
let boardResyncGeneration = 0;
|
|
507
506
|
let layoutRevision = 0;
|
|
508
507
|
let pendingBoardLayout: Record<string, unknown> | null = null;
|
|
@@ -586,8 +585,8 @@ function handleConnected(data: Record<string, unknown>): void {
|
|
|
586
585
|
// Human collaborators (phase 8): announce this tab and read who else is here.
|
|
587
586
|
// An `?agent=<key>` tab is an AGENT's view — it must not heartbeat a human
|
|
588
587
|
// guest cursor (its writes already book as the agent).
|
|
589
|
-
stopHumanPresence
|
|
590
|
-
if (!tabAgentKey && !isHostedWorkbench())
|
|
588
|
+
stopHumanPresence();
|
|
589
|
+
if (!tabAgentKey && !isHostedWorkbench()) startHumanPresence();
|
|
591
590
|
void requestJson<Partial<HumanPresenceSnapshot> | null>(
|
|
592
591
|
'fetchHumanPresence',
|
|
593
592
|
'/api/canvas/human-presence',
|
|
@@ -1378,7 +1377,32 @@ function startPollingTransport(): () => void {
|
|
|
1378
1377
|
// normalization still arrive even though the event cursor advances past the old batch.
|
|
1379
1378
|
const acceptPins = pinsAtRequest === contextPinsRevision && contextPinsPendingWrites === 0;
|
|
1380
1379
|
let skippedPins = false;
|
|
1380
|
+
// Full layouts in a buffered poll can predate an HTTP refresh. Replaying
|
|
1381
|
+
// an obsolete empty layout would detach a live editor before its card
|
|
1382
|
+
// reappears. Keep the last snapshot across bookkeeping frames only:
|
|
1383
|
+
// commands (focus, tours, deltas, etc.) still need their preceding layout.
|
|
1384
|
+
const supersededLayouts = new Set<(typeof body.events)[number]>();
|
|
1385
|
+
if (!body.snapshot && hasInitialServerLayout.value) {
|
|
1386
|
+
let previousLayout: (typeof body.events)[number] | undefined;
|
|
1387
|
+
for (const entry of body.events) {
|
|
1388
|
+
const data = entry.payload as Record<string, unknown>;
|
|
1389
|
+
const sameActivation =
|
|
1390
|
+
data.catalogEpoch === catalogEpoch &&
|
|
1391
|
+
data.boardGeneration === boardGeneration &&
|
|
1392
|
+
(entry.event === 'boards-changed' ? data.activeBoardId : data.boardId) === activeBoardId.value;
|
|
1393
|
+
if (entry.event === 'canvas-layout-update' && sameActivation) {
|
|
1394
|
+
if (previousLayout) supersededLayouts.add(previousLayout);
|
|
1395
|
+
previousLayout = entry;
|
|
1396
|
+
} else if (
|
|
1397
|
+
!(entry.event === 'boards-changed' && sameActivation) &&
|
|
1398
|
+
!['agent-presence', 'human-presence', 'ax-intent', 'ax-intent-clear'].includes(entry.event)
|
|
1399
|
+
) {
|
|
1400
|
+
previousLayout = undefined;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1381
1404
|
for (const entry of body.events) {
|
|
1405
|
+
if (supersededLayouts.has(entry)) continue;
|
|
1382
1406
|
if (entry.event === 'context-pins-changed' && !acceptPins) {
|
|
1383
1407
|
skippedPins = true;
|
|
1384
1408
|
continue;
|
|
@@ -1411,6 +1435,7 @@ function startPollingTransport(): () => void {
|
|
|
1411
1435
|
void pollOnce();
|
|
1412
1436
|
|
|
1413
1437
|
return () => {
|
|
1438
|
+
stopHumanPresence();
|
|
1414
1439
|
pollGeneration += 1;
|
|
1415
1440
|
pollingMode = false;
|
|
1416
1441
|
pollSeq = null;
|
|
@@ -1511,6 +1536,7 @@ export function connectSSE(): () => void {
|
|
|
1511
1536
|
};
|
|
1512
1537
|
|
|
1513
1538
|
return () => {
|
|
1539
|
+
stopHumanPresence();
|
|
1514
1540
|
if (reconnectTimer) {
|
|
1515
1541
|
clearTimeout(reconnectTimer);
|
|
1516
1542
|
reconnectTimer = null;
|