pmx-canvas 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +591 -0
  2. package/CHANGELOG.md +140 -0
  3. package/Readme.md +40 -8
  4. package/dist/canvas/global.css +36 -3
  5. package/dist/canvas/index.js +54 -54
  6. package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -0
  7. package/dist/types/client/nodes/iframe-document-url.d.ts +8 -0
  8. package/dist/types/client/state/intent-bridge.d.ts +4 -0
  9. package/dist/types/client/types.d.ts +1 -0
  10. package/dist/types/json-render/catalog.d.ts +1 -1
  11. package/dist/types/mcp/canvas-access.d.ts +9 -0
  12. package/dist/types/server/ax-context.d.ts +3 -0
  13. package/dist/types/server/ax-state.d.ts +43 -0
  14. package/dist/types/server/canvas-db.d.ts +38 -0
  15. package/dist/types/server/canvas-state.d.ts +36 -16
  16. package/dist/types/server/index.d.ts +6 -0
  17. package/dist/types/server/mutation-history.d.ts +1 -1
  18. package/docs/cli.md +13 -0
  19. package/docs/http-api.md +24 -0
  20. package/docs/mcp.md +20 -2
  21. package/docs/plans/plan-004-pmx-ax-primitives.md +463 -0
  22. package/docs/screenshot.png +0 -0
  23. package/docs/sdk.md +5 -0
  24. package/package.json +3 -2
  25. package/skills/pmx-canvas/SKILL.md +22 -4
  26. package/skills/pmx-canvas/references/codex-app-adapter.md +107 -0
  27. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +111 -0
  28. package/src/cli/agent.ts +34 -0
  29. package/src/cli/index.ts +2 -1
  30. package/src/client/App.tsx +2 -0
  31. package/src/client/canvas/CanvasNode.tsx +7 -0
  32. package/src/client/canvas/CommandPalette.tsx +2 -1
  33. package/src/client/canvas/use-node-drag.ts +29 -7
  34. package/src/client/canvas/use-node-resize.ts +27 -7
  35. package/src/client/nodes/ExtAppFrame.tsx +51 -10
  36. package/src/client/nodes/HtmlNode.tsx +5 -2
  37. package/src/client/nodes/iframe-document-url.ts +58 -0
  38. package/src/client/state/intent-bridge.ts +8 -0
  39. package/src/client/state/sse-bridge.ts +2 -2
  40. package/src/client/theme/global.css +36 -3
  41. package/src/client/types.ts +1 -0
  42. package/src/mcp/canvas-access.ts +38 -0
  43. package/src/mcp/server.ts +113 -4
  44. package/src/server/ax-context.ts +38 -0
  45. package/src/server/ax-state.ts +130 -0
  46. package/src/server/canvas-db.ts +745 -0
  47. package/src/server/canvas-operations.ts +80 -1
  48. package/src/server/canvas-schema.ts +3 -3
  49. package/src/server/canvas-state.ts +390 -50
  50. package/src/server/canvas-validation.ts +6 -0
  51. package/src/server/index.ts +18 -0
  52. package/src/server/mutation-history.ts +1 -0
  53. package/src/server/server.ts +197 -11
@@ -24,6 +24,7 @@ export declare function resolveExtAppContainerDimensions(target: ExtAppHostDimen
24
24
  height: number;
25
25
  };
26
26
  export declare function shouldApplyExtAppSizeChange(height: unknown, isExpanded: boolean): height is number;
