pmx-canvas 0.8.2 → 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/.github/extensions/pmx-canvas/tools.json +601 -22
- package/CHANGELOG.md +59 -0
- package/Readme.md +131 -532
- package/dist/canvas/global.css +2724 -476
- package/dist/canvas/index.js +168 -107
- package/dist/canvas/mcp-app.html +2899 -596
- package/dist/json-render/index.js +20 -20
- package/dist/types/client/canvas/BarHint.d.ts +12 -2
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
- package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandBar.d.ts +3 -4
- package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
- package/dist/types/client/canvas/ContextChip.d.ts +3 -0
- package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
- package/dist/types/client/canvas/EmptyState.d.ts +4 -4
- package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
- package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
- package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -7
- package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
- package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
- package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
- package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
- package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
- package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
- package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
- package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
- package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
- package/dist/types/client/canvas/canvas-area.d.ts +1 -1
- package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
- package/dist/types/client/canvas/graph-model.d.ts +37 -0
- package/dist/types/client/canvas/memory-layout.d.ts +23 -0
- package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
- package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
- package/dist/types/client/icons.d.ts +0 -10
- package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
- package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
- package/dist/types/client/state/boards-store.d.ts +81 -9
- package/dist/types/client/state/canvas-store.d.ts +14 -1
- package/dist/types/client/state/context-chip-store.d.ts +22 -0
- package/dist/types/client/state/context-status-store.d.ts +35 -0
- package/dist/types/client/state/human-store.d.ts +2 -1
- package/dist/types/client/state/intent-bridge.d.ts +11 -8
- package/dist/types/client/state/knowledge-store.d.ts +22 -0
- package/dist/types/client/state/near-pin-store.d.ts +44 -1
- package/dist/types/client/state/presence-store.d.ts +4 -4
- package/dist/types/client/state/presentation.d.ts +28 -0
- package/dist/types/client/state/relation-store.d.ts +10 -0
- package/dist/types/client/state/session-store.d.ts +41 -2
- package/dist/types/client/state/workspace-settings.d.ts +7 -0
- package/dist/types/client/types.d.ts +13 -1
- package/dist/types/client/utils/keyboard.d.ts +2 -0
- package/dist/types/client/utils/time-ago.d.ts +4 -0
- package/dist/types/mcp/context-uri-template.d.ts +3 -1
- package/dist/types/server/agent-presence.d.ts +10 -3
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +30 -7
- package/dist/types/server/canvas-operations.d.ts +24 -2
- package/dist/types/server/canvas-provenance.d.ts +22 -2
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +93 -15
- package/dist/types/server/canvas-validation.d.ts +1 -1
- package/dist/types/server/context-brief.d.ts +94 -6
- package/dist/types/server/context-reads.d.ts +33 -7
- package/dist/types/server/edit-summary.d.ts +20 -0
- package/dist/types/server/index.d.ts +71 -10
- package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
- package/dist/types/server/lifecycle-status.d.ts +10 -0
- package/dist/types/server/memory-graph.d.ts +9 -0
- package/dist/types/server/mutation-history.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
- package/dist/types/server/operations/ops/boards.d.ts +2 -1
- package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
- package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +5 -0
- package/dist/types/server/operations/ops/query.d.ts +23 -0
- package/dist/types/server/operations/registry.d.ts +1 -0
- package/dist/types/server/placement.d.ts +7 -0
- package/dist/types/server/scope-fence.d.ts +2 -1
- package/dist/types/server/spatial-analysis.d.ts +6 -3
- package/dist/types/server/tour-control.d.ts +32 -0
- package/dist/types/server/wiki-links.d.ts +14 -0
- package/dist/types/server/workbench-auth.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +2 -0
- package/dist/types/shared/agent-presence.d.ts +15 -12
- package/dist/types/shared/auto-arrange.d.ts +1 -0
- package/dist/types/shared/boards.d.ts +4 -4
- package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
- package/dist/types/shared/knowledge-presets.d.ts +6 -0
- package/dist/types/shared/lifecycle-status.d.ts +24 -0
- package/dist/types/shared/memory-graph.d.ts +53 -0
- package/dist/types/shared/relations.d.ts +68 -0
- package/dist/types/shared/semantic-attention.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +38 -0
- package/dist/types/shared/workspace-settings.d.ts +11 -0
- package/docs/RELEASE.md +21 -3
- package/docs/adr-001-bun-only-runtime.md +7 -1
- package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
- package/docs/api-reference.md +109 -0
- package/docs/ax-host-adapter-contract.md +1 -1
- package/docs/ax-state-contract.md +5 -4
- package/docs/cli.md +153 -15
- package/docs/demos/pmx-canvas-demo.mp4 +0 -0
- package/docs/design/AgentContext.dc.html +3 -3
- package/docs/design/BoardPins.dc.html +37 -22
- package/docs/design/Categories.dc.html +208 -0
- package/docs/design/CategoryEditor.dc.html +33 -0
- package/docs/design/Chrome.dc.html +52 -9
- package/docs/design/ContextChip.dc.html +224 -0
- package/docs/design/ContextChip600.dc.html +136 -0
- package/docs/design/EmptyState.dc.html +45 -0
- package/docs/design/Flows.dc.html +23 -1
- package/docs/design/HeaderMarks.dc.html +183 -0
- package/docs/design/Home.dc.html +19 -1
- package/docs/design/HomeGraph.dc.html +86 -202
- package/docs/design/HomeMap.dc.html +23 -1
- package/docs/design/HomeTeam.dc.html +32 -14
- package/docs/design/Lifecycle.dc.html +22 -0
- package/docs/design/Lifecycle600.dc.html +18 -0
- package/docs/design/LifecycleVariants.dc.html +157 -0
- package/docs/design/LinkAuthorship.dc.html +139 -0
- package/docs/design/LinksChanged.dc.html +203 -0
- package/docs/design/LinksOptions.dc.html +137 -0
- package/docs/design/LinksOptions600.dc.html +134 -0
- package/docs/design/NearPin.dc.html +9 -10
- package/docs/design/Pane600.dc.html +4 -3
- package/docs/design/Relations.dc.html +3 -2
- package/docs/design/Restore.dc.html +152 -0
- package/docs/design/SeeChange.dc.html +267 -0
- package/docs/design/Uses.dc.html +12 -6
- package/docs/design/canvas.json +146 -19
- package/docs/design.md +449 -16
- package/docs/environment.md +0 -1
- package/docs/evals/curation-effect.md +128 -0
- package/docs/http-api.md +459 -37
- package/docs/mcp.md +146 -18
- package/docs/node-types.md +58 -3
- package/docs/plans/design-gaps-2026-10-08.md +53 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
- package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
- package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
- package/docs/product-context-vision-2026-09.md +17 -1
- package/docs/product-vision-2026-09.md +1082 -43
- package/docs/product-vision-review-2026-09.md +13 -1
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +62 -2
- 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/plugins/pmx-canvas/tools.json +601 -22
- package/skills/pmx-canvas/SKILL.md +216 -42
- package/skills/pmx-canvas/evals/evals.json +224 -0
- package/skills/pmx-canvas/references/full-reference.md +120 -21
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/importing-context.md +132 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
- package/skills/pmx-canvas-testing/SKILL.md +20 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +12 -0
- package/src/cli/commands/boards.ts +88 -11
- package/src/cli/commands/edges.ts +28 -0
- package/src/cli/commands/groups.ts +7 -0
- package/src/cli/commands/nodes.ts +1 -0
- package/src/cli/commands/query.ts +34 -2
- package/src/cli/commands/record.ts +199 -48
- package/src/cli/index.ts +6 -1
- package/src/client/App.tsx +177 -70
- package/src/client/canvas/AttentionHistory.tsx +0 -2
- package/src/client/canvas/BarHint.tsx +40 -2
- package/src/client/canvas/BoardCopyDialog.tsx +1 -1
- package/src/client/canvas/BoardFolderDialog.tsx +7 -7
- package/src/client/canvas/BoardLinkDialog.tsx +138 -33
- package/src/client/canvas/BoardPinButton.tsx +33 -0
- package/src/client/canvas/BoardSwitcher.tsx +26 -40
- package/src/client/canvas/CanvasNode.tsx +360 -237
- package/src/client/canvas/CanvasViewport.tsx +45 -7
- package/src/client/canvas/CommandBar.tsx +29 -23
- package/src/client/canvas/CommandPalette.tsx +5 -12
- package/src/client/canvas/ConnectionBanner.tsx +3 -3
- package/src/client/canvas/ContextChip.tsx +296 -0
- package/src/client/canvas/ContextMenu.tsx +63 -5
- package/src/client/canvas/CrossBoardReferences.tsx +60 -0
- package/src/client/canvas/EdgeLayer.tsx +339 -76
- package/src/client/canvas/EmptyState.tsx +33 -18
- package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
- package/src/client/canvas/ExternalWriters.tsx +57 -42
- package/src/client/canvas/HomeGraphView.tsx +942 -0
- package/src/client/canvas/HomeMapView.tsx +641 -0
- package/src/client/canvas/HomeReviewView.tsx +167 -0
- package/src/client/canvas/HomeView.tsx +687 -160
- package/src/client/canvas/IntentLayer.tsx +19 -12
- package/src/client/canvas/KnowledgeDialog.tsx +488 -0
- package/src/client/canvas/LifecycleChrome.tsx +133 -0
- package/src/client/canvas/LifecycleFields.tsx +91 -0
- package/src/client/canvas/LifecyclePanel.tsx +543 -0
- package/src/client/canvas/MemoryInspector.tsx +104 -0
- package/src/client/canvas/Minimap.tsx +3 -3
- package/src/client/canvas/NearPinLayer.tsx +187 -0
- package/src/client/canvas/NodeContextMark.tsx +175 -97
- package/src/client/canvas/Presentation.tsx +23 -57
- package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
- package/src/client/canvas/RelationsPanel.tsx +82 -0
- package/src/client/canvas/SelectionBar.tsx +3 -6
- package/src/client/canvas/SessionPanel.tsx +90 -50
- package/src/client/canvas/SessionReceipt.tsx +311 -39
- package/src/client/canvas/ShortcutOverlay.tsx +1 -1
- package/src/client/canvas/SnapshotPanel.tsx +27 -36
- package/src/client/canvas/ToolRail.tsx +367 -192
- package/src/client/canvas/TopBar.tsx +354 -124
- package/src/client/canvas/canvas-area.ts +10 -3
- package/src/client/canvas/graph-geometry.ts +195 -0
- package/src/client/canvas/graph-model.ts +254 -0
- package/src/client/canvas/memory-layout.ts +106 -0
- package/src/client/canvas/use-chrome-height.ts +20 -0
- package/src/client/canvas/use-focus-trap.ts +31 -5
- package/src/client/canvas/use-memory-graph.ts +58 -0
- package/src/client/canvas/use-node-drag.ts +2 -1
- package/src/client/canvas/use-pan-zoom.ts +64 -7
- package/src/client/icons.tsx +0 -63
- package/src/client/mcp-app/style.css +0 -6
- package/src/client/nodes/AxStepControls.tsx +74 -50
- package/src/client/nodes/BoardNode.tsx +5 -1
- package/src/client/nodes/ContextNode.tsx +18 -11
- package/src/client/nodes/ExtAppFrame.tsx +6 -3
- package/src/client/nodes/FileNode.tsx +46 -42
- package/src/client/nodes/HtmlNode.tsx +32 -0
- package/src/client/nodes/ImageNode.tsx +1 -1
- package/src/client/nodes/InlineFormatBar.tsx +6 -9
- package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
- package/src/client/nodes/MarkdownNode.tsx +377 -109
- package/src/client/nodes/McpAppNode.tsx +50 -20
- package/src/client/nodes/MdFormatBar.tsx +21 -18
- package/src/client/nodes/PromptNode.tsx +1 -1
- package/src/client/nodes/ResponseNode.tsx +1 -1
- package/src/client/nodes/viewer-runtime-state.ts +54 -22
- package/src/client/state/attention-bridge.ts +64 -15
- package/src/client/state/boards-store.ts +193 -39
- package/src/client/state/canvas-store.ts +99 -12
- package/src/client/state/context-chip-store.ts +70 -0
- package/src/client/state/context-status-store.ts +53 -0
- package/src/client/state/human-store.ts +13 -7
- package/src/client/state/intent-bridge.ts +51 -30
- package/src/client/state/knowledge-store.ts +43 -0
- package/src/client/state/near-pin-store.ts +115 -6
- package/src/client/state/presence-store.ts +7 -10
- package/src/client/state/presentation.ts +91 -0
- package/src/client/state/relation-store.ts +81 -0
- package/src/client/state/session-store.ts +144 -18
- package/src/client/state/sse-bridge.ts +235 -32
- package/src/client/state/workspace-settings.ts +11 -0
- package/src/client/theme/global.css +2724 -476
- package/src/client/types.ts +12 -0
- package/src/client/utils/keyboard.ts +10 -0
- package/src/client/utils/time-ago.ts +14 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/mcp/canvas-app.ts +12 -2
- package/src/mcp/context-uri-template.ts +7 -4
- package/src/mcp/server.ts +63 -41
- package/src/mcp/workbench-app.ts +39 -6
- package/src/server/agent-presence.ts +128 -28
- package/src/server/ax-context.ts +27 -13
- package/src/server/board-export.ts +12 -1
- package/src/server/canvas-db.ts +331 -42
- package/src/server/canvas-operations.ts +195 -83
- package/src/server/canvas-provenance.ts +53 -22
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +950 -122
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/context-brief.ts +345 -38
- package/src/server/context-reads.ts +178 -30
- package/src/server/demo-state.json +355 -24
- package/src/server/edit-summary.ts +74 -0
- package/src/server/html-surface.ts +8 -1
- package/src/server/index.ts +181 -11
- package/src/server/knowledge-lifecycle.ts +304 -0
- package/src/server/lifecycle-status.ts +232 -0
- package/src/server/memory-graph.ts +166 -0
- package/src/server/mutation-history.ts +18 -6
- package/src/server/operations/composites.ts +32 -11
- package/src/server/operations/http.ts +7 -2
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/mcp.ts +26 -1
- package/src/server/operations/ops/ax-presence.ts +1 -1
- package/src/server/operations/ops/ax-read.ts +57 -6
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/backup.ts +5 -3
- package/src/server/operations/ops/batch.ts +35 -4
- package/src/server/operations/ops/boards.ts +188 -47
- package/src/server/operations/ops/canvas-wire.ts +31 -1
- package/src/server/operations/ops/edges.ts +36 -2
- package/src/server/operations/ops/groups.ts +6 -1
- package/src/server/operations/ops/lifecycle.ts +262 -0
- package/src/server/operations/ops/memory-graph.ts +45 -0
- package/src/server/operations/ops/nodes.ts +107 -24
- package/src/server/operations/ops/query.ts +281 -68
- package/src/server/operations/ops/snapshots.ts +73 -1
- package/src/server/operations/ops/validate.ts +5 -1
- package/src/server/operations/ops/viewport.ts +97 -3
- package/src/server/operations/registry.ts +74 -25
- package/src/server/placement.ts +29 -0
- package/src/server/scope-fence.ts +78 -6
- package/src/server/server.ts +115 -16
- package/src/server/spatial-analysis.ts +48 -23
- package/src/server/tour-control.ts +82 -0
- package/src/server/wiki-links.ts +137 -0
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/agent-context.ts +3 -0
- package/src/shared/agent-presence.ts +15 -15
- package/src/shared/auto-arrange.ts +2 -0
- package/src/shared/boards.ts +4 -4
- package/src/shared/knowledge-lifecycle.ts +22 -0
- package/src/shared/knowledge-presets.ts +80 -0
- package/src/shared/lifecycle-status.ts +26 -0
- package/src/shared/memory-graph.ts +74 -0
- package/src/shared/relations.ts +98 -0
- package/src/shared/semantic-attention.ts +15 -1
- package/src/shared/tour.ts +23 -5
- package/src/shared/workspace-settings.ts +13 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
- package/dist/types/client/canvas/kind-colors.d.ts +0 -3
- package/dist/types/server/agent-context.d.ts +0 -22
- package/src/client/canvas/ContextPinBar.tsx +0 -25
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { exportDialogOpen } from './ExportDialog';
|
|
2
|
-
import { activeBoardId } from '../state/boards-store';
|
|
2
|
+
import { activeBoard, activeBoardId, browsingMemory } from '../state/boards-store';
|
|
3
3
|
import { BoardSwitcher } from './BoardSwitcher';
|
|
4
|
+
import { BoardPinButton } from './BoardPinButton';
|
|
5
|
+
import { ContextChip } from './ContextChip';
|
|
4
6
|
import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
|
|
5
7
|
import type { ComponentChildren } from 'preact';
|
|
6
|
-
import {
|
|
7
|
-
import { useEffect, useState } from 'preact/hooks';
|
|
8
|
+
import { startPresentation } from '../state/presentation';
|
|
9
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
8
10
|
import { IconFitAll, IconZoomIn, IconZoomOut, IconClose } from '../icons';
|
|
9
11
|
import {
|
|
10
12
|
animateViewport,
|
|
@@ -20,14 +22,22 @@ import {
|
|
|
20
22
|
} from '../state/canvas-store';
|
|
21
23
|
import { modChord } from '../utils/platform';
|
|
22
24
|
import { canvasArea } from './canvas-area';
|
|
23
|
-
import { BarHint } from './BarHint';
|
|
25
|
+
import { BarHint, BarPortal } from './BarHint';
|
|
24
26
|
import { degradedState } from './ConnectionBanner';
|
|
25
27
|
import { ExternalWriterIndicator } from './ExternalWriters';
|
|
26
28
|
import { useNow } from './use-now';
|
|
27
|
-
import { agentPhaseLabel } from '../../shared/agent-presence.js';
|
|
28
|
-
import { activeSession, agentPresences, attachedSessions,
|
|
29
|
-
import {
|
|
29
|
+
import { type AgentPresence, agentPhaseLabel } from '../../shared/agent-presence.js';
|
|
30
|
+
import { activeSession, agentPresences, attachedSessions, writerColor, writerInitial } from '../state/presence-store';
|
|
31
|
+
import {
|
|
32
|
+
activityLens,
|
|
33
|
+
endSession,
|
|
34
|
+
pendingGates,
|
|
35
|
+
sessionPanelOpened,
|
|
36
|
+
setActivityLens,
|
|
37
|
+
startSession,
|
|
38
|
+
} from '../state/session-store';
|
|
30
39
|
import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
|
|
40
|
+
import { agentSessionsEnabled } from '../state/workspace-settings';
|
|
31
41
|
|
|
32
42
|
/**
|
|
33
43
|
* The attached session's chip (rail-chrome-v2 phase 3): phase-colored, with a
|
|
@@ -39,6 +49,31 @@ const AGENT_CHIP_LIMIT = 3;
|
|
|
39
49
|
const AGENT_PHASE_RANK: Record<string, number> = { 'waiting-approval': 0, thinking: 1, tooling: 2, idle: 3 };
|
|
40
50
|
|
|
41
51
|
function AgentChip() {
|
|
52
|
+
const [opened, setOpened] = useState<{ id: string; button: HTMLButtonElement } | null>(null);
|
|
53
|
+
const popover = useRef<HTMLDivElement>(null);
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
if (!opened) return;
|
|
56
|
+
const close = (event: PointerEvent) => {
|
|
57
|
+
if (!popover.current?.contains(event.target as Node) && !opened.button.contains(event.target as Node))
|
|
58
|
+
setOpened(null);
|
|
59
|
+
};
|
|
60
|
+
const onKey = (event: KeyboardEvent) => {
|
|
61
|
+
if (event.key === 'Escape' && !event.defaultPrevented) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
setOpened(null);
|
|
64
|
+
opened.button.focus({ preventScroll: true });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const onResize = () => setOpened(null);
|
|
68
|
+
document.addEventListener('pointerdown', close, true);
|
|
69
|
+
document.addEventListener('keydown', onKey);
|
|
70
|
+
window.addEventListener('resize', onResize);
|
|
71
|
+
return () => {
|
|
72
|
+
document.removeEventListener('pointerdown', close, true);
|
|
73
|
+
document.removeEventListener('keydown', onKey);
|
|
74
|
+
window.removeEventListener('resize', onResize);
|
|
75
|
+
};
|
|
76
|
+
}, [opened]);
|
|
42
77
|
// Every attached session gets its chip — with several agents on the board,
|
|
43
78
|
// showing only the first hid the rest from the top bar entirely. But one
|
|
44
79
|
// chip PER agent floods the bar on a many-agent board (user feedback,
|
|
@@ -47,7 +82,10 @@ function AgentChip() {
|
|
|
47
82
|
// active few wear their own chips and the rest fold into a census chip
|
|
48
83
|
// that always names the total.
|
|
49
84
|
const allSessions = attachedSessions.value;
|
|
50
|
-
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (opened && !opened.button.isConnected) setOpened(null);
|
|
87
|
+
}, [allSessions, opened]);
|
|
88
|
+
if (!agentSessionsEnabled.value || allSessions.length === 0) return null;
|
|
51
89
|
// Attached workers are represented by their orchestrator's roll-up, not a
|
|
52
90
|
// second top-level chip of their own. Orphans remain visible.
|
|
53
91
|
const sessions = allSessions.filter(
|
|
@@ -106,9 +144,26 @@ function AgentChip() {
|
|
|
106
144
|
data-phase={session.phase}
|
|
107
145
|
style={{ '--identity-color': writerColor(session.sessionId) }}
|
|
108
146
|
>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
class="agent-chip-initial"
|
|
150
|
+
aria-label={`${session.label} session details`}
|
|
151
|
+
aria-expanded={opened?.id === session.sessionId}
|
|
152
|
+
aria-haspopup="dialog"
|
|
153
|
+
onClick={(event) => {
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
setOpened(
|
|
156
|
+
opened?.id === session.sessionId ? null : { id: session.sessionId, button: event.currentTarget },
|
|
157
|
+
);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{writerInitial(session.label)}
|
|
161
|
+
<span class="agent-chip-dot" aria-hidden="true" />
|
|
162
|
+
</button>
|
|
109
163
|
<span class="agent-chip-dot" aria-hidden="true" />
|
|
110
164
|
<span class="agent-chip-label">{agentPhaseLabel(session)}</span>
|
|
111
165
|
<span class="agent-chip-who hud-collapsible-text">{session.label}</span>
|
|
166
|
+
<SessionTouches session={session} />
|
|
112
167
|
{workers > 0 && (
|
|
113
168
|
<span class="agent-chip-workers">
|
|
114
169
|
+{workers} worker{workers === 1 ? '' : 's'}
|
|
@@ -118,7 +173,6 @@ function AgentChip() {
|
|
|
118
173
|
type="button"
|
|
119
174
|
class="agent-chip-end"
|
|
120
175
|
aria-label={`End ${session.label} session`}
|
|
121
|
-
title={`End ${session.label}'s session`}
|
|
122
176
|
onClick={(e) => {
|
|
123
177
|
// The chip sits inside a tap-to-open hint — ending a session
|
|
124
178
|
// must not also open it.
|
|
@@ -144,17 +198,109 @@ function AgentChip() {
|
|
|
144
198
|
</span>
|
|
145
199
|
</BarHint>
|
|
146
200
|
)}
|
|
201
|
+
{opened &&
|
|
202
|
+
visible
|
|
203
|
+
.filter((session) => session.sessionId === opened.id)
|
|
204
|
+
.map((session) => (
|
|
205
|
+
<BarPortal key={session.sessionId}>
|
|
206
|
+
<div
|
|
207
|
+
ref={popover}
|
|
208
|
+
class={`agent-session-popover phase-${session.phase}`}
|
|
209
|
+
role="dialog"
|
|
210
|
+
aria-label={`${session.label} session details`}
|
|
211
|
+
style={{
|
|
212
|
+
right: `${Math.max(12, window.innerWidth - opened.button.getBoundingClientRect().right)}px`,
|
|
213
|
+
top: `${opened.button.getBoundingClientRect().bottom + 8}px`,
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<div class="agent-session-popover-head">
|
|
217
|
+
<span class="agent-chip-dot" aria-hidden="true" />
|
|
218
|
+
<strong>
|
|
219
|
+
{session.label} · {agentPhaseLabel(session)}
|
|
220
|
+
</strong>
|
|
221
|
+
</div>
|
|
222
|
+
<SessionTouches session={session} popover />
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
class="session-receipt-link"
|
|
226
|
+
onClick={() => {
|
|
227
|
+
sessionPanelOpened.value = true;
|
|
228
|
+
setOpened(null);
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
Open session panel
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
</BarPortal>
|
|
235
|
+
))}
|
|
147
236
|
</>
|
|
148
237
|
);
|
|
149
238
|
}
|
|
150
239
|
|
|
240
|
+
/**
|
|
241
|
+
* What the session has done so far (AgentContext.dc.html lens, Pane600
|
|
242
|
+
* "Claude · 4 read · 1 edited"): the counts, and a press dims every card it
|
|
243
|
+
* has not touched. Hidden until it has touched something.
|
|
244
|
+
*/
|
|
245
|
+
function SessionTouches({ session, popover = false }: { session: AgentPresence; popover?: boolean }) {
|
|
246
|
+
const touches = session.session;
|
|
247
|
+
const parts = touches
|
|
248
|
+
? (
|
|
249
|
+
[
|
|
250
|
+
['read', touches.read.length],
|
|
251
|
+
['created', touches.created.length],
|
|
252
|
+
['edited', touches.edited.length],
|
|
253
|
+
['pinned', touches.pinned.length],
|
|
254
|
+
] as const
|
|
255
|
+
).filter(([, count]) => count > 0)
|
|
256
|
+
: [];
|
|
257
|
+
const lens = activityLens.value;
|
|
258
|
+
const on = lens?.kind === 'live' && lens.sessionId === session.sessionId;
|
|
259
|
+
// While its lens is on the button stays, even at zero, so it can be turned off.
|
|
260
|
+
if (parts.length === 0 && !on && !popover) return null;
|
|
261
|
+
const counts = parts.map(([word, count]) => `${count} ${word}`).join(' · ') || 'nothing yet';
|
|
262
|
+
if (popover)
|
|
263
|
+
return (
|
|
264
|
+
<>
|
|
265
|
+
<span class="agent-session-counts">{counts}</span>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
role="switch"
|
|
269
|
+
aria-checked={on}
|
|
270
|
+
class={`session-receipt-lens${on ? ' is-on' : ''}`}
|
|
271
|
+
onClick={() => setActivityLens(!on, { sessionId: session.sessionId })}
|
|
272
|
+
>
|
|
273
|
+
<span class="session-receipt-lens-track" aria-hidden="true">
|
|
274
|
+
<span class="session-receipt-lens-knob" />
|
|
275
|
+
</span>
|
|
276
|
+
Dim untouched nodes
|
|
277
|
+
</button>
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
return (
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
class={`agent-chip-touches${on ? ' is-on' : ''}`}
|
|
284
|
+
aria-pressed={on}
|
|
285
|
+
aria-label={`This session: ${parts.map(([word, count]) => `${count} ${word}`).join(', ') || 'nothing yet'}. ${on ? 'Show all cards' : 'Dim untouched cards'}`}
|
|
286
|
+
onClick={(event) => {
|
|
287
|
+
// Inside the chip's tap-to-open hint: toggling the lens must not open it.
|
|
288
|
+
event.stopPropagation();
|
|
289
|
+
setActivityLens(!on, { sessionId: session.sessionId });
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
{counts}
|
|
293
|
+
</button>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
151
297
|
/**
|
|
152
298
|
* Amber escalation badge while any approval gate is pending in an attached
|
|
153
299
|
* session: "1 gate · 4:31" — the countdown is the soonest auto-hold.
|
|
154
300
|
*/
|
|
155
301
|
function GateBadge() {
|
|
156
302
|
const count = pendingGates.value.length;
|
|
157
|
-
const active =
|
|
303
|
+
const active = count > 0;
|
|
158
304
|
const now = useNow(active ? 1000 : 0);
|
|
159
305
|
if (!active) return null;
|
|
160
306
|
const soonest = pendingGates.value
|
|
@@ -164,65 +310,12 @@ function GateBadge() {
|
|
|
164
310
|
return (
|
|
165
311
|
<BarHint
|
|
166
312
|
label="Approval gates waiting on you"
|
|
167
|
-
|
|
168
|
-
body="The agent is blocked until you approve or reject in the session panel; unanswered gates auto-hold when the countdown runs out."
|
|
313
|
+
body="Open agent controls to approve or reject. Unanswered gates auto-hold when the countdown runs out, even with the session beta off."
|
|
169
314
|
>
|
|
170
|
-
<
|
|
315
|
+
<button type="button" class="gate-badge" onClick={() => (sessionPanelOpened.value = true)}>
|
|
171
316
|
{count} gate{count === 1 ? '' : 's'}
|
|
172
317
|
{soonest !== undefined && ` · ${formatCountdown(soonest)}`}
|
|
173
|
-
</
|
|
174
|
-
</BarHint>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function budgetTone(ratio: number): 'ok' | 'warn' | 'danger' {
|
|
179
|
-
if (ratio > 0.9) return 'danger';
|
|
180
|
-
if (ratio >= 0.7) return 'warn';
|
|
181
|
-
return 'ok';
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function formatTokens(value: number): string {
|
|
185
|
-
return value >= 1000 ? `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k` : String(value);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Context meter (rail-chrome-v2 phase 5). Two honest modes:
|
|
190
|
-
* - the host reported the agent's REAL window (`contextUsage` on its presence)
|
|
191
|
-
* → "Context": used / window, with the pinned payload as a note;
|
|
192
|
-
* - nothing reported (no adapter does yet) → "Pins": the pinned-context
|
|
193
|
-
* payload estimate against the configured budget
|
|
194
|
-
* (PMX_CANVAS_CONTEXT_BUDGET_TOKENS). Never presented as the agent's window.
|
|
195
|
-
* Session-only, like the chip; the estimate moves within a frame of a pin toggle.
|
|
196
|
-
*/
|
|
197
|
-
function ContextBudget() {
|
|
198
|
-
const session = activeSession.value;
|
|
199
|
-
if (!session) return null;
|
|
200
|
-
const budget = contextBudget.value;
|
|
201
|
-
const real = session.contextUsage;
|
|
202
|
-
const used = real ? real.used : budget.used;
|
|
203
|
-
const total = real ? real.total : budget.total;
|
|
204
|
-
const ratio = total > 0 ? Math.min(1, used / total) : 0;
|
|
205
|
-
const pct = Math.round(ratio * 100);
|
|
206
|
-
const hint = real
|
|
207
|
-
? {
|
|
208
|
-
label: `Context window — reported by ${session.label}`,
|
|
209
|
-
body: `${formatTokens(real.used)} of ${formatTokens(real.total)} tokens of the agent's actual context window are used. Your ✦-pinned nodes account for ≈ ${formatTokens(budget.used)} tokens of it.`,
|
|
210
|
-
}
|
|
211
|
-
: {
|
|
212
|
-
label: 'Pins — pinned-context size (estimate)',
|
|
213
|
-
body: `The nodes you pinned (✦) are what the agent is asked to carry as context: ≈ ${formatTokens(budget.used)} of a ${formatTokens(budget.total)}-token budget. When the agent's host reports its real context window, this meter switches to showing that instead.`,
|
|
214
|
-
};
|
|
215
|
-
return (
|
|
216
|
-
<BarHint label={hint.label} body={hint.body} tapToOpen>
|
|
217
|
-
<span class={`context-budget tone-${budgetTone(ratio)}`} data-mode={real ? 'window' : 'pins'}>
|
|
218
|
-
<span class="context-budget-caption hud-collapsible-text">{real ? 'Context' : 'Pins'}</span>
|
|
219
|
-
<span class="context-budget-track" aria-hidden="true">
|
|
220
|
-
<span class="context-budget-fill" style={{ width: `${pct}%` }} />
|
|
221
|
-
</span>
|
|
222
|
-
<span class="context-budget-label" data-testid="budget-label">
|
|
223
|
-
{used > 0 && pct === 0 ? '<1' : pct}%
|
|
224
|
-
</span>
|
|
225
|
-
</span>
|
|
318
|
+
</button>
|
|
226
319
|
</BarHint>
|
|
227
320
|
);
|
|
228
321
|
}
|
|
@@ -234,7 +327,7 @@ function ContextBudget() {
|
|
|
234
327
|
*/
|
|
235
328
|
function StartSessionButton() {
|
|
236
329
|
const [busy, setBusy] = useState(false);
|
|
237
|
-
if (activeSession.value) return null;
|
|
330
|
+
if (!agentSessionsEnabled.value || activeSession.value) return null;
|
|
238
331
|
return (
|
|
239
332
|
<BarHint
|
|
240
333
|
label="Start an agent session"
|
|
@@ -243,6 +336,7 @@ function StartSessionButton() {
|
|
|
243
336
|
<button
|
|
244
337
|
type="button"
|
|
245
338
|
class="start-session-btn"
|
|
339
|
+
aria-label="Start agent session beta"
|
|
246
340
|
disabled={busy}
|
|
247
341
|
onClick={() => {
|
|
248
342
|
setBusy(true);
|
|
@@ -250,7 +344,7 @@ function StartSessionButton() {
|
|
|
250
344
|
}}
|
|
251
345
|
>
|
|
252
346
|
<span class="start-session-dot" aria-hidden="true" />
|
|
253
|
-
Start agent session
|
|
347
|
+
<span class="start-session-label">Start agent session</span>
|
|
254
348
|
<span class="beta-tag">beta</span>
|
|
255
349
|
</button>
|
|
256
350
|
</BarHint>
|
|
@@ -264,6 +358,32 @@ function StartSessionButton() {
|
|
|
264
358
|
* below 1180px, and nothing ever wraps to a second row.
|
|
265
359
|
*/
|
|
266
360
|
export function TopBar() {
|
|
361
|
+
const barRef = useRef<HTMLDivElement>(null);
|
|
362
|
+
useLayoutEffect(() => {
|
|
363
|
+
const bar = barRef.current;
|
|
364
|
+
const controls = bar?.querySelector<HTMLElement>('.top-bar-right');
|
|
365
|
+
if (!bar || !controls) return;
|
|
366
|
+
// Fold by actual content, not only window width: session counts and agent
|
|
367
|
+
// names can fill a wide pane too. Re-evaluate from the full layout so a
|
|
368
|
+
// resize or ended session can unfold it again.
|
|
369
|
+
const fit = () => {
|
|
370
|
+
const menuOpen = bar.querySelector('.top-bar-more [aria-expanded="true"]');
|
|
371
|
+
bar.classList.remove('is-folded', 'is-compact');
|
|
372
|
+
const overflow = () => controls.getBoundingClientRect().right > bar.getBoundingClientRect().right - 14;
|
|
373
|
+
// A resize or updated count must not hide the menu under its pointer.
|
|
374
|
+
if (menuOpen || overflow()) bar.classList.add('is-folded');
|
|
375
|
+
if (overflow()) bar.classList.add('is-compact');
|
|
376
|
+
};
|
|
377
|
+
const resize = new ResizeObserver(fit);
|
|
378
|
+
resize.observe(bar);
|
|
379
|
+
const content = new MutationObserver(fit);
|
|
380
|
+
content.observe(bar, { childList: true, characterData: true, subtree: true });
|
|
381
|
+
fit();
|
|
382
|
+
return () => {
|
|
383
|
+
resize.disconnect();
|
|
384
|
+
content.disconnect();
|
|
385
|
+
};
|
|
386
|
+
}, []);
|
|
267
387
|
const status = connectionStatus.value;
|
|
268
388
|
const hasSynced = hasInitialServerLayout.value;
|
|
269
389
|
const v = viewport.value;
|
|
@@ -295,6 +415,7 @@ export function TopBar() {
|
|
|
295
415
|
|
|
296
416
|
const degraded = degradedState.value;
|
|
297
417
|
const statusTitle = degraded ?? (status === 'connected' && !hasSynced ? 'syncing' : status);
|
|
418
|
+
const pinTarget = activeBoard();
|
|
298
419
|
const countsLabel = hasSynced
|
|
299
420
|
? [
|
|
300
421
|
`${nodeCount} node${nodeCount !== 1 ? 's' : ''}`,
|
|
@@ -313,7 +434,7 @@ export function TopBar() {
|
|
|
313
434
|
};
|
|
314
435
|
|
|
315
436
|
return (
|
|
316
|
-
<div class="top-bar">
|
|
437
|
+
<div class="top-bar" ref={barRef}>
|
|
317
438
|
<BarHint
|
|
318
439
|
label={`Canvas status: ${statusTitle}`}
|
|
319
440
|
tapToOpen
|
|
@@ -323,73 +444,182 @@ export function TopBar() {
|
|
|
323
444
|
<span class={`connection-dot ${degraded ?? status}`} aria-label={`Canvas status: ${statusTitle}`} />
|
|
324
445
|
</BarHint>
|
|
325
446
|
<BoardSwitcher fallbackName={workspaceName || 'PMX Canvas'} />
|
|
447
|
+
{pinTarget && !browsingMemory.value && <BoardPinButton board={pinTarget} />}
|
|
326
448
|
<span class="top-bar-meta hud-collapsible-text">{sessionId.value ? sessionId.value.slice(0, 12) : '…'}</span>
|
|
327
|
-
<span class="top-bar-meta hud-collapsible-text">{countsLabel}</span>
|
|
449
|
+
{!browsingMemory.value && <span class="top-bar-meta hud-collapsible-text">{countsLabel}</span>}
|
|
328
450
|
|
|
329
451
|
<span class="top-bar-spacer" />
|
|
330
452
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
453
|
+
{/* Keep the context summary; fold controls before compacting sessions
|
|
454
|
+
when the actual content fills the bar (ContextChip600.dc.html). */}
|
|
455
|
+
<div class="top-bar-right">
|
|
456
|
+
<AgentChip />
|
|
457
|
+
<GateBadge />
|
|
458
|
+
<ContextChip />
|
|
459
|
+
<ExternalWriterIndicator />
|
|
460
|
+
{!isHostedWorkbench() && <StartSessionButton />}
|
|
336
461
|
|
|
337
|
-
|
|
462
|
+
{!browsingMemory.value && (
|
|
463
|
+
<>
|
|
464
|
+
<div class="top-bar-sep" />
|
|
465
|
+
<div class="top-bar-actions">
|
|
466
|
+
{activeBoardId.value && (
|
|
467
|
+
<BarHint
|
|
468
|
+
label="Export board"
|
|
469
|
+
body="One HTML file of this board that anyone can open, no install needed."
|
|
470
|
+
>
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
class="present-button"
|
|
474
|
+
onClick={() => {
|
|
475
|
+
exportDialogOpen.value = true;
|
|
476
|
+
}}
|
|
477
|
+
>
|
|
478
|
+
Export
|
|
479
|
+
</button>
|
|
480
|
+
</BarHint>
|
|
481
|
+
)}
|
|
482
|
+
{activeBoardId.value && (
|
|
483
|
+
<BarHint
|
|
484
|
+
label="Present board"
|
|
485
|
+
body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
|
|
486
|
+
>
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
class="present-button"
|
|
490
|
+
onClick={() => {
|
|
491
|
+
void startPresentation();
|
|
492
|
+
}}
|
|
493
|
+
>
|
|
494
|
+
Present
|
|
495
|
+
</button>
|
|
496
|
+
</BarHint>
|
|
497
|
+
)}
|
|
498
|
+
<BarHint label="Zoom out" shortcut={modChord('\u2212')}>
|
|
499
|
+
<button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
|
|
500
|
+
<IconZoomOut />
|
|
501
|
+
</button>
|
|
502
|
+
</BarHint>
|
|
503
|
+
<BarHint label="Reset zoom" shortcut={modChord('0')}>
|
|
504
|
+
<button
|
|
505
|
+
type="button"
|
|
506
|
+
class="top-bar-zoom-label"
|
|
507
|
+
onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)}
|
|
508
|
+
aria-label="Reset view"
|
|
509
|
+
>
|
|
510
|
+
{Math.round(v.scale * 100)}%
|
|
511
|
+
</button>
|
|
512
|
+
</BarHint>
|
|
513
|
+
<BarHint label="Zoom in" shortcut={modChord('+')}>
|
|
514
|
+
<button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1.25)} aria-label="Zoom in">
|
|
515
|
+
<IconZoomIn />
|
|
516
|
+
</button>
|
|
517
|
+
</BarHint>
|
|
518
|
+
<BarHint label="Fit all" shortcut="F" align="end">
|
|
519
|
+
<button type="button" class="top-bar-btn" onClick={handleFit} aria-label="Fit canvas">
|
|
520
|
+
<IconFitAll />
|
|
521
|
+
</button>
|
|
522
|
+
</BarHint>
|
|
523
|
+
</div>
|
|
524
|
+
<TopBarMore boardOpen={!!activeBoardId.value} onFit={handleFit} />
|
|
525
|
+
</>
|
|
526
|
+
)}
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
338
531
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
532
|
+
/** Narrow bars fold Export, Present, zoom and fit into one ⋯ menu (Pane600.dc.html). */
|
|
533
|
+
function TopBarMore({ boardOpen, onFit }: { boardOpen: boolean; onFit: () => void }) {
|
|
534
|
+
const [open, setOpen] = useState(false);
|
|
535
|
+
const root = useRef<HTMLDivElement>(null);
|
|
536
|
+
const trigger = useRef<HTMLButtonElement>(null);
|
|
537
|
+
useLayoutEffect(() => {
|
|
538
|
+
if (!open) return;
|
|
539
|
+
const items = () => [...(root.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])];
|
|
540
|
+
items()[0]?.focus();
|
|
541
|
+
const onKey = (event: KeyboardEvent) => {
|
|
542
|
+
if (event.key === 'Escape') {
|
|
543
|
+
setOpen(false);
|
|
544
|
+
trigger.current?.focus();
|
|
545
|
+
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
546
|
+
event.preventDefault();
|
|
547
|
+
const list = items();
|
|
548
|
+
const at = list.indexOf(document.activeElement as HTMLElement);
|
|
549
|
+
const next = event.key === 'ArrowDown' ? at + 1 : at - 1;
|
|
550
|
+
list[(next + list.length) % list.length]?.focus();
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
const onDown = (event: PointerEvent) => {
|
|
554
|
+
if (root.current && !root.current.contains(event.target as Node)) setOpen(false);
|
|
555
|
+
};
|
|
556
|
+
window.addEventListener('keydown', onKey);
|
|
557
|
+
window.addEventListener('pointerdown', onDown);
|
|
558
|
+
return () => {
|
|
559
|
+
window.removeEventListener('keydown', onKey);
|
|
560
|
+
window.removeEventListener('pointerdown', onDown);
|
|
561
|
+
};
|
|
562
|
+
}, [open]);
|
|
563
|
+
const run = (action: () => void) => () => {
|
|
564
|
+
setOpen(false);
|
|
565
|
+
action();
|
|
566
|
+
};
|
|
567
|
+
return (
|
|
568
|
+
<div class="top-bar-more" ref={root}>
|
|
569
|
+
<button
|
|
570
|
+
ref={trigger}
|
|
571
|
+
type="button"
|
|
572
|
+
class="top-bar-btn"
|
|
573
|
+
aria-label="More: export, present, zoom, fit"
|
|
574
|
+
aria-haspopup="menu"
|
|
575
|
+
aria-expanded={open}
|
|
576
|
+
onClick={() => setOpen(!open)}
|
|
577
|
+
>
|
|
578
|
+
⋯
|
|
579
|
+
</button>
|
|
580
|
+
{open && (
|
|
581
|
+
<div class="toolbar-menu top-bar-more-menu" role="menu">
|
|
582
|
+
{boardOpen && (
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
585
|
+
role="menuitem"
|
|
586
|
+
class="toolbar-menu-item"
|
|
587
|
+
onClick={run(() => {
|
|
588
|
+
exportDialogOpen.value = true;
|
|
589
|
+
})}
|
|
590
|
+
>
|
|
591
|
+
Export board
|
|
592
|
+
</button>
|
|
593
|
+
)}
|
|
594
|
+
{boardOpen && (
|
|
595
|
+
<button
|
|
596
|
+
type="button"
|
|
597
|
+
role="menuitem"
|
|
598
|
+
class="toolbar-menu-item"
|
|
599
|
+
onClick={run(() => void startPresentation())}
|
|
600
|
+
>
|
|
601
|
+
Present board
|
|
602
|
+
</button>
|
|
603
|
+
)}
|
|
604
|
+
<button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(() => zoomByFactor(1.25))}>
|
|
605
|
+
Zoom in
|
|
606
|
+
</button>
|
|
607
|
+
<button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(() => zoomByFactor(1 / 1.25))}>
|
|
608
|
+
Zoom out
|
|
349
609
|
</button>
|
|
350
|
-
</BarHint>
|
|
351
|
-
)}
|
|
352
|
-
{activeBoardId.value && (
|
|
353
|
-
<BarHint
|
|
354
|
-
label="Present board"
|
|
355
|
-
body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
|
|
356
|
-
>
|
|
357
610
|
<button
|
|
358
611
|
type="button"
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}}
|
|
612
|
+
role="menuitem"
|
|
613
|
+
class="toolbar-menu-item"
|
|
614
|
+
onClick={run(() => animateViewport({ x: 0, y: 0, scale: 1 }, 250))}
|
|
363
615
|
>
|
|
364
|
-
|
|
616
|
+
Reset zoom
|
|
365
617
|
</button>
|
|
366
|
-
|
|
618
|
+
<button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(onFit)}>
|
|
619
|
+
Fit all
|
|
620
|
+
</button>
|
|
621
|
+
</div>
|
|
367
622
|
)}
|
|
368
|
-
<BarHint label="Zoom out" shortcut={modChord('\u2212')}>
|
|
369
|
-
<button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1 / 1.25)} aria-label="Zoom out">
|
|
370
|
-
<IconZoomOut />
|
|
371
|
-
</button>
|
|
372
|
-
</BarHint>
|
|
373
|
-
<BarHint label="Reset zoom" shortcut={modChord('0')}>
|
|
374
|
-
<button
|
|
375
|
-
type="button"
|
|
376
|
-
class="top-bar-zoom-label"
|
|
377
|
-
onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)}
|
|
378
|
-
aria-label="Reset view"
|
|
379
|
-
>
|
|
380
|
-
{Math.round(v.scale * 100)}%
|
|
381
|
-
</button>
|
|
382
|
-
</BarHint>
|
|
383
|
-
<BarHint label="Zoom in" shortcut={modChord('+')}>
|
|
384
|
-
<button type="button" class="top-bar-btn" onClick={() => zoomByFactor(1.25)} aria-label="Zoom in">
|
|
385
|
-
<IconZoomIn />
|
|
386
|
-
</button>
|
|
387
|
-
</BarHint>
|
|
388
|
-
<BarHint label="Fit all" shortcut="F" align="end">
|
|
389
|
-
<button type="button" class="top-bar-btn" onClick={handleFit} aria-label="Fit canvas">
|
|
390
|
-
<IconFitAll />
|
|
391
|
-
</button>
|
|
392
|
-
</BarHint>
|
|
393
623
|
</div>
|
|
394
624
|
);
|
|
395
625
|
}
|
|
@@ -49,17 +49,24 @@ export interface CanvasFitInsets {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Screen-space insets Fit must reserve for the floating chrome that overlays
|
|
52
|
-
* the canvas region: the bottom command bar
|
|
52
|
+
* the canvas region: the bottom command bar or standalone context pill,
|
|
53
53
|
* and the bottom-right minimap. Measured from the live elements — fit's old
|
|
54
54
|
* world-space padding shrank with the fit zoom and parked nodes underneath
|
|
55
55
|
* both (0.5.0 Amp report, finding A; architecture rule 9's trap).
|
|
56
56
|
*/
|
|
57
57
|
export function canvasFitInsets(): CanvasFitInsets {
|
|
58
|
+
let top = 16;
|
|
58
59
|
let bottom = 16;
|
|
59
60
|
try {
|
|
60
61
|
const area = canvasArea();
|
|
61
62
|
const areaBottom = area.top + area.height;
|
|
62
|
-
for (const selector of ['.
|
|
63
|
+
for (const selector of ['.lifecycle-filters', '.relation-legend']) {
|
|
64
|
+
const el = document.querySelector(selector);
|
|
65
|
+
if (!(el instanceof HTMLElement) || !el.isConnected) continue;
|
|
66
|
+
const rect = el.getBoundingClientRect();
|
|
67
|
+
if (rect.height > 0) top = Math.max(top, rect.bottom - area.top + 12);
|
|
68
|
+
}
|
|
69
|
+
for (const selector of ['.command-bar', '.context-pin-bar', '.minimap']) {
|
|
63
70
|
const el = document.querySelector(selector);
|
|
64
71
|
if (!(el instanceof HTMLElement) || !el.isConnected) continue;
|
|
65
72
|
const rect = el.getBoundingClientRect();
|
|
@@ -70,5 +77,5 @@ export function canvasFitInsets(): CanvasFitInsets {
|
|
|
70
77
|
} catch {
|
|
71
78
|
// Chromeless embeds and unit DOMs: the defaults below are the contract.
|
|
72
79
|
}
|
|
73
|
-
return { top
|
|
80
|
+
return { top, right: 16, bottom, left: 16 };
|
|
74
81
|
}
|