pmx-canvas 0.7.3 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/.github/extensions/pmx-canvas/extension.mjs +25 -172
- package/.github/extensions/pmx-canvas/mcp-bootstrap.ts +198 -0
- package/.github/extensions/pmx-canvas/runtime.d.mts +41 -0
- package/.github/extensions/pmx-canvas/runtime.json +3 -0
- package/.github/extensions/pmx-canvas/runtime.mjs +194 -0
- package/.github/extensions/pmx-canvas/start-mcp.mjs +51 -0
- package/.github/extensions/pmx-canvas/tools.json +2560 -0
- package/CHANGELOG.md +64 -0
- package/Readme.md +21 -7
- package/dist/canvas/fonts/OFL.txt +93 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/dist/canvas/global.css +726 -674
- package/dist/canvas/index.js +90 -88
- package/dist/canvas/mcp-app.html +7879 -0
- package/dist/canvas/mermaid-entry.js +1 -1
- package/dist/canvas/surface-theme.css +151 -46
- package/dist/json-render/index.css +81 -1
- package/dist/json-render/index.js +1 -1
- package/dist/types/cli/commands/record.d.ts +1 -1
- package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
- package/dist/types/client/icons.d.ts +1 -2
- package/dist/types/client/mcp-app/index.d.ts +1 -0
- package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
- package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
- package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
- package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
- package/dist/types/client/state/attention-store.d.ts +0 -5
- package/dist/types/client/state/context-status-store.d.ts +53 -0
- package/dist/types/client/state/intent-bridge.d.ts +3 -0
- package/dist/types/client/state/near-pin-store.d.ts +16 -0
- package/dist/types/client/state/presence-store.d.ts +5 -0
- package/dist/types/client/state/session-store.d.ts +33 -3
- package/dist/types/client/state/workbench-transport.d.ts +16 -0
- package/dist/types/client/theme/tokens.d.ts +1 -3
- package/dist/types/client/types.d.ts +9 -0
- package/dist/types/json-render/server.d.ts +1 -1
- package/dist/types/mcp/app-server.d.ts +3 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -1
- package/dist/types/mcp/canvas-app.d.ts +4 -0
- package/dist/types/mcp/workbench-app.d.ts +4 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/ax-state.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +8 -0
- package/dist/types/server/canvas-geometry.d.ts +25 -0
- package/dist/types/server/canvas-operations.d.ts +1 -11
- package/dist/types/server/canvas-state.d.ts +25 -3
- package/dist/types/server/context-brief.d.ts +8 -1
- package/dist/types/server/context-reads.d.ts +30 -7
- package/dist/types/server/index.d.ts +6 -1
- package/dist/types/server/mcp-app-runtime.d.ts +1 -0
- package/dist/types/server/operations/http.d.ts +2 -0
- package/dist/types/server/operations/invoker.d.ts +4 -2
- package/dist/types/server/operations/ops/nodes.d.ts +0 -6
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/spatial-analysis.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +22 -0
- package/dist/types/shared/agent-presence.d.ts +3 -10
- package/dist/types/shared/canvas-app.d.ts +54 -0
- package/dist/types/shared/themes.d.ts +1 -1
- package/docs/RELEASE.md +7 -3
- package/docs/agent-plugin.md +123 -0
- package/docs/ax-state-contract.md +40 -0
- package/docs/cli.md +7 -2
- package/docs/design/AgentContext.dc.html +186 -0
- package/docs/design/Before.dc.html +208 -0
- package/docs/design/BoardPins.dc.html +151 -0
- package/docs/design/Chrome.dc.html +211 -0
- package/docs/design/Context.dc.html +239 -0
- package/docs/design/Flows.dc.html +181 -0
- package/docs/design/Home.dc.html +209 -0
- package/docs/design/HomeGraph.dc.html +211 -0
- package/docs/design/HomeMap.dc.html +117 -0
- package/docs/design/HomeTeam.dc.html +176 -0
- package/docs/design/Light.dc.html +28 -0
- package/docs/design/Main.dc.html +280 -0
- package/docs/design/NearPin.dc.html +260 -0
- package/docs/design/Pane600.dc.html +120 -0
- package/docs/design/Relations.dc.html +297 -0
- package/docs/design/Themes.dc.html +121 -0
- package/docs/design/TokenSystem.dc.html +136 -0
- package/docs/design/Tokens.dc.html +163 -0
- package/docs/design/Types1.dc.html +177 -0
- package/docs/design/Types2.dc.html +226 -0
- package/docs/design/Types3.dc.html +195 -0
- package/docs/design/Uses.dc.html +152 -0
- package/docs/design/Vision.dc.html +116 -0
- package/docs/design/Zoom.dc.html +82 -0
- package/docs/design/canvas.json +274 -0
- package/docs/design.md +217 -0
- package/docs/environment.md +1 -1
- package/docs/http-api.md +86 -16
- package/docs/mcp.md +202 -4
- package/docs/node-types.md +34 -10
- package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
- package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
- package/docs/product-vision-2026-09.md +47 -7
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +1 -0
- package/package.json +10 -4
- package/plugins/pmx-canvas/LICENSE +21 -0
- package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/plugins/pmx-canvas/assets/preview.png +0 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/plugins/pmx-canvas/mcp.json +12 -0
- package/plugins/pmx-canvas/plugin.json +24 -0
- package/plugins/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/start-mcp.mjs +51 -0
- package/plugins/pmx-canvas/tools.json +2560 -0
- package/skills/pmx-canvas/SKILL.md +29 -9
- package/skills/pmx-canvas/evals/evals.json +62 -0
- package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
- package/skills/pmx-canvas/references/full-reference.md +11 -7
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas-testing/SKILL.md +5 -4
- package/src/cli/commands/copilot.ts +5 -0
- package/src/cli/commands/nodes.ts +3 -2
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +10 -4
- package/src/client/App.tsx +6 -1
- package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
- package/src/client/canvas/AnnotationLayer.tsx +1 -1
- package/src/client/canvas/CanvasNode.tsx +32 -49
- package/src/client/canvas/CanvasViewport.tsx +0 -2
- package/src/client/canvas/CommandPalette.tsx +3 -2
- package/src/client/canvas/ContextMenu.tsx +11 -0
- package/src/client/canvas/EdgeLayer.tsx +1 -1
- package/src/client/canvas/ExportDialog.tsx +33 -6
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/Minimap.tsx +12 -7
- package/src/client/canvas/NodeContextMark.tsx +156 -0
- package/src/client/canvas/SessionPanel.tsx +4 -7
- package/src/client/canvas/SessionReceipt.tsx +114 -17
- package/src/client/canvas/ShortcutOverlay.tsx +6 -1
- package/src/client/canvas/SnapshotPanel.tsx +11 -2
- package/src/client/canvas/ToolRail.tsx +12 -9
- package/src/client/canvas/TopBar.tsx +16 -11
- package/src/client/icons.tsx +2 -12
- package/src/client/mcp-app/index.tsx +137 -0
- package/src/client/mcp-app/style.css +10 -0
- package/src/client/nodes/AttachmentNode.tsx +8 -1
- package/src/client/nodes/AxStepControls.tsx +1 -1
- package/src/client/nodes/ContextNode.tsx +9 -9
- package/src/client/nodes/ExtAppFrame.tsx +13 -8
- package/src/client/nodes/FileNode.tsx +8 -5
- package/src/client/nodes/HtmlNode.tsx +12 -2
- package/src/client/nodes/ImageNode.tsx +4 -2
- package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
- package/src/client/nodes/LedgerNode.tsx +2 -2
- package/src/client/nodes/MarkdownNode.tsx +44 -9
- package/src/client/nodes/McpAppNode.tsx +6 -4
- package/src/client/nodes/PromptNode.tsx +5 -17
- package/src/client/nodes/ResponseNode.tsx +3 -15
- package/src/client/nodes/StatusNode.tsx +2 -2
- package/src/client/nodes/StatusSummary.tsx +3 -1
- package/src/client/nodes/TraceNode.tsx +6 -4
- package/src/client/nodes/WebpageNode.tsx +1 -1
- package/src/client/nodes/ax-node-actions.ts +2 -2
- package/src/client/nodes/iframe-document-url.ts +3 -2
- package/src/client/nodes/markdown-sanitize.ts +10 -0
- package/src/client/nodes/surface-url.ts +10 -0
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/nodes/use-workbench-asset.ts +26 -0
- package/src/client/state/attention-bridge.ts +0 -8
- package/src/client/state/attention-store.ts +0 -17
- package/src/client/state/canvas-store.ts +4 -2
- package/src/client/state/context-status-store.ts +66 -0
- package/src/client/state/human-store.ts +2 -0
- package/src/client/state/iframe-mode.ts +5 -0
- package/src/client/state/intent-bridge.ts +10 -4
- package/src/client/state/near-pin-store.ts +53 -0
- package/src/client/state/presence-store.ts +5 -10
- package/src/client/state/session-store.ts +106 -9
- package/src/client/state/sse-bridge.ts +26 -7
- package/src/client/state/theme-override.ts +1 -1
- package/src/client/state/workbench-transport.ts +36 -0
- package/src/client/theme/global.css +726 -674
- package/src/client/theme/surface-theme.css +151 -46
- package/src/client/theme/tokens.ts +7 -11
- package/src/client/types.ts +10 -0
- package/src/json-render/renderer/index.css +6 -1
- package/src/json-render/renderer/index.tsx +0 -1
- package/src/json-render/server.ts +71 -14
- package/src/mcp/app-server.ts +64 -0
- package/src/mcp/canvas-access.ts +14 -11
- package/src/mcp/canvas-app.ts +318 -0
- package/src/mcp/server.ts +1 -1
- package/src/mcp/workbench-app.ts +211 -0
- package/src/server/agent-presence.ts +45 -6
- package/src/server/ax-context.ts +1 -1
- package/src/server/ax-state.ts +1 -1
- package/src/server/board-export.ts +5 -2
- package/src/server/canvas-db.ts +46 -12
- package/src/server/canvas-geometry.ts +64 -0
- package/src/server/canvas-operations.ts +7 -25
- package/src/server/canvas-state.ts +105 -15
- package/src/server/canvas-validation.ts +19 -4
- package/src/server/context-brief.ts +39 -2
- package/src/server/context-reads.ts +87 -15
- package/src/server/demo-state.json +31 -31
- package/src/server/html-surface.ts +4 -5
- package/src/server/index.ts +41 -5
- package/src/server/mcp-app-runtime.ts +11 -0
- package/src/server/operations/composites.ts +4 -3
- package/src/server/operations/http.ts +4 -4
- package/src/server/operations/invoker.ts +13 -4
- package/src/server/operations/mcp.ts +6 -0
- package/src/server/operations/ops/ax-read.ts +1 -1
- package/src/server/operations/ops/ax-timeline.ts +37 -0
- package/src/server/operations/ops/boards.ts +1 -1
- package/src/server/operations/ops/canvas-wire.ts +3 -6
- package/src/server/operations/ops/intent.ts +31 -2
- package/src/server/operations/ops/json-render.ts +6 -2
- package/src/server/operations/ops/nodes.ts +8 -76
- package/src/server/operations/ops/query.ts +8 -2
- package/src/server/operations/ops/snapshots.ts +4 -1
- package/src/server/operations/ops/viewport.ts +29 -5
- package/src/server/operations/registry.ts +41 -5
- package/src/server/scope-fence.ts +55 -10
- package/src/server/server.ts +83 -5
- package/src/server/spatial-analysis.ts +2 -2
- package/src/server/workboard.ts +6 -1
- package/src/{server → shared}/agent-context.ts +2 -2
- package/src/shared/agent-presence.ts +3 -15
- package/src/shared/canvas-app.ts +62 -0
- package/src/shared/themes.ts +5 -5
- package/src/client/canvas/FocusFieldLayer.tsx +0 -116
- package/src/client/canvas/kind-colors.ts +0 -23
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
|
|
3
|
+
|
|
4
|
+
/** Asset bytes travel through the host, not a localhost URL in the user's browser. */
|
|
5
|
+
export function useWorkbenchAsset(path: string): string | undefined {
|
|
6
|
+
const hosted = isHostedWorkbench() && path.startsWith('/');
|
|
7
|
+
const [asset, setAsset] = useState<{ path: string; url: string } | null>(null);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!hosted) return;
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
void workbenchFetch(path)
|
|
12
|
+
.then(async (response) => {
|
|
13
|
+
if (!response.ok) throw new Error(`Asset read failed: ${response.status}`);
|
|
14
|
+
const reader = new FileReader();
|
|
15
|
+
reader.onload = () => {
|
|
16
|
+
if (!cancelled && typeof reader.result === 'string') setAsset({ path, url: reader.result });
|
|
17
|
+
};
|
|
18
|
+
reader.readAsDataURL(await response.blob());
|
|
19
|
+
})
|
|
20
|
+
.catch((error: unknown) => console.error('[workbench-asset]', error));
|
|
21
|
+
return () => {
|
|
22
|
+
cancelled = true;
|
|
23
|
+
};
|
|
24
|
+
}, [path, hosted]);
|
|
25
|
+
return hosted ? (asset?.path === path ? asset.url : undefined) : path;
|
|
26
|
+
}
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
pushAttentionHistory,
|
|
13
13
|
resetAttentionState,
|
|
14
|
-
setAttentionFocus,
|
|
15
14
|
setAttentionPulse,
|
|
16
15
|
setAttentionToast,
|
|
17
16
|
type AttentionEntry,
|
|
@@ -242,11 +241,6 @@ function entryFromEvent(event: SemanticWatchEvent): AttentionEntry | null {
|
|
|
242
241
|
}
|
|
243
242
|
}
|
|
244
243
|
|
|
245
|
-
function applyAttentionSnapshot(): void {
|
|
246
|
-
const snapshot = reducer.getAttentionSnapshot();
|
|
247
|
-
setAttentionFocus(snapshot.primaryFocusNodeIds, snapshot.secondaryFocusNodeIds, snapshot.regions);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
244
|
function flushToastQueue(): void {
|
|
251
245
|
if (toastTimer !== null) return;
|
|
252
246
|
const next = toastQueue.shift() ?? null;
|
|
@@ -322,8 +316,6 @@ export function syncAttentionFromSse(message: SseMessage): void {
|
|
|
322
316
|
.map((event) => entryFromEvent(event))
|
|
323
317
|
.filter((entry): entry is AttentionEntry => entry !== null);
|
|
324
318
|
|
|
325
|
-
applyAttentionSnapshot();
|
|
326
|
-
|
|
327
319
|
for (const entry of entries) {
|
|
328
320
|
if (shouldSuppressEntry(entry)) continue;
|
|
329
321
|
pushAttentionHistory(entry);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { signal } from '@preact/signals';
|
|
2
|
-
import type { SemanticAttentionRegion } from '../../shared/semantic-attention.js';
|
|
3
2
|
|
|
4
3
|
export type AttentionTone = 'context' | 'relationship' | 'group' | 'cluster' | 'neighborhood' | 'remove';
|
|
5
4
|
|
|
@@ -14,9 +13,6 @@ export interface AttentionEntry {
|
|
|
14
13
|
|
|
15
14
|
export const attentionToast = signal<AttentionEntry | null>(null);
|
|
16
15
|
export const attentionHistory = signal<AttentionEntry[]>([]);
|
|
17
|
-
export const attentionPrimaryNodeIds = signal<Set<string>>(new Set());
|
|
18
|
-
export const attentionSecondaryNodeIds = signal<Set<string>>(new Set());
|
|
19
|
-
export const attentionRegions = signal<SemanticAttentionRegion[]>([]);
|
|
20
16
|
export const attentionPulseNodeIds = signal<Set<string>>(new Set());
|
|
21
17
|
export const attentionHistoryOpen = signal<boolean>(false);
|
|
22
18
|
export const attentionHistoryUnread = signal<number>(0);
|
|
@@ -24,9 +20,6 @@ export const attentionHistoryUnread = signal<number>(0);
|
|
|
24
20
|
export function resetAttentionState(): void {
|
|
25
21
|
attentionToast.value = null;
|
|
26
22
|
attentionHistory.value = [];
|
|
27
|
-
attentionPrimaryNodeIds.value = new Set();
|
|
28
|
-
attentionSecondaryNodeIds.value = new Set();
|
|
29
|
-
attentionRegions.value = [];
|
|
30
23
|
attentionPulseNodeIds.value = new Set();
|
|
31
24
|
attentionHistoryOpen.value = false;
|
|
32
25
|
attentionHistoryUnread.value = 0;
|
|
@@ -41,16 +34,6 @@ export function closeAttentionHistory(): void {
|
|
|
41
34
|
attentionHistoryOpen.value = false;
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
export function setAttentionFocus(
|
|
45
|
-
primaryNodeIds: string[],
|
|
46
|
-
secondaryNodeIds: string[],
|
|
47
|
-
regions: SemanticAttentionRegion[],
|
|
48
|
-
): void {
|
|
49
|
-
attentionPrimaryNodeIds.value = new Set(primaryNodeIds);
|
|
50
|
-
attentionSecondaryNodeIds.value = new Set(secondaryNodeIds);
|
|
51
|
-
attentionRegions.value = regions;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
37
|
export function setAttentionToast(entry: AttentionEntry | null): void {
|
|
55
38
|
attentionToast.value = entry;
|
|
56
39
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { computeAutoArrange } from '../../shared/auto-arrange';
|
|
12
12
|
import { canvasArea, canvasAreaCenter, canvasFitInsets } from '../canvas/canvas-area';
|
|
13
13
|
import { pushCanvasUpdate, requestBestEffort, requestOk, updateViewportFromClient } from './intent-bridge';
|
|
14
|
+
import { isHostedWorkbench } from './workbench-transport';
|
|
14
15
|
|
|
15
16
|
function logCanvasStoreError(action: string, error: unknown): void {
|
|
16
17
|
console.error(`[canvas-store] ${action} failed`, error);
|
|
@@ -39,7 +40,7 @@ export const reconnectAttempt = signal<number>(0);
|
|
|
39
40
|
export const reconnectDelay = signal<number>(0);
|
|
40
41
|
export const sessionId = signal<string>('');
|
|
41
42
|
export const traceEnabled = signal<boolean>(false);
|
|
42
|
-
export const canvasTheme = signal<string>('
|
|
43
|
+
export const canvasTheme = signal<string>('harbor');
|
|
43
44
|
export const hasInitialServerLayout = signal<boolean>(false);
|
|
44
45
|
// Compact AX state snapshot (work items, focus, …) mirrored from the server and
|
|
45
46
|
// pushed into AX-enabled surfaces so authored boards can render the live queue.
|
|
@@ -775,7 +776,8 @@ export function persistLayout(options: { recordHistory?: boolean } = {}): void {
|
|
|
775
776
|
})),
|
|
776
777
|
contextPinnedNodeIds: Array.from(contextPinnedNodeIds.value),
|
|
777
778
|
};
|
|
778
|
-
localStorage
|
|
779
|
+
// Opaque MCP App frames have no localStorage; the daemon persists layout.
|
|
780
|
+
if (!isHostedWorkbench()) localStorage.setItem(STORAGE_KEY, JSON.stringify(layout));
|
|
779
781
|
void pushCanvasUpdate(nodeUpdates, options);
|
|
780
782
|
} catch (error) {
|
|
781
783
|
logCanvasStoreError('persistLayout', error);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { signal } from '@preact/signals';
|
|
2
|
+
import { activeBoardId } from './boards-store';
|
|
3
|
+
import { requestJson } from './intent-bridge';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What agents did with the board's context (docs/design/AgentContext.dc.html):
|
|
7
|
+
* per node, the latest agent read of its content and the revision it read;
|
|
8
|
+
* per pin, who pinned it, when and why. Server truth from
|
|
9
|
+
* `GET /api/canvas/ax/context-status`, refetched when a read lands
|
|
10
|
+
* (`context-status-changed`), when pins change, and on (re)connect.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface NodeReadStatus {
|
|
14
|
+
nodeId: string;
|
|
15
|
+
lastReadAt: string;
|
|
16
|
+
lastReadBy: string;
|
|
17
|
+
readRevision: number;
|
|
18
|
+
readCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ContextPinMeta {
|
|
22
|
+
pinnedBy: { actor: 'human' | 'agent' | 'system' | 'unknown'; source: string; agentId?: string };
|
|
23
|
+
pinnedAt: string;
|
|
24
|
+
reason?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ContextStatusResponse {
|
|
28
|
+
ok?: boolean;
|
|
29
|
+
boardId?: string | null;
|
|
30
|
+
nodes?: NodeReadStatus[];
|
|
31
|
+
pins?: Record<string, ContextPinMeta>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const nodeReadStatus = signal<Map<string, NodeReadStatus>>(new Map());
|
|
35
|
+
export const contextPinMeta = signal<Record<string, ContextPinMeta>>({});
|
|
36
|
+
|
|
37
|
+
export function applyContextStatus(response: ContextStatusResponse): void {
|
|
38
|
+
nodeReadStatus.value = new Map((response.nodes ?? []).map((entry) => [entry.nodeId, entry]));
|
|
39
|
+
contextPinMeta.value = response.pins ?? {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function refreshContextStatus(): Promise<void> {
|
|
43
|
+
const response = await requestJson<ContextStatusResponse>('fetchContextStatus', '/api/canvas/ax/context-status', {});
|
|
44
|
+
// A late answer for a board you have since left must not mark this one.
|
|
45
|
+
if (response.ok && response.boardId === activeBoardId.value) applyContextStatus(response);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** How an agent's context relates to one pinned node — the mark its header shows. */
|
|
49
|
+
export type PinnedReadState =
|
|
50
|
+
| { kind: 'not-read' }
|
|
51
|
+
| { kind: 'read'; by: string; at: string; count: number }
|
|
52
|
+
| { kind: 'changed'; by: string; at: string };
|
|
53
|
+
|
|
54
|
+
export function pinnedReadState(nodeId: string, currentRevision: number): PinnedReadState {
|
|
55
|
+
const read = nodeReadStatus.value.get(nodeId);
|
|
56
|
+
const pinnedAt = contextPinMeta.value[nodeId]?.pinnedAt;
|
|
57
|
+
// Pinned after the latest read: the agent has not loaded it since you pinned it.
|
|
58
|
+
if (!read || (pinnedAt && read.lastReadAt < pinnedAt)) return { kind: 'not-read' };
|
|
59
|
+
if (currentRevision > read.readRevision) return { kind: 'changed', by: read.lastReadBy, at: read.lastReadAt };
|
|
60
|
+
return { kind: 'read', by: read.lastReadBy, at: read.lastReadAt, count: read.readCount };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Display name for an attributed writer: its agent id, else its transport label. */
|
|
64
|
+
export function writerName(actor: { source: string; agentId?: string }): string {
|
|
65
|
+
return actor.agentId?.trim() || actor.source;
|
|
66
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from '../../shared/human-presence.js';
|
|
7
7
|
import { requestBestEffort, vetoGhostIntent } from './intent-bridge';
|
|
8
8
|
import { intents, removeIntent } from './intent-store';
|
|
9
|
+
import { isHostedWorkbench } from './workbench-transport';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Human collaborator presence (rail-chrome-v2 phase 8). This tab reports its
|
|
@@ -64,6 +65,7 @@ let lastSentAt = 0;
|
|
|
64
65
|
let pending: ReturnType<typeof setTimeout> | null = null;
|
|
65
66
|
|
|
66
67
|
function post(body: Record<string, unknown>): void {
|
|
68
|
+
if (isHostedWorkbench()) return;
|
|
67
69
|
void requestBestEffort('humanPresence', '/api/canvas/human-presence', {
|
|
68
70
|
method: 'POST',
|
|
69
71
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { signal } from '@preact/signals';
|
|
25
25
|
import { IFRAME_PROBE_MESSAGE_SOURCE } from '../../shared/iframe-probe.js';
|
|
26
|
+
import { isHostedWorkbench } from './workbench-transport';
|
|
26
27
|
|
|
27
28
|
export type IframeMode = 'src' | 'srcdoc';
|
|
28
29
|
|
|
@@ -105,6 +106,10 @@ function isAmpOrbHost(): boolean {
|
|
|
105
106
|
export function resolveIframeMode(
|
|
106
107
|
opts: { embedded?: boolean; ampOrb?: boolean; probeTimeoutMs?: number } = {},
|
|
107
108
|
): Promise<IframeMode> {
|
|
109
|
+
if (isHostedWorkbench()) {
|
|
110
|
+
iframeMode.value = 'srcdoc';
|
|
111
|
+
return Promise.resolve('srcdoc');
|
|
112
|
+
}
|
|
108
113
|
if (iframeMode.value) return Promise.resolve(iframeMode.value);
|
|
109
114
|
if (pending) return pending;
|
|
110
115
|
const forced = forcedIframeMode();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { canvasArea } from '../canvas/canvas-area';
|
|
2
2
|
import { showToast } from './attention-bridge';
|
|
3
|
+
import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
|
|
3
4
|
import type { AgentPresenceSnapshot } from '../../shared/agent-presence.js';
|
|
4
5
|
import type { CanvasEdge } from '../types';
|
|
5
6
|
import { addEdge, canvasInteractionEpoch, nodes } from './canvas-store';
|
|
7
|
+
import { boardsLoaded } from './boards-store';
|
|
6
8
|
function logRequestError(action: string, error: unknown): void {
|
|
7
9
|
console.error(`[intent-bridge] ${action} failed`, error);
|
|
8
10
|
}
|
|
@@ -29,6 +31,7 @@ export const tabAgentKey: string | null = (() => {
|
|
|
29
31
|
* opened with `?agent=<key>` identifies as that agent instead.
|
|
30
32
|
*/
|
|
31
33
|
function withWorkbenchMarker(init?: RequestInit): RequestInit {
|
|
34
|
+
if (isHostedWorkbench()) return init ?? {};
|
|
32
35
|
const headers = { ...(init?.headers as Record<string, string> | undefined) };
|
|
33
36
|
if (tabAgentKey) {
|
|
34
37
|
headers['x-pmx-source'] = tabAgentKey;
|
|
@@ -61,7 +64,7 @@ async function readJsonBody(res: Response): Promise<unknown> {
|
|
|
61
64
|
|
|
62
65
|
export async function requestJson<T>(action: string, url: string, fallback: T, init?: RequestInit): Promise<T> {
|
|
63
66
|
try {
|
|
64
|
-
const res = await
|
|
67
|
+
const res = await workbenchFetch(url, withWorkbenchMarker(init));
|
|
65
68
|
const body = await readJsonBody(res);
|
|
66
69
|
reportRefusedWrite(res, init, body);
|
|
67
70
|
if (body === null) throw new Error(`${res.status} response had no JSON body`);
|
|
@@ -74,7 +77,7 @@ export async function requestJson<T>(action: string, url: string, fallback: T, i
|
|
|
74
77
|
|
|
75
78
|
export async function requestOk(action: string, url: string, init?: RequestInit): Promise<{ ok: boolean }> {
|
|
76
79
|
try {
|
|
77
|
-
const res = await
|
|
80
|
+
const res = await workbenchFetch(url, withWorkbenchMarker(init));
|
|
78
81
|
if (!res.ok) reportRefusedWrite(res, init, await readJsonBody(res));
|
|
79
82
|
return { ok: res.ok };
|
|
80
83
|
} catch (error) {
|
|
@@ -85,7 +88,7 @@ export async function requestOk(action: string, url: string, init?: RequestInit)
|
|
|
85
88
|
|
|
86
89
|
export async function requestBestEffort(action: string, url: string, init?: RequestInit): Promise<void> {
|
|
87
90
|
try {
|
|
88
|
-
const res = await
|
|
91
|
+
const res = await workbenchFetch(url, withWorkbenchMarker(init));
|
|
89
92
|
if (!res.ok) reportRefusedWrite(res, init, await readJsonBody(res));
|
|
90
93
|
} catch (error) {
|
|
91
94
|
logRequestError(action, error);
|
|
@@ -328,8 +331,10 @@ export async function updateNodeFromClient(
|
|
|
328
331
|
data?: Record<string, unknown>;
|
|
329
332
|
/** Groups: replace the membership list (positions preserved). */
|
|
330
333
|
children?: string[];
|
|
334
|
+
expectedContentRevision?: number;
|
|
335
|
+
expectedBoardId?: string | null;
|
|
331
336
|
},
|
|
332
|
-
): Promise<{ ok: boolean; id?: string }> {
|
|
337
|
+
): Promise<{ ok: boolean; id?: string; contentRevision?: number }> {
|
|
333
338
|
return requestJson(
|
|
334
339
|
'updateNodeFromClient',
|
|
335
340
|
`/api/canvas/node/${encodeURIComponent(id)}`,
|
|
@@ -477,6 +482,7 @@ function clientWindowSize(): { clientWidth: number; clientHeight: number } | Rec
|
|
|
477
482
|
* window size before the human ever pans or zooms.
|
|
478
483
|
*/
|
|
479
484
|
export async function reportClientViewportSize(): Promise<void> {
|
|
485
|
+
if (isHostedWorkbench() && !boardsLoaded.value) return;
|
|
480
486
|
const size = clientWindowSize();
|
|
481
487
|
if (!('clientWidth' in size)) return;
|
|
482
488
|
await requestBestEffort('reportClientViewportSize', '/api/canvas/viewport', {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { computed } from '@preact/signals';
|
|
2
|
+
import { findNeighborhoods } from '../../server/spatial-analysis.js';
|
|
3
|
+
import { contextPinnedNodeIds, nodes } from './canvas-store';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Near a pin (docs/design/NearPin.dc.html): each pinned node pulls up to five
|
|
7
|
+
* unpinned nodes within 600 px into the agent's brief as title + short
|
|
8
|
+
* summary. Same rule as the server's brief (`findNeighborhoods`), so the mark
|
|
9
|
+
* claims exactly what the agent receives.
|
|
10
|
+
*/
|
|
11
|
+
export interface NearPin {
|
|
12
|
+
pinNodeId: string;
|
|
13
|
+
pinTitle: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let lastKey = '';
|
|
17
|
+
let lastNear = new Map<string, NearPin[]>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Unpinned node id → the pins it is near, nearest first. Every drag frame
|
|
21
|
+
* moves a node, so the result keeps its identity while no node's set of pins
|
|
22
|
+
* changes: the chips subscribed to it re-render only when a neighbourhood does.
|
|
23
|
+
*/
|
|
24
|
+
export const nearPins = computed<Map<string, NearPin[]>>(() => {
|
|
25
|
+
const pins = contextPinnedNodeIds.value;
|
|
26
|
+
const near = new Map<string, NearPin[]>();
|
|
27
|
+
if (pins.size > 0) {
|
|
28
|
+
const nearest = new Map<string, Array<NearPin & { distance: number }>>();
|
|
29
|
+
for (const neighborhood of findNeighborhoods([...nodes.value.values()], pins)) {
|
|
30
|
+
const pinTitle = neighborhood.pinnedNodeTitle ?? neighborhood.pinnedNodeId;
|
|
31
|
+
for (const neighbor of neighborhood.neighbors) {
|
|
32
|
+
const list = nearest.get(neighbor.id) ?? [];
|
|
33
|
+
list.push({ pinNodeId: neighborhood.pinnedNodeId, pinTitle, distance: neighbor.distance });
|
|
34
|
+
nearest.set(neighbor.id, list);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const [id, list] of nearest) {
|
|
38
|
+
list.sort((a, b) => a.distance - b.distance);
|
|
39
|
+
near.set(
|
|
40
|
+
id,
|
|
41
|
+
list.map(({ pinNodeId, pinTitle }) => ({ pinNodeId, pinTitle })),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const key = [...near]
|
|
46
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
47
|
+
.map(([id, list]) => `${id}:${list.map((pin) => `${pin.pinNodeId}=${pin.pinTitle}`).join(',')}`)
|
|
48
|
+
.join('|');
|
|
49
|
+
if (key === lastKey) return lastNear;
|
|
50
|
+
lastKey = key;
|
|
51
|
+
lastNear = near;
|
|
52
|
+
return near;
|
|
53
|
+
});
|
|
@@ -32,18 +32,13 @@ export const writersSheetOpen = signal(false);
|
|
|
32
32
|
export const activityFilter = signal<string | null>(null);
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* A writer's colour says what it is, not who: agents take the agent violet,
|
|
36
|
+
* subagents (fleet workers) the subagent fuchsia. Writers are told apart by
|
|
37
|
+
* name and initial (docs/design.md, "Kinds carry no colour").
|
|
38
38
|
*/
|
|
39
|
-
const WRITER_PALETTE = ['var(--c-purple)', 'var(--c-accent)', 'var(--c-ok)', 'var(--c-warn)', 'var(--c-danger)'];
|
|
40
|
-
const writerColors = new Map<string, string>();
|
|
41
39
|
export function writerColor(sessionId: string): string {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const color = WRITER_PALETTE[writerColors.size % WRITER_PALETTE.length]!;
|
|
45
|
-
writerColors.set(sessionId, color);
|
|
46
|
-
return color;
|
|
40
|
+
const presence = agentPresences.value.find((entry) => entry.sessionId === sessionId);
|
|
41
|
+
return presence?.parentAgentId ? 'var(--c-subagent)' : 'var(--c-agent)';
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
/** Avatar initial: first letter of the label, upper-cased. */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isHostedWorkbench } from './workbench-transport';
|
|
2
|
+
import { showToast } from './attention-bridge';
|
|
1
3
|
import { computed, signal } from '@preact/signals';
|
|
2
4
|
import type { AxApprovalStatus, AxEventKind, AxWorkItemStatus } from '../../shared/ax-kinds.js';
|
|
3
5
|
import { HUMAN_STARTED_SESSION_LABEL } from '../../shared/agent-presence.js';
|
|
@@ -207,7 +209,7 @@ export function mergeTimeline(
|
|
|
207
209
|
// Activity entries are already attributed server-side — the sessionId is
|
|
208
210
|
// the identity key (hue-stable across chrome), the label its display name.
|
|
209
211
|
who: write.sessionId ?? null,
|
|
210
|
-
...(top?.actor === 'agent' && newestWrite?.id === write.id ? { undoable: true } : {}),
|
|
212
|
+
...(!isHostedWorkbench() && top?.actor === 'agent' && newestWrite?.id === write.id ? { undoable: true } : {}),
|
|
211
213
|
})),
|
|
212
214
|
...timeline.events.map((event) => ({
|
|
213
215
|
id: `event-${event.id}`,
|
|
@@ -298,6 +300,10 @@ export async function undoAgentEdit(entry: TimelineEntry): Promise<boolean> {
|
|
|
298
300
|
|
|
299
301
|
/** Ctrl+Z / Ctrl+Shift+Z: whichever op is top of the shared stack, agent or human. */
|
|
300
302
|
export async function undoFromKeyboard(redo = false): Promise<boolean> {
|
|
303
|
+
if (isHostedWorkbench()) {
|
|
304
|
+
showToast('remove', 'Undo / redo unavailable', 'Open the local PMX workbench to use shared history.');
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
301
307
|
const result = await requestOk(redo ? 'redo' : 'undo', redo ? '/api/canvas/redo' : '/api/canvas/undo', {
|
|
302
308
|
method: 'POST',
|
|
303
309
|
});
|
|
@@ -371,37 +377,117 @@ export async function endSession(session: { source: string; agentId: string | nu
|
|
|
371
377
|
}
|
|
372
378
|
|
|
373
379
|
export interface SessionReceipt {
|
|
374
|
-
|
|
380
|
+
/** Distinct labels of the sessions on this card (one unless merged). */
|
|
381
|
+
labels: string[];
|
|
382
|
+
/** Endings folded into this card (see applySessionReceipt). */
|
|
383
|
+
sessions: number;
|
|
375
384
|
endedAt: string;
|
|
376
385
|
/** Why it ended — the receipt should answer this, not leave the human asking. */
|
|
377
386
|
endedBy?: 'human' | 'agent' | 'idle-timeout';
|
|
378
|
-
/** The session changed nothing on the board (its pre-session snapshot was dropped). */
|
|
379
|
-
unchanged?: boolean;
|
|
380
387
|
/** Cancelled (withdrawn items), rejected (human's explicit no on a gate), and
|
|
381
388
|
* held (TTL expired unanswered) are three different outcomes — never one
|
|
382
389
|
* "vetoed" pile. */
|
|
383
390
|
counts: { items: number; done: number; cancelled: number; rejected: number; held: number };
|
|
391
|
+
/** The pre-session snapshot; null when the board was empty at attach, or on a
|
|
392
|
+
* merged receipt (each session's snapshot is in History). */
|
|
384
393
|
snapshot: { id: string; name: string } | null;
|
|
394
|
+
/** What the session did with context (docs/design/AgentContext.dc.html receipt). */
|
|
395
|
+
context: SessionContextActivity;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface ReceiptNode {
|
|
399
|
+
id: string;
|
|
400
|
+
title: string;
|
|
401
|
+
reason?: string;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export interface SessionContextActivity {
|
|
405
|
+
read: ReceiptNode[];
|
|
406
|
+
pinned: ReceiptNode[];
|
|
407
|
+
created: ReceiptNode[];
|
|
408
|
+
edited: ReceiptNode[];
|
|
409
|
+
changedSinceRead: ReceiptNode[];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function receiptNodes(value: unknown): ReceiptNode[] {
|
|
413
|
+
if (!Array.isArray(value)) return [];
|
|
414
|
+
return value.flatMap((entry) => {
|
|
415
|
+
if (!entry || typeof entry !== 'object') return [];
|
|
416
|
+
const record = entry as Record<string, unknown>;
|
|
417
|
+
if (typeof record.id !== 'string' || typeof record.title !== 'string') return [];
|
|
418
|
+
return [
|
|
419
|
+
{ id: record.id, title: record.title, ...(typeof record.reason === 'string' ? { reason: record.reason } : {}) },
|
|
420
|
+
];
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The agent-activity lens: while on, nodes the last session did not touch
|
|
426
|
+
* (read, pin, create, edit) are dimmed. Null when off.
|
|
427
|
+
*/
|
|
428
|
+
export const activityLensNodeIds = signal<Set<string> | null>(null);
|
|
429
|
+
|
|
430
|
+
export function setActivityLens(on: boolean): void {
|
|
431
|
+
const context = sessionReceipt.value?.context;
|
|
432
|
+
activityLensNodeIds.value =
|
|
433
|
+
on && context
|
|
434
|
+
? new Set([...context.read, ...context.pinned, ...context.created, ...context.edited].map((node) => node.id))
|
|
435
|
+
: null;
|
|
385
436
|
}
|
|
386
437
|
|
|
387
438
|
/** The last ended session's receipt (design item 2); client-side, cleared on dismiss. */
|
|
388
439
|
export const sessionReceipt = signal<SessionReceipt | null>(null);
|
|
389
440
|
|
|
441
|
+
function unionNodes(a: ReceiptNode[], b: ReceiptNode[]): ReceiptNode[] {
|
|
442
|
+
const seen = new Set(a.map((node) => node.id));
|
|
443
|
+
return [...a, ...b.filter((node) => !seen.has(node.id))];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Endings that arrive while a receipt is still up fold into it, so a burst of
|
|
448
|
+
* sessions shows one card. A merged card carries no single snapshot — no one
|
|
449
|
+
* snapshot undoes overlapping sessions — and points at History instead.
|
|
450
|
+
*/
|
|
451
|
+
function mergeReceipts(open: SessionReceipt, next: SessionReceipt): SessionReceipt {
|
|
452
|
+
return {
|
|
453
|
+
labels: [...new Set([...open.labels, ...next.labels])],
|
|
454
|
+
sessions: open.sessions + 1,
|
|
455
|
+
endedAt: next.endedAt,
|
|
456
|
+
endedBy: open.endedBy === next.endedBy ? next.endedBy : undefined,
|
|
457
|
+
// Board-wide counts: the latest frame is the current board.
|
|
458
|
+
counts: next.counts,
|
|
459
|
+
snapshot: null,
|
|
460
|
+
context: {
|
|
461
|
+
read: unionNodes(open.context.read, next.context.read),
|
|
462
|
+
pinned: unionNodes(open.context.pinned, next.context.pinned),
|
|
463
|
+
created: unionNodes(open.context.created, next.context.created),
|
|
464
|
+
edited: unionNodes(open.context.edited, next.context.edited),
|
|
465
|
+
changedSinceRead: unionNodes(open.context.changedSinceRead, next.context.changedSinceRead),
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The receipt rule: a pop-up only for a top-level session that changed the
|
|
472
|
+
* board or read/pinned context. `unchanged` endings and worker endings
|
|
473
|
+
* (`parentAgentId` set — their orchestrator's receipt covers the board since
|
|
474
|
+
* it attached) stay in the timeline and History only, and never replace or
|
|
475
|
+
* extend an open receipt. Qualifying endings while one is up merge into it.
|
|
476
|
+
*/
|
|
390
477
|
export function applySessionReceipt(data: Record<string, unknown>): void {
|
|
391
478
|
const counts = data.counts as Partial<SessionReceipt['counts']> | undefined;
|
|
392
479
|
const snapshot = data.snapshot as SessionReceipt['snapshot'] | undefined;
|
|
480
|
+
const context = data.context as Record<string, unknown> | undefined;
|
|
393
481
|
if (typeof data.label !== 'string' || typeof data.endedAt !== 'string') return;
|
|
394
|
-
// Keep history/timeline intact, but only interrupt for a changed top-level
|
|
395
|
-
// session. Quiet endings must not replace an existing useful receipt.
|
|
396
482
|
if (data.unchanged === true || typeof data.parentAgentId === 'string') return;
|
|
397
|
-
|
|
398
|
-
|
|
483
|
+
const next: SessionReceipt = {
|
|
484
|
+
labels: [data.label],
|
|
485
|
+
sessions: 1,
|
|
399
486
|
endedAt: data.endedAt,
|
|
400
487
|
endedBy:
|
|
401
488
|
data.endedBy === 'human' || data.endedBy === 'agent' || data.endedBy === 'idle-timeout'
|
|
402
489
|
? data.endedBy
|
|
403
490
|
: undefined,
|
|
404
|
-
unchanged: data.unchanged === true,
|
|
405
491
|
counts: {
|
|
406
492
|
items: Number(counts?.items ?? 0) || 0,
|
|
407
493
|
done: Number(counts?.done ?? 0) || 0,
|
|
@@ -411,11 +497,22 @@ export function applySessionReceipt(data: Record<string, unknown>): void {
|
|
|
411
497
|
},
|
|
412
498
|
snapshot:
|
|
413
499
|
snapshot && typeof snapshot.id === 'string' ? { id: snapshot.id, name: String(snapshot.name ?? '') } : null,
|
|
500
|
+
context: {
|
|
501
|
+
read: receiptNodes(context?.read),
|
|
502
|
+
pinned: receiptNodes(context?.pinned),
|
|
503
|
+
created: receiptNodes(context?.created),
|
|
504
|
+
edited: receiptNodes(context?.edited),
|
|
505
|
+
changedSinceRead: receiptNodes(context?.changedSinceRead),
|
|
506
|
+
},
|
|
414
507
|
};
|
|
508
|
+
const open = sessionReceipt.value;
|
|
509
|
+
sessionReceipt.value = open ? mergeReceipts(open, next) : next;
|
|
510
|
+
if (open && activityLensNodeIds.value) setActivityLens(true);
|
|
415
511
|
}
|
|
416
512
|
|
|
417
513
|
export function dismissSessionReceipt(): void {
|
|
418
514
|
sessionReceipt.value = null;
|
|
515
|
+
activityLensNodeIds.value = null;
|
|
419
516
|
}
|
|
420
517
|
|
|
421
518
|
export function resetSessionStore(): void {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { findOpenCanvasPosition } from '../utils/placement.js';
|
|
2
2
|
import { ownsCamera } from './presentation';
|
|
3
|
+
import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
|
|
3
4
|
import { normalizeExtAppToolResult } from '../utils/ext-app-tool-result.js';
|
|
4
|
-
import type { CanvasAnnotation, CanvasEdge, CanvasNodeState } from '../types';
|
|
5
|
+
import type { CanvasAnnotation, CanvasEdge, CanvasNodeState, NodeActor } from '../types';
|
|
5
6
|
import {
|
|
6
7
|
activeNodeId,
|
|
7
8
|
addEdge,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
resetCanvasInteractionState,
|
|
31
32
|
} from './canvas-store';
|
|
32
33
|
import { applyBoards, loadBoards, setBoardSwitchHandler } from './boards-store';
|
|
34
|
+
import { applyContextStatus, refreshContextStatus } from './context-status-store';
|
|
33
35
|
import {
|
|
34
36
|
fetchAgentPresence,
|
|
35
37
|
fetchAxSurfaceState,
|
|
@@ -75,6 +77,7 @@ let pollGeneration = 0;
|
|
|
75
77
|
let sseFirstEventWatchdog: ReturnType<typeof setTimeout> | null = null;
|
|
76
78
|
|
|
77
79
|
export function forcedTransport(): 'poll' | 'sse' | null {
|
|
80
|
+
if (isHostedWorkbench()) return 'poll';
|
|
78
81
|
if (typeof location === 'undefined' || typeof window === 'undefined') return null;
|
|
79
82
|
const value = new URLSearchParams(location.search).get('transport');
|
|
80
83
|
if (value === 'poll' || value === 'sse') return value;
|
|
@@ -365,10 +368,19 @@ function parseCanvasNode(raw: Record<string, unknown>): CanvasNodeState | null {
|
|
|
365
368
|
zIndex: typeof raw.zIndex === 'number' ? raw.zIndex : 1,
|
|
366
369
|
collapsed: raw.collapsed === true,
|
|
367
370
|
pinned: raw.pinned === true,
|
|
371
|
+
contentRevision: typeof raw.contentRevision === 'number' ? raw.contentRevision : 0,
|
|
372
|
+
...(isActor(raw.createdBy) ? { createdBy: raw.createdBy } : {}),
|
|
373
|
+
...(isActor(raw.lastEditedBy) ? { lastEditedBy: raw.lastEditedBy } : {}),
|
|
368
374
|
data,
|
|
369
375
|
};
|
|
370
376
|
}
|
|
371
377
|
|
|
378
|
+
function isActor(value: unknown): value is NodeActor {
|
|
379
|
+
if (!value || typeof value !== 'object') return false;
|
|
380
|
+
const record = value as Record<string, unknown>;
|
|
381
|
+
return typeof record.actor === 'string' && typeof record.source === 'string';
|
|
382
|
+
}
|
|
383
|
+
|
|
372
384
|
function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
|
|
373
385
|
if (typeof raw.id !== 'string' || !raw.id) return null;
|
|
374
386
|
if (typeof raw.from !== 'string' || !raw.from) return null;
|
|
@@ -481,6 +493,7 @@ function resyncForBoardSwitch(): void {
|
|
|
481
493
|
resetCanvasInteractionState();
|
|
482
494
|
resetAttentionBridge();
|
|
483
495
|
dismissSessionReceipt();
|
|
496
|
+
applyContextStatus({});
|
|
484
497
|
axSurfaceState.value = null;
|
|
485
498
|
void refreshAxSurface();
|
|
486
499
|
const layoutAtRequest = layoutRevision;
|
|
@@ -499,21 +512,21 @@ setBoardSwitchHandler(resyncForBoardSwitch);
|
|
|
499
512
|
function handleConnected(data: Record<string, unknown>): void {
|
|
500
513
|
sessionId.value = (data.sessionId as string) || '';
|
|
501
514
|
connectionStatus.value = 'connected';
|
|
502
|
-
|
|
515
|
+
// Hosted writes capture the active board. Load its identity before reporting
|
|
516
|
+
// the viewport, rather than sending a spurious Home precondition on boot.
|
|
517
|
+
void loadBoards().then(reportClientViewportSize);
|
|
503
518
|
// Reconnect marker for holders of server-minted URLs (Finding S).
|
|
504
519
|
workbenchConnectionEpoch.value += 1;
|
|
505
520
|
reloadIfServerUpgraded(data.version, data.bundleStamp);
|
|
506
|
-
// Tell the server how big this window actually is, so an agent `fit` sizes
|
|
507
|
-
// the board to the human's window (0.4.6 orb feedback #2).
|
|
508
|
-
void reportClientViewportSize();
|
|
509
521
|
// Agent presence: read the snapshot on (re)connect; `agent-presence` frames
|
|
510
522
|
// keep it live from here on.
|
|
511
523
|
void fetchAgentPresence().then(applyPresenceSnapshot);
|
|
524
|
+
void refreshContextStatus();
|
|
512
525
|
// Human collaborators (phase 8): announce this tab and read who else is here.
|
|
513
526
|
// An `?agent=<key>` tab is an AGENT's view — it must not heartbeat a human
|
|
514
527
|
// guest cursor (its writes already book as the agent).
|
|
515
528
|
stopHumanPresence?.();
|
|
516
|
-
if (!tabAgentKey) stopHumanPresence = startHumanPresence();
|
|
529
|
+
if (!tabAgentKey && !isHostedWorkbench()) stopHumanPresence = startHumanPresence();
|
|
517
530
|
void requestJson<Partial<HumanPresenceSnapshot> | null>(
|
|
518
531
|
'fetchHumanPresence',
|
|
519
532
|
'/api/canvas/human-presence',
|
|
@@ -1033,6 +1046,11 @@ function handleContextPinsChanged(data: Record<string, unknown>): void {
|
|
|
1033
1046
|
const nodeIds = Array.isArray(data.nodeIds) ? data.nodeIds.filter((id): id is string => typeof id === 'string') : [];
|
|
1034
1047
|
replaceContextPinsFromServer(nodeIds);
|
|
1035
1048
|
syncAttentionFromSse({ event: 'context-pins-changed', data });
|
|
1049
|
+
void refreshContextStatus();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function handleContextStatusChanged(): void {
|
|
1053
|
+
void refreshContextStatus();
|
|
1036
1054
|
}
|
|
1037
1055
|
|
|
1038
1056
|
// AX state changes arrive as per-primitive deltas; rather than reduce them, treat
|
|
@@ -1109,6 +1127,7 @@ export const EVENT_HANDLERS: Record<string, (data: Record<string, unknown>) => v
|
|
|
1109
1127
|
'ext-app-update': handleExtAppUpdate,
|
|
1110
1128
|
'ext-app-result': handleExtAppResult,
|
|
1111
1129
|
'context-pins-changed': handleContextPinsChanged,
|
|
1130
|
+
'context-status-changed': handleContextStatusChanged,
|
|
1112
1131
|
'boards-changed': applyBoards,
|
|
1113
1132
|
'canvas-layout-update': handleCanvasLayoutUpdate,
|
|
1114
1133
|
'canvas-focus-node': handleCanvasFocusNode,
|
|
@@ -1170,7 +1189,7 @@ function startPollingTransport(): () => void {
|
|
|
1170
1189
|
const sid = sessionId.value;
|
|
1171
1190
|
if (sid) params.set('session', sid);
|
|
1172
1191
|
const query = params.toString();
|
|
1173
|
-
const response = await
|
|
1192
|
+
const response = await workbenchFetch(`/api/workbench/poll${query ? `?${query}` : ''}`);
|
|
1174
1193
|
if (generation !== pollGeneration) return;
|
|
1175
1194
|
if (!response.ok) throw new Error(`poll failed with HTTP ${response.status}`);
|
|
1176
1195
|
const body = (await response.json()) as {
|
|
@@ -76,7 +76,7 @@ export function initSessionThemeOverride(apply: (theme: CanvasThemeName) => void
|
|
|
76
76
|
}
|
|
77
77
|
schemeQuery = window.matchMedia('(prefers-color-scheme: light)');
|
|
78
78
|
schemeListener = () => {
|
|
79
|
-
if (active && schemeQuery) apply(schemeQuery.matches ? '
|
|
79
|
+
if (active && schemeQuery) apply(schemeQuery.matches ? 'daylight' : 'harbor');
|
|
80
80
|
};
|
|
81
81
|
schemeListener();
|
|
82
82
|
schemeQuery.addEventListener('change', schemeListener);
|