27
+ export declare function resolveExtAppInlineFrameHeight(appHeight: number, hostHeight: number): number;
27
28
  export declare function ExtAppFrame({ node, expanded }: {
28
29
  node: CanvasNodeState;
29
30
  expanded?: boolean;
@@ -0,0 +1,8 @@
1
+ export declare function createIframeDocumentUrl(html: string, sandbox: string): Promise<string>;
2
+ export declare function useIframeDocument(html: string, sandbox: string): {
3
+ attributes: {
4
+ src?: string;
5
+ };
6
+ ready: boolean;
7
+ key: string;
8
+ };
@@ -22,6 +22,10 @@ export declare function openWorkbenchFile(path: string): Promise<{
22
22
  }>;
23
23
  /** Fetch canvas state from server. */
24
24
  export declare function fetchCanvasState(): Promise<Record<string, unknown>>;
25
+ export declare function saveCanvasTheme(theme: string): Promise<{
26
+ ok: boolean;
27
+ theme?: string;
28
+ }>;
25
29
  /** Fetch available slash commands for prompt completion. */
26
30
  export declare function fetchSlashCommands(): Promise<Array<{
27
31
  name: string;
@@ -58,6 +58,7 @@ export declare const EXCALIDRAW_CREATE_VIEW_TOOL = "create_view";
58
58
  export declare function isExcalidrawNode(node: CanvasNodeState): boolean;
59
59
  export interface CanvasLayout {
60
60
  viewport: ViewportState;
61
+ theme?: 'dark' | 'light' | 'high-contrast';
61
62
  nodes: CanvasNodeState[];
62
63
  edges: CanvasEdge[];
63
64
  annotations?: CanvasAnnotation[];
@@ -287,8 +287,8 @@ export declare const allComponentDefinitions: {
287
287
  props: z.ZodObject<{
288
288
  text: z.ZodString;
289
289
  variant: z.ZodNullable<z.ZodEnum<{
290
- default: "default";
291
290
  error: "error";
291
+ default: "default";
292
292
  success: "success";
293
293
  secondary: "secondary";
294
294
  destructive: "destructive";
@@ -1,4 +1,5 @@
1
1
  import { type CanvasLayout, type CanvasNodeState, type CanvasSnapshot, type PmxCanvas } from '../server/index.js';
2
+ import type { PmxAxSource } from '../server/ax-state.js';
2
3
  type AddNodeInput = Parameters<PmxCanvas['addNode']>[0];
3
4
  type AddWebpageNodeInput = Parameters<PmxCanvas['addWebpageNode']>[0];
4
5
  type RefreshWebpageNodeResult = Awaited<ReturnType<PmxCanvas['refreshWebpageNode']>>;
@@ -20,6 +21,9 @@ type ArrangeLayout = Parameters<PmxCanvas['arrange']>[0];
20
21
  type FocusNodeResult = ReturnType<PmxCanvas['focusNode']>;
21
22
  type FitViewOptions = Parameters<PmxCanvas['fitView']>[0];
22
23
  type FitViewResult = ReturnType<PmxCanvas['fitView']>;
24
+ type AxStateResult = ReturnType<PmxCanvas['getAxState']>;
25
+ type AxContextResult = ReturnType<PmxCanvas['getAxContext']>;
26
+ type SetAxFocusResult = ReturnType<PmxCanvas['setAxFocus']>;
23
27
  type SearchResult = ReturnType<PmxCanvas['search']>;
24
28
  type UndoRedoResult = Awaited<ReturnType<PmxCanvas['undo']>>;
25
29
  type HistoryResult = ReturnType<PmxCanvas['getHistory']>;
@@ -68,6 +72,11 @@ export interface CanvasAccess {
68
72
  noPan?: boolean;
69
73
  }): Promise<FocusNodeResult>;
70
74
  fitView(options?: FitViewOptions): Promise<FitViewResult>;
75
+ getAxState(): Promise<AxStateResult>;
76
+ getAxContext(): Promise<AxContextResult>;
77
+ setAxFocus(nodeIds: string[], options?: {
78
+ source?: PmxAxSource;
79
+ }): Promise<SetAxFocusResult>;
71
80
  clear(): Promise<void>;
72
81
  search(query: string): Promise<SearchResult>;
73
82
  undo(): Promise<UndoRedoResult>;
@@ -0,0 +1,3 @@
1
+ import { type PmxAxContext, type PmxAxPinnedContext } from './ax-state.js';
2
+ export declare function buildCanvasAxPinnedContext(): PmxAxPinnedContext;
3
+ export declare function buildCanvasAxContext(): PmxAxContext;
@@ -0,0 +1,43 @@
1
+ import type { CanvasLayout, CanvasNodeState } from './canvas-state.js';
2
+ import type { AgentContextNode } from './agent-context.js';
3
+ export type PmxAxSource = 'agent' | 'api' | 'browser' | 'cli' | 'codex' | 'copilot' | 'mcp' | 'sdk' | 'system';
4
+ export interface PmxAxFocusState {
5
+ nodeIds: string[];
6
+ primaryNodeId: string | null;
7
+ updatedAt: string | null;
8
+ source: PmxAxSource | null;
9
+ }
10
+ export interface PmxAxState {
11
+ version: 1;
12
+ focus: PmxAxFocusState;
13
+ }
14
+ export interface PmxAxPinnedContext {
15
+ preamble: string;
16
+ nodeIds: string[];
17
+ count: number;
18
+ nodes: AgentContextNode[];
19
+ }
20
+ export interface PmxAxFocusContext extends PmxAxFocusState {
21
+ nodes: AgentContextNode[];
22
+ }
23
+ export interface PmxAxContext {
24
+ version: 1;
25
+ generatedAt: string;
26
+ surface: {
27
+ nodeCount: number;
28
+ edgeCount: number;
29
+ };
30
+ pinned: PmxAxPinnedContext;
31
+ focus: PmxAxFocusContext;
32
+ }
33
+ export declare function createEmptyAxFocusState(): PmxAxFocusState;
34
+ export declare function createEmptyAxState(): PmxAxState;
35
+ export declare function normalizeAxFocusState(input: unknown, validNodeIds?: Set<string>): PmxAxFocusState;
36
+ export declare function normalizeAxState(input: unknown, validNodeIds?: Set<string>): PmxAxState;
37
+ export declare function buildAxContext(input: {
38
+ layout: CanvasLayout;
39
+ pinned: PmxAxPinnedContext;
40
+ focus: PmxAxFocusState;
41
+ focusNodes: AgentContextNode[];
42
+ }): PmxAxContext;
43
+ export declare function nodeSetFromLayout(nodes: CanvasNodeState[]): Set<string>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SQLite persistence layer for canvas state.
3
+ *
4
+ * Uses Bun's built-in `bun:sqlite` for zero-dependency, synchronous,
5
+ * WAL-mode persistence. Replaces the previous JSON file-based approach.
6
+ */
7
+ import { Database } from 'bun:sqlite';
8
+ import type { CanvasAnnotation, CanvasEdge, CanvasNodeState, CanvasSnapshot, CanvasSnapshotListOptions, ViewportState } from './canvas-state.js';
9
+ import { type PmxAxState } from './ax-state.js';
10
+ export type CanvasTheme = 'dark' | 'light' | 'high-contrast';
11
+ export declare function normalizeCanvasTheme(value: unknown, fallback?: CanvasTheme): CanvasTheme;
12
+ export interface PersistedCanvasState {
13
+ version: number;
14
+ theme?: CanvasTheme;
15
+ viewport: ViewportState;
16
+ nodes: CanvasNodeState[];
17
+ edges: CanvasEdge[];
18
+ annotations?: CanvasAnnotation[];
19
+ contextPins: string[];
20
+ ax?: PmxAxState;
21
+ }
22
+ export declare function openCanvasDb(dbPath: string): Database;
23
+ export declare function checkpointCanvasDb(db: Database): void;
24
+ export declare function finalizeCanvasDbForClose(db: Database): void;
25
+ export declare function saveStateToDB(db: Database, state: PersistedCanvasState): void;
26
+ /** Check if the DB has been populated with canvas state at least once. */
27
+ export declare function isDbPopulated(db: Database): boolean;
28
+ export declare function loadStateFromDB(db: Database): PersistedCanvasState | null;
29
+ export declare function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state: PersistedCanvasState): void;
30
+ export declare function loadSnapshotFromDB(db: Database, idOrName: string): {
31
+ snapshot: CanvasSnapshot;
32
+ state: PersistedCanvasState;
33
+ } | null;
34
+ export declare function listSnapshotsFromDB(db: Database, options?: CanvasSnapshotListOptions): CanvasSnapshot[];
35
+ export declare function deleteSnapshotFromDB(db: Database, id: string): boolean;
36
+ export declare function writeBlobToDB(db: Database, sha256: string, jsonValue: string): number;
37
+ export declare function readBlobFromDB(db: Database, sha256: string): string | null;
38
+ export declare function hasBlobInDB(db: Database, sha256: string): boolean;
@@ -5,9 +5,12 @@
5
5
  * - Agent tools (Phase 3) can read/mutate canvas state
6
6
  * - Client syncs bidirectionally (SSE for server→client, POST for client→server)
7
7
  *
8
- * Persistence: canvas state auto-saves to `.pmx-canvas/state.json` in the
9
- * workspace root on every mutation (debounced). Auto-loads on `loadFromDisk()`.
8
+ * Persistence: canvas state auto-saves to `.pmx-canvas/canvas.db` (SQLite WAL mode)
9
+ * in the workspace root on every mutation (debounced). Auto-loads on `loadFromDisk()`.
10
+ * Legacy `.pmx-canvas/state.json` is auto-migrated on first boot.
10
11
  */
12
+ import { type PersistedCanvasState, type CanvasTheme } from './canvas-db.js';
13
+ import { type PmxAxFocusState, type PmxAxSource, type PmxAxState } from './ax-state.js';
11
14
  export declare const PMX_CANVAS_DIR = ".pmx-canvas";
12
15
  export interface PersistedBlobRef {
13
16
  __pmxCanvasBlob: 'v1';
@@ -17,14 +20,7 @@ export interface PersistedBlobRef {
17
20
  bytes: number;
18
21
  jsonBytes: number;
19
22
  }
20
- interface PersistedCanvasState {
21
- version: number;
22
- viewport: ViewportState;
23
- nodes: CanvasNodeState[];
24
- edges: CanvasEdge[];
25
- annotations?: CanvasAnnotation[];
26
- contextPins: string[];
27
- }
23
+ export type { PersistedCanvasState } from './canvas-db.js';
28
24
  interface LoadFromDiskOptions {
29
25
  clearExisting?: boolean;
30
26
  }
@@ -106,6 +102,7 @@ export interface CanvasAnnotation {
106
102
  }
107
103
  export interface CanvasLayout {
108
104
  viewport: ViewportState;
105
+ theme: CanvasTheme;
109
106
  nodes: CanvasNodeState[];
110
107
  edges: CanvasEdge[];
111
108
  annotations: CanvasAnnotation[];
@@ -123,9 +120,9 @@ export interface CanvasNodeUpdate {
123
120
  collapsed?: boolean;
124
121
  dockPosition?: 'left' | 'right' | null;
125
122
  }
126
- export type CanvasChangeType = 'pins' | 'nodes';
123
+ export type CanvasChangeType = 'pins' | 'nodes' | 'ax';
127
124
  export interface MutationRecordInfo {
128
- operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
125
+ operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'setAxFocus' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
129
126
  description: string;
130
127
  forward: () => void;
131
128
  inverse: () => void;
@@ -143,7 +140,9 @@ declare class CanvasStateManager {
143
140
  private edges;
144
141
  private annotations;
145
142
  private _viewport;
143
+ private _theme;
146
144
  private _contextPinnedNodeIds;
145
+ private _axState;
147
146
  private _workspaceRoot;
148
147
  private _changeListeners;
149
148
  /** Register a listener for state changes. Used by MCP server to emit resource notifications. */
@@ -158,6 +157,9 @@ declare class CanvasStateManager {
158
157
  /** Create a closure that runs with recording suppressed. */
159
158
  private suppressed;
160
159
  private recordMutation;
160
+ private currentNodeIdSet;
161
+ private normalizeAxForCurrentNodes;
162
+ private applyAxState;
161
163
  private applyResolvedGroupBounds;
162
164
  private getGroupSnapshot;
163
165
  private normalizeNode;
@@ -166,6 +168,7 @@ declare class CanvasStateManager {
166
168
  private recomputeParentGroupBounds;
167
169
  private compactGroupChildren;
168
170
  private _stateFilePath;
171
+ private _db;
169
172
  private _saveTimer;
170
173
  /** Set the workspace root to enable auto-persistence. */
171
174
  setWorkspaceRoot(workspaceRoot: string): void;
@@ -185,15 +188,22 @@ declare class CanvasStateManager {
185
188
  * No-op when the new layout already exists.
186
189
  */
187
190
  private migrateLegacyLayout;
191
+ /**
192
+ * One-time migration: import state.json + snapshot JSON files + blob files
193
+ * into the SQLite database. Renames originals to `.bak`.
194
+ */
195
+ private migrateJsonToSqlite;
188
196
  getWorkspaceRoot(): string;
189
197
  private emptyPersistedState;
190
- /** Load canvas state from disk. Call once on server startup. */
198
+ /** Load canvas state from SQLite (or legacy JSON fallback). Call once on server startup. */
191
199
  loadFromDisk(options?: LoadFromDiskOptions): boolean;
192
- /** Debounced save — coalesces rapid mutations into a single disk write. */
200
+ /** Debounced save — coalesces rapid mutations into a single write. */
193
201
  private scheduleSave;
194
202
  flushToDisk(): void;
195
- /** Write current state to disk immediately. */
203
+ /** Write current state to SQLite immediately. */
196
204
  private saveToDisk;
205
+ /** Close the SQLite database cleanly. Call on server shutdown. */
206
+ close(): void;
197
207
  private get snapshotsDir();
198
208
  private applyPersistedState;
199
209
  private readResolvedSnapshot;
@@ -217,6 +227,8 @@ declare class CanvasStateManager {
217
227
  } | null;
218
228
  /** Delete a snapshot. */
219
229
  deleteSnapshot(id: string): boolean;
230
+ /** Remove all snapshots from the DB. Used by test teardown. */
231
+ clearAllSnapshots(): void;
220
232
  get viewport(): ViewportState;
221
233
  addNode(node: CanvasNodeState): void;
222
234
  addJsonRenderNode(node: CanvasNodeState): void;
@@ -240,7 +252,16 @@ declare class CanvasStateManager {
240
252
  skipped: number;
241
253
  };
242
254
  setViewport(v: Partial<ViewportState>): void;
255
+ get theme(): CanvasTheme;
256
+ setTheme(theme: CanvasTheme): CanvasTheme;
243
257
  get contextPinnedNodeIds(): Set<string>;
258
+ getAxState(): PmxAxState;
259
+ getAxFocus(): PmxAxFocusState;
260
+ setAxFocus(nodeIds: string[], options?: {
261
+ source?: PmxAxSource;
262
+ recordHistory?: boolean;
263
+ }): PmxAxFocusState;
264
+ clearAxFocus(): PmxAxFocusState;
244
265
  setContextPins(nodeIds: string[]): void;
245
266
  clearContextPins(): void;
246
267
  /** Move child nodes into a group. Sets data.parentGroup on children and data.children on the group. */
@@ -250,4 +271,3 @@ declare class CanvasStateManager {
250
271
  clear(): void;
251
272
  }
252
273
  export declare const canvasState: CanvasStateManager;
253
- export {};
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
3
+ import type { PmxAxContext, PmxAxFocusState, PmxAxSource, PmxAxState } from './ax-state.js';
3
4
  import { searchNodes } from './spatial-analysis.js';
4
5
  import { diffLayouts } from './mutation-history.js';
5
6
  import { fitCanvasView, gcCanvasSnapshots, listCanvasSnapshots } from './canvas-operations.js';
@@ -104,6 +105,11 @@ export declare class PmxCanvas extends EventEmitter {
104
105
  focused: string;
105
106
  panned: boolean;
106
107
  } | null;
108
+ getAxState(): PmxAxState;
109
+ getAxContext(): PmxAxContext;
110
+ setAxFocus(nodeIds: string[], options?: {
111
+ source?: PmxAxSource;
112
+ }): PmxAxFocusState;
107
113
  fitView(options?: {
108
114
  width?: number;
109
115
  height?: number;
@@ -12,7 +12,7 @@
12
12
  * - _replaying flag prevents undo/redo from recording new entries
13
13
  */
14
14
  import type { CanvasNodeState, CanvasEdge } from './canvas-state.js';
15
- export type MutationOp = 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'arrange' | 'restoreSnapshot' | 'setPins' | 'batch' | 'viewport' | 'groupNodes' | 'ungroupNodes';
15
+ export type MutationOp = 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'arrange' | 'restoreSnapshot' | 'setPins' | 'setAxFocus' | 'batch' | 'viewport' | 'groupNodes' | 'ungroupNodes';
16
16
  export interface MutationEntry {
17
17
  id: string;
18
18
  timestamp: string;
package/docs/cli.md CHANGED
@@ -112,6 +112,19 @@ pmx-canvas focus <node-id> # Pan viewport to a node
112
112
  pmx-canvas focus <node-id> --no-pan # Select/raise without panning
113
113
  ```
114
114
 
115
+ ## AX context
116
+
117
+ AX commands expose the host-agnostic context contract used by adapters. Pins
118
+ remain the explicit human-curated context set; AX focus is the current
119
+ attention target and persists with canvas state and snapshots.
120
+
121
+ ```bash
122
+ pmx-canvas ax status # Persisted AX state
123
+ pmx-canvas ax context # Pinned + focused agent context
124
+ pmx-canvas ax focus node-1 node-2 # Set AX focus
125
+ pmx-canvas ax focus --clear # Clear AX focus
126
+ ```
127
+
115
128
  ## WebView automation
116
129
 
117
130
  Drive a headless Bun.WebView (Chromium or WebKit) pointed at the workbench:
package/docs/http-api.md CHANGED
@@ -93,6 +93,30 @@ curl -X POST http://localhost:4313/api/canvas/context-pins \
93
93
  curl http://localhost:4313/api/canvas/pinned-context
94
94
  ```
95
95
 
96
+ ## AX context and focus
97
+
98
+ AX context is the host-agnostic agent-experience layer. It combines existing
99
+ context pins with a persisted focus node set that adapters can inject into
100
+ their native prompt/context hooks.
101
+
102
+ ```bash
103
+ # Get persisted AX state
104
+ curl http://localhost:4313/api/canvas/ax
105
+
106
+ # Get agent-readable pinned + focused context
107
+ curl http://localhost:4313/api/canvas/ax/context
108
+
109
+ # Set AX focus
110
+ curl -X POST http://localhost:4313/api/canvas/ax/focus \
111
+ -H "Content-Type: application/json" \
112
+ -d '{"nodeIds":["node-1"],"source":"api"}'
113
+
114
+ # Patch AX focus through the state endpoint
115
+ curl -X PATCH http://localhost:4313/api/canvas/ax \
116
+ -H "Content-Type: application/json" \
117
+ -d '{"focus":{"nodeIds":["node-1"],"source":"api"}}'
118
+ ```
119
+
96
120
  ## Diagrams (Excalidraw preset)
97
121
 
98
122
  ```bash
package/docs/mcp.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MCP reference
2
2
 
3
- PMX Canvas ships an MCP stdio server with **42 tools** + **8 core resources**,
3
+ PMX Canvas ships an MCP stdio server with **45 tools** + **9 core resources**,
4
4
  plus per-skill resources at `canvas://skills/<name>`. The server emits
5
5
  `notifications/resources/updated` when canvas state changes — humans pin
6
6
  nodes in the browser, agents are notified immediately.
@@ -51,6 +51,8 @@ searchable and readable in pinned/spatial context.
51
51
  | `canvas_arrange` | Auto-arrange (grid/column/flow) |
52
52
  | `canvas_validate` | Validate collisions, containment, and missing edge endpoints |
53
53
  | `canvas_focus_node` | Pan viewport to a node; use CLI `focus --no-pan` when you only need to select/raise |
54
+ | `canvas_get_ax` | Read the PMX AX state plus pinned/focused context |
55
+ | `canvas_set_ax_focus` | Set the host-agnostic AX focus node set; adapters can pass a source such as `codex` |
54
56
  | `canvas_pin_nodes` | Pin nodes to include in agent context |
55
57
  | `canvas_clear` | Clear all nodes and edges |
56
58
  | `canvas_snapshot` | Save current canvas as a named snapshot |
@@ -80,6 +82,8 @@ Individual bundled skills are also readable at `canvas://skills/<name>`.
80
82
  | Resource | Description |
81
83
  |----------|-------------|
82
84
  | `canvas://pinned-context` | Content of pinned nodes + nearby unpinned neighbors |
85
+ | `canvas://ax` | PMX AX state, currently including persisted focus |
86
+ | `canvas://ax-context` | Agent-readable pinned and focused AX context |
83
87
  | `canvas://schema` | Running-server create schemas and json-render catalog metadata |
84
88
  | `canvas://layout` | Full canvas state (all nodes, edges, viewport) |
85
89
  | `canvas://summary` | Compact overview: counts, pinned titles, viewport |
@@ -93,13 +97,27 @@ Individual bundled skills are also readable at `canvas://skills/<name>`.
93
97
  The MCP server emits `notifications/resources/updated` whenever canvas state
94
98
  changes:
95
99
 
96
- - Pin changes notify `canvas://pinned-context`
100
+ - Pin changes notify `canvas://pinned-context`, `canvas://ax`, and `canvas://ax-context`
101
+ - AX focus changes notify `canvas://ax` and `canvas://ax-context`
97
102
  - All mutations notify `canvas://layout`, `canvas://summary`,
98
103
  `canvas://spatial-context`, `canvas://history`, and `canvas://code-graph`
99
104
 
100
105
  This closes the human-to-agent loop: spatial curation in the browser becomes
101
106
  an immediate signal in the agent's context.
102
107
 
108
+ ## Codex App Adapter
109
+
110
+ In the Codex app, PMX Canvas runs natively through the existing Codex surfaces:
111
+ MCP for tools/resources and the in-app Browser for the live `/workbench` view.
112
+ No separate PMX renderer is needed. Prefer MCP over the CLI for Codex-native
113
+ operation; keep the CLI for fallback scripts and manual debugging.
114
+
115
+ Use `canvas://ax-context` or `canvas_get_ax` to read pinned/focused context.
116
+ When Codex-hosted steering sets the current attention target, call
117
+ `canvas_set_ax_focus` with `source: "codex"` so the AX state records where the
118
+ focus came from. The full workflow lives in
119
+ `skills/pmx-canvas/references/codex-app-adapter.md`.
120
+
103
121
  ## Annotation Visibility
104
122
 
105
123
  Human-drawn canvas annotations are rendered as browser SVG ink. MCP resources