pmx-canvas 0.4.7 → 0.5.0
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/extension.mjs +154 -5
- package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/CHANGELOG.md +156 -0
- package/Readme.md +72 -23
- package/dist/canvas/global.css +3069 -796
- package/dist/canvas/index.js +104 -104
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/pump.d.ts +9 -0
- package/dist/types/client/App.d.ts +1 -1
- package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
- package/dist/types/client/canvas/BarHint.d.ts +22 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
- package/dist/types/client/canvas/CommandBar.d.ts +10 -0
- package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
- package/dist/types/client/canvas/EmptyState.d.ts +11 -0
- package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
- package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/Minimap.d.ts +12 -1
- package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
- package/dist/types/client/canvas/ToolRail.d.ts +21 -0
- package/dist/types/client/canvas/TopBar.d.ts +7 -0
- package/dist/types/client/canvas/canvas-area.d.ts +25 -0
- package/dist/types/client/canvas/create-in-view.d.ts +18 -0
- package/dist/types/client/canvas/import-files.d.ts +8 -0
- package/dist/types/client/canvas/kind-colors.d.ts +3 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
- package/dist/types/client/canvas/use-now.d.ts +6 -0
- package/dist/types/client/icons.d.ts +11 -4
- package/dist/types/client/nodes/ExtAppFrame.d.ts +32 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
- package/dist/types/client/state/canvas-store.d.ts +49 -5
- package/dist/types/client/state/human-store.d.ts +26 -0
- package/dist/types/client/state/iframe-mode.d.ts +1 -0
- package/dist/types/client/state/intent-bridge.d.ts +37 -5
- package/dist/types/client/state/node-factory.d.ts +0 -1
- package/dist/types/client/state/presence-store.d.ts +78 -0
- package/dist/types/client/state/session-store.d.ts +176 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/server/agent-presence.d.ts +192 -0
- package/dist/types/server/ax-gate-ttl.d.ts +6 -0
- package/dist/types/server/ax-interaction.d.ts +4 -10
- package/dist/types/server/ax-state-manager.d.ts +21 -2
- package/dist/types/server/ax-state.d.ts +49 -3
- package/dist/types/server/ax-wait.d.ts +13 -0
- package/dist/types/server/canvas-db.d.ts +23 -1
- package/dist/types/server/canvas-operations.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +27 -13
- package/dist/types/server/canvas-validation.d.ts +0 -14
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/human-presence.d.ts +40 -0
- package/dist/types/server/index.d.ts +29 -3
- package/dist/types/server/intent-registry.d.ts +7 -0
- package/dist/types/server/mutation-history.d.ts +15 -2
- package/dist/types/server/operations/invoker.d.ts +13 -2
- package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
- package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
- package/dist/types/server/operations/registry.d.ts +11 -0
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/scope-fence.d.ts +62 -0
- package/dist/types/server/server.d.ts +0 -12
- package/dist/types/shared/agent-presence.d.ts +130 -0
- package/dist/types/shared/approval-gates.d.ts +19 -0
- package/dist/types/shared/auto-arrange.d.ts +0 -1
- package/dist/types/shared/ax-kinds.d.ts +11 -0
- package/dist/types/shared/human-presence.d.ts +31 -0
- package/dist/types/shared/iframe-probe.d.ts +8 -0
- package/dist/types/shared/scope-fence.d.ts +21 -0
- package/docs/RELEASE.md +54 -0
- package/docs/ax-host-adapter-contract.md +30 -14
- package/docs/cli.md +37 -1
- package/docs/environment.md +3 -0
- package/docs/http-api.md +222 -5
- package/docs/mcp.md +6 -5
- package/docs/node-types.md +3 -1
- package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/multi-agent-live.png +0 -0
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +1 -1
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +109 -18
- package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
- package/skills/pmx-canvas/references/full-reference.md +55 -16
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
- package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
- package/skills/pmx-canvas-testing/SKILL.md +57 -0
- package/src/cli/agent.ts +1 -0
- package/src/cli/commands/copilot.ts +15 -8
- package/src/cli/commands/nodes.ts +1 -19
- package/src/cli/commands/pump.ts +252 -0
- package/src/cli/index.ts +1 -5
- package/src/cli/shared.ts +1 -1
- package/src/client/App.tsx +209 -585
- package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
- package/src/client/canvas/AttentionHistory.tsx +0 -10
- package/src/client/canvas/BarHint.tsx +49 -0
- package/src/client/canvas/CanvasNode.tsx +488 -136
- package/src/client/canvas/CanvasViewport.tsx +190 -94
- package/src/client/canvas/CommandBar.tsx +140 -0
- package/src/client/canvas/CommandPalette.tsx +133 -58
- package/src/client/canvas/ConnectionBanner.tsx +65 -0
- package/src/client/canvas/ContextMenu.tsx +88 -27
- package/src/client/canvas/ContextPinBar.tsx +2 -2
- package/src/client/canvas/EdgeLayer.tsx +121 -38
- package/src/client/canvas/EmptyState.tsx +93 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
- package/src/client/canvas/ExternalWriters.tsx +308 -0
- package/src/client/canvas/FocusFieldLayer.tsx +12 -1
- package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
- package/src/client/canvas/IntentLayer.tsx +1 -1
- package/src/client/canvas/Minimap.tsx +117 -190
- package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
- package/src/client/canvas/SelectionBar.tsx +175 -23
- package/src/client/canvas/SessionPanel.tsx +481 -0
- package/src/client/canvas/SessionReceipt.tsx +141 -0
- package/src/client/canvas/ShortcutOverlay.tsx +21 -6
- package/src/client/canvas/SnapshotPanel.tsx +166 -80
- package/src/client/canvas/TextPrompt.tsx +128 -0
- package/src/client/canvas/ToolRail.tsx +447 -0
- package/src/client/canvas/TopBar.tsx +308 -0
- package/src/client/canvas/auto-fit.ts +1 -5
- package/src/client/canvas/canvas-area.ts +41 -0
- package/src/client/canvas/create-in-view.ts +48 -0
- package/src/client/canvas/import-files.ts +64 -0
- package/src/client/canvas/kind-colors.ts +22 -0
- package/src/client/canvas/snap-guides.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +54 -0
- package/src/client/canvas/use-now.ts +17 -0
- package/src/client/canvas/use-pan-zoom.ts +13 -2
- package/src/client/icons.tsx +57 -18
- package/src/client/nodes/ExtAppFrame.tsx +79 -2
- package/src/client/nodes/GroupNode.tsx +9 -28
- package/src/client/nodes/InlineFormatBar.tsx +4 -3
- package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
- package/src/client/nodes/MarkdownNode.tsx +59 -24
- package/src/client/nodes/MdFormatBar.tsx +2 -4
- package/src/client/nodes/inline-editor-commands.ts +17 -6
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/state/attention-bridge.ts +3 -0
- package/src/client/state/canvas-store.ts +273 -84
- package/src/client/state/human-store.ts +175 -0
- package/src/client/state/iframe-mode.ts +33 -9
- package/src/client/state/intent-bridge.ts +75 -13
- package/src/client/state/node-factory.ts +2 -8
- package/src/client/state/presence-store.ts +168 -0
- package/src/client/state/session-store.ts +419 -0
- package/src/client/state/sse-bridge.ts +93 -30
- package/src/client/theme/global.css +3069 -796
- package/src/client/types.ts +3 -1
- package/src/client/utils/platform.ts +6 -1
- package/src/mcp/canvas-access.ts +2 -2
- package/src/server/agent-presence.ts +755 -0
- package/src/server/ax-context.ts +45 -0
- package/src/server/ax-gate-ttl.ts +61 -0
- package/src/server/ax-interaction.ts +29 -1
- package/src/server/ax-state-manager.ts +78 -6
- package/src/server/ax-state.ts +82 -15
- package/src/server/ax-wait.ts +33 -0
- package/src/server/canvas-db.ts +237 -61
- package/src/server/canvas-operations.ts +17 -8
- package/src/server/canvas-state.ts +131 -58
- package/src/server/canvas-validation.ts +3 -53
- package/src/server/demo-state.json +2 -70
- package/src/server/html-surface.ts +11 -2
- package/src/server/human-presence.ts +160 -0
- package/src/server/index.ts +107 -4
- package/src/server/intent-registry.ts +13 -0
- package/src/server/mutation-history.ts +47 -2
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/http.ts +10 -1
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/invoker.ts +33 -6
- package/src/server/operations/ops/ax-presence.ts +62 -0
- package/src/server/operations/ops/ax-read.ts +8 -0
- package/src/server/operations/ops/ax-state.ts +106 -1
- package/src/server/operations/ops/ax-timeline.ts +54 -5
- package/src/server/operations/ops/ax-work.ts +46 -0
- package/src/server/operations/ops/batch.ts +3 -1
- package/src/server/operations/ops/edges.ts +88 -2
- package/src/server/operations/ops/groups.ts +4 -3
- package/src/server/operations/ops/human-presence.ts +35 -0
- package/src/server/operations/ops/json-render.ts +11 -1
- package/src/server/operations/ops/nodes.ts +65 -16
- package/src/server/operations/ops/query.ts +1 -0
- package/src/server/operations/ops/snapshots.ts +16 -0
- package/src/server/operations/registry.ts +196 -1
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +207 -0
- package/src/server/server.ts +177 -79
- package/src/server/trace-manager.ts +0 -2
- package/src/server/web-artifacts.ts +0 -1
- package/src/shared/agent-presence.ts +162 -0
- package/src/shared/approval-gates.ts +35 -0
- package/src/shared/auto-arrange.ts +121 -13
- package/src/shared/ax-kinds.ts +23 -0
- package/src/shared/human-presence.ts +30 -0
- package/src/shared/iframe-probe.ts +8 -0
- package/src/shared/scope-fence.ts +31 -0
- package/dist/types/client/canvas/DockedNode.d.ts +0 -4
- package/src/client/canvas/DockedNode.tsx +0 -165
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import type { AgentPresence } from '../../shared/agent-presence.js';
|
|
3
|
+
import { vetoGhostIntent } from '../state/intent-bridge';
|
|
4
|
+
import { type ClientIntent, intents, removeIntent } from '../state/intent-store';
|
|
5
|
+
import {
|
|
6
|
+
activityFeedOpen,
|
|
7
|
+
activityFilter,
|
|
8
|
+
agentActivity,
|
|
9
|
+
agentPresences,
|
|
10
|
+
externalWriterPresences,
|
|
11
|
+
relativeAge,
|
|
12
|
+
sessionActive,
|
|
13
|
+
writerColor,
|
|
14
|
+
writerInitial,
|
|
15
|
+
writersSheetOpen,
|
|
16
|
+
} from '../state/presence-store';
|
|
17
|
+
import { scopeFence, pendingGates, startSession } from '../state/session-store';
|
|
18
|
+
import { sessionId } from '../state/canvas-store';
|
|
19
|
+
import { useFocusTrap } from './use-focus-trap';
|
|
20
|
+
import { useNow } from './use-now';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* External Steering surfaces (rail-chrome-v2 phase 6, design items 1, 9, 17):
|
|
24
|
+
* an external agent writes to the board with NO session attached. The chrome
|
|
25
|
+
* stays quiet; the only additions are a passive top-bar indicator (writers +
|
|
26
|
+
* op count), a click-to-open activity feed with per-writer filters and inline
|
|
27
|
+
* Veto on pending intents, and a connected-writers sheet that is pure
|
|
28
|
+
* visibility — pmx-canvas is local-first, veto/ghosting is the safety model,
|
|
29
|
+
* not permissions. Every surface here mounts only while external writers are
|
|
30
|
+
* live and no session is attached.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
function Avatar({ presence, size = 18 }: { presence: Pick<AgentPresence, 'sessionId' | 'label'>; size?: number }) {
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
class="writer-avatar"
|
|
37
|
+
style={{ background: writerColor(presence.sessionId), width: `${size}px`, height: `${size}px` }}
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
>
|
|
40
|
+
{writerInitial(presence.label)}
|
|
41
|
+
</span>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Top-bar indicator: avatar cluster, "3 writers", op count, pulsing dot. */
|
|
46
|
+
export function ExternalWriterIndicator() {
|
|
47
|
+
const writers = externalWriterPresences.value;
|
|
48
|
+
if (sessionActive.value || writers.length === 0) return null;
|
|
49
|
+
const ops = writers.reduce((sum, writer) => sum + writer.opCount, 0);
|
|
50
|
+
const label = writers.length === 1 ? writers[0]!.label : `${writers.length} writers`;
|
|
51
|
+
const open = activityFeedOpen.value;
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
class="external-indicator"
|
|
56
|
+
data-testid="external-indicator"
|
|
57
|
+
aria-expanded={open}
|
|
58
|
+
title={`${writers.length} external writer${writers.length === 1 ? '' : 's'} on this board — click for activity`}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
activityFeedOpen.value = !open;
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<span class="external-indicator-avatars">
|
|
64
|
+
{writers.slice(0, 3).map((writer) => (
|
|
65
|
+
<Avatar key={writer.sessionId} presence={writer} />
|
|
66
|
+
))}
|
|
67
|
+
</span>
|
|
68
|
+
<span class="external-indicator-label">{label}</span>
|
|
69
|
+
<span class="external-indicator-ops hud-collapsible-text">
|
|
70
|
+
{ops} op{ops === 1 ? '' : 's'}
|
|
71
|
+
</span>
|
|
72
|
+
<span class="external-indicator-dot" aria-hidden="true" />
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function opGlyph(op: string): { glyph: string; tone: string } {
|
|
78
|
+
if (op === 'node.add' || op === 'jsonrender.add' || op === 'graph.add' || op === 'group.create') {
|
|
79
|
+
return { glyph: '+', tone: 'ok' };
|
|
80
|
+
}
|
|
81
|
+
if (op === 'node.remove' || op === 'edge.remove' || op === 'canvas.clear') return { glyph: '×', tone: 'danger' };
|
|
82
|
+
if (op === 'edge.add') return { glyph: '⟶', tone: 'accent' };
|
|
83
|
+
if (op.startsWith('ax.')) return { glyph: '◆', tone: 'purple' };
|
|
84
|
+
return { glyph: '✎', tone: 'accent' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** ISO timestamp of the most recent write across the writers. */
|
|
88
|
+
function latestWrite(writers: readonly AgentPresence[]): string {
|
|
89
|
+
return writers.reduce((latest, writer) => (writer.lastSeenAt > latest ? writer.lastSeenAt : latest), '');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Pending explicit intents from external writers — the rows that carry an inline Veto. */
|
|
93
|
+
function pendingProposals(): ClientIntent[] {
|
|
94
|
+
return [...intents.value.values()].filter((intent) => !intent.auto && intent.phase === 'forming');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function ActivityFeed() {
|
|
98
|
+
const open = activityFeedOpen.value;
|
|
99
|
+
const writers = externalWriterPresences.value;
|
|
100
|
+
const now = useNow(open ? 10_000 : 0);
|
|
101
|
+
// Esc closes the top-most surface: the sheet first, then the feed.
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!open) return;
|
|
104
|
+
const onKey = (e: KeyboardEvent) => {
|
|
105
|
+
if (e.key !== 'Escape') return;
|
|
106
|
+
if (writersSheetOpen.value) writersSheetOpen.value = false;
|
|
107
|
+
else activityFeedOpen.value = false;
|
|
108
|
+
};
|
|
109
|
+
window.addEventListener('keydown', onKey);
|
|
110
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
111
|
+
}, [open]);
|
|
112
|
+
if (!open || sessionActive.value || writers.length === 0) return null;
|
|
113
|
+
|
|
114
|
+
const filter = activityFilter.value;
|
|
115
|
+
const proposals = pendingProposals().filter((intent) => !filter || (intent.source ?? '') === filter);
|
|
116
|
+
const rows = agentActivity.value.filter((entry) => !filter || entry.sessionId === filter);
|
|
117
|
+
const filterChip = (id: string | null, label: string) => (
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
class={`activity-filter${filter === id ? ' is-active' : ''}`}
|
|
121
|
+
style={id ? { '--writer': writerColor(id) } : undefined}
|
|
122
|
+
aria-pressed={filter === id}
|
|
123
|
+
onClick={() => {
|
|
124
|
+
activityFilter.value = id;
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{label}
|
|
128
|
+
</button>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div class="activity-feed" data-testid="activity-feed" role="dialog" aria-label="External activity">
|
|
133
|
+
<div class="activity-feed-head">
|
|
134
|
+
<span class="external-indicator-dot" aria-hidden="true" />
|
|
135
|
+
<div class="activity-feed-titles">
|
|
136
|
+
<div class="activity-feed-title">
|
|
137
|
+
External activity — {writers.length} writer{writers.length === 1 ? '' : 's'}
|
|
138
|
+
</div>
|
|
139
|
+
<div class="activity-feed-sub">
|
|
140
|
+
{writers.reduce((sum, writer) => sum + writer.opCount, 0)} writes · last{' '}
|
|
141
|
+
{relativeAge(latestWrite(writers), now)} · no session attached
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
class="activity-feed-close"
|
|
147
|
+
aria-label="Close activity feed"
|
|
148
|
+
onClick={() => {
|
|
149
|
+
activityFeedOpen.value = false;
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
×
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="activity-filters">
|
|
156
|
+
{filterChip(null, 'All')}
|
|
157
|
+
{writers.map((writer) => filterChip(writer.sessionId, writer.label))}
|
|
158
|
+
</div>
|
|
159
|
+
<div class="activity-rows">
|
|
160
|
+
{proposals.map((intent) => (
|
|
161
|
+
<div key={intent.id} class="activity-row is-pending" data-testid="activity-proposal">
|
|
162
|
+
<span class="activity-glyph tone-purple" aria-hidden="true">
|
|
163
|
+
▢
|
|
164
|
+
</span>
|
|
165
|
+
<div class="activity-main">
|
|
166
|
+
<div class="activity-text">
|
|
167
|
+
Proposing: <b>{intent.label?.trim() || `${intent.kind} intent`}</b>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="activity-meta">
|
|
170
|
+
{intent.reason?.trim() || 'Intent signalled — a real veto window before the mutation'}
|
|
171
|
+
{intent.source ? ` · ${intent.source}` : ''}
|
|
172
|
+
</div>
|
|
173
|
+
<div class="activity-actions">
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
class="activity-veto"
|
|
177
|
+
onClick={() => {
|
|
178
|
+
void vetoGhostIntent(intent).then((ok) => {
|
|
179
|
+
if (ok) removeIntent(intent.id);
|
|
180
|
+
});
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
Veto
|
|
184
|
+
</button>
|
|
185
|
+
<span class="activity-hint">or hover ghost + Esc</span>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<span class="activity-age">now</span>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
{rows.length === 0 && proposals.length === 0 && <div class="activity-empty">No writes yet.</div>}
|
|
192
|
+
{rows.map((entry) => {
|
|
193
|
+
const { glyph, tone } = opGlyph(entry.op);
|
|
194
|
+
return (
|
|
195
|
+
<div key={entry.id} class="activity-row" data-testid="activity-row" data-op={entry.op}>
|
|
196
|
+
<span class={`activity-glyph tone-${tone}`} aria-hidden="true">
|
|
197
|
+
{glyph}
|
|
198
|
+
</span>
|
|
199
|
+
<div class="activity-main">
|
|
200
|
+
<div class="activity-text">{entry.summary}</div>
|
|
201
|
+
<div class="activity-writer" style={{ color: writerColor(entry.sessionId) }}>
|
|
202
|
+
{entry.label}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<span class="activity-age">{relativeAge(entry.at, now)}</span>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</div>
|
|
210
|
+
<div class="activity-feed-foot">
|
|
211
|
+
<span class="activity-feed-note">All writes ghost first · Esc vetoes</span>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
class="activity-feed-link"
|
|
215
|
+
onClick={() => {
|
|
216
|
+
writersSheetOpen.value = true;
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
Writers
|
|
220
|
+
</button>
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
class="activity-feed-link is-accent"
|
|
224
|
+
onClick={() => {
|
|
225
|
+
activityFeedOpen.value = false;
|
|
226
|
+
void startSession();
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
Start session ↗
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sessionConfig(): string {
|
|
237
|
+
const parts: string[] = [];
|
|
238
|
+
if (scopeFence.value) parts.push('scoped');
|
|
239
|
+
if (pendingGates.value.length > 0) parts.push('gated');
|
|
240
|
+
return parts.length > 0 ? parts.join(' + ') : 'open';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Connected writers — pure visibility, no permissions. */
|
|
244
|
+
export function WritersSheet() {
|
|
245
|
+
const open = writersSheetOpen.value;
|
|
246
|
+
const now = useNow(open ? 10_000 : 0);
|
|
247
|
+
const sheetRef = useRef<HTMLDivElement>(null);
|
|
248
|
+
useFocusTrap(sheetRef, open);
|
|
249
|
+
if (!open) return null;
|
|
250
|
+
const sessions = agentPresences.value.filter((presence) => presence.attached);
|
|
251
|
+
const external = agentPresences.value.filter((presence) => !presence.attached);
|
|
252
|
+
const close = () => {
|
|
253
|
+
writersSheetOpen.value = false;
|
|
254
|
+
};
|
|
255
|
+
return (
|
|
256
|
+
<>
|
|
257
|
+
<div class="writers-scrim" onClick={close} aria-hidden="true" />
|
|
258
|
+
<div
|
|
259
|
+
ref={sheetRef}
|
|
260
|
+
class="writers-sheet"
|
|
261
|
+
data-testid="writers-sheet"
|
|
262
|
+
role="dialog"
|
|
263
|
+
aria-modal="true"
|
|
264
|
+
aria-label="Connected writers"
|
|
265
|
+
>
|
|
266
|
+
<div class="writers-head">
|
|
267
|
+
<span class="writers-title">Connected writers</span>
|
|
268
|
+
<span class="writers-board">{sessionId.value.slice(0, 12)}</span>
|
|
269
|
+
<button type="button" class="activity-feed-close" aria-label="Close writers" onClick={close}>
|
|
270
|
+
×
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="writers-body">
|
|
274
|
+
<div class="writers-section">Agent sessions</div>
|
|
275
|
+
{sessions.length === 0 && <div class="writers-empty">None attached — start one from the top bar.</div>}
|
|
276
|
+
{sessions.map((presence) => (
|
|
277
|
+
<div key={presence.sessionId} class="writers-row">
|
|
278
|
+
<Avatar presence={presence} size={20} />
|
|
279
|
+
<span class="writers-name">{presence.label}</span>
|
|
280
|
+
<span class="writers-config">{sessionConfig()}</span>
|
|
281
|
+
</div>
|
|
282
|
+
))}
|
|
283
|
+
<div class="writers-section">External writers · MCP</div>
|
|
284
|
+
{external.length === 0 && <div class="writers-empty">No external writers right now.</div>}
|
|
285
|
+
{external.map((presence) => (
|
|
286
|
+
<div key={presence.sessionId} class="writers-row">
|
|
287
|
+
<Avatar presence={presence} size={20} />
|
|
288
|
+
<span class="writers-name is-mono">{presence.label}</span>
|
|
289
|
+
<span class="writers-meta">
|
|
290
|
+
{presence.source !== presence.label ? `${presence.source} · ` : ''}wrote{' '}
|
|
291
|
+
{relativeAge(presence.lastSeenAt, now)} ago
|
|
292
|
+
</span>
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
<div class="writers-foot">
|
|
297
|
+
<span class="writers-note">
|
|
298
|
+
Local-first: any process on this machine can connect and write. Every write ghosts first — veto is the
|
|
299
|
+
control, not permissions.
|
|
300
|
+
</span>
|
|
301
|
+
<button type="button" class="writers-done" onClick={close}>
|
|
302
|
+
Done
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -10,7 +10,7 @@ interface Rect {
|
|
|
10
10
|
|
|
11
11
|
function getNodeRect(nodeId: string): Rect | null {
|
|
12
12
|
const node = nodes.value.get(nodeId);
|
|
13
|
-
if (!node
|
|
13
|
+
if (!node) return null;
|
|
14
14
|
return {
|
|
15
15
|
left: node.position.x,
|
|
16
16
|
top: node.position.y,
|
|
@@ -55,11 +55,22 @@ export function FocusFieldLayer() {
|
|
|
55
55
|
|
|
56
56
|
if (primaryNodeIds.length === 0 && secondaryNodeIds.length === 0) return null;
|
|
57
57
|
|
|
58
|
+
// A region that spans (nearly) the whole populated board distinguishes
|
|
59
|
+
// nothing — pins far apart degrade the glow into a border around
|
|
60
|
+
// everything. Skip those; the per-node halos still mark the pins.
|
|
61
|
+
const all = [...nodes.value.values()];
|
|
62
|
+
const boardArea =
|
|
63
|
+
all.length > 0
|
|
64
|
+
? (Math.max(...all.map((n) => n.position.x + n.size.width)) - Math.min(...all.map((n) => n.position.x))) *
|
|
65
|
+
(Math.max(...all.map((n) => n.position.y + n.size.height)) - Math.min(...all.map((n) => n.position.y)))
|
|
66
|
+
: 0;
|
|
67
|
+
|
|
58
68
|
return (
|
|
59
69
|
<div class="attention-field-layer" aria-hidden="true">
|
|
60
70
|
{regions.map((region) => {
|
|
61
71
|
const rect = getRegionRect(region.nodeIds);
|
|
62
72
|
if (!rect) return null;
|
|
73
|
+
if (boardArea > 0 && rect.width * rect.height >= boardArea * 0.75) return null;
|
|
63
74
|
return <div key={region.id} class="attention-field-region" style={rectStyle(rect, 42)} />;
|
|
64
75
|
})}
|
|
65
76
|
{secondaryNodeIds.map((nodeId) => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { HUMAN_PRESENCE_TTL_MS } from '../../shared/human-presence.js';
|
|
2
|
+
import { viewport } from '../state/canvas-store';
|
|
3
|
+
import { otherHumans } from '../state/human-store';
|
|
4
|
+
import { useNow } from './use-now';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Human collaborator cursors (rail-chrome-v2 phase 8, item 5): the other
|
|
8
|
+
* render style of the presence layer — green arrow + name tag, never a phase
|
|
9
|
+
* chip. Mounts whenever another tab is on the board, session or not (humans
|
|
10
|
+
* collaborate on quiet boards too). World-space like the agent layer, glyph
|
|
11
|
+
* counter-scaled so it keeps a constant screen size.
|
|
12
|
+
*/
|
|
13
|
+
export function HumanPresenceLayer() {
|
|
14
|
+
// Staleness guard (0.5.0 readiness): an embedded pane whose stream stalls
|
|
15
|
+
// (background throttling, dead SSE) kept painting ancient guest cursors from
|
|
16
|
+
// its last frame. Cursors past the server TTL drop on the local clock even
|
|
17
|
+
// when no fresh frame ever arrives.
|
|
18
|
+
const now = useNow(otherHumans.value.length > 0 ? 5_000 : 0);
|
|
19
|
+
const humans = otherHumans.value.filter(
|
|
20
|
+
(human) =>
|
|
21
|
+
human.cursor !== null &&
|
|
22
|
+
(!human.lastSeenAt || now - Date.parse(human.lastSeenAt) < HUMAN_PRESENCE_TTL_MS + 5_000),
|
|
23
|
+
);
|
|
24
|
+
if (humans.length === 0) return null;
|
|
25
|
+
const counterScale = 1 / Math.max(0.05, viewport.value.scale);
|
|
26
|
+
return (
|
|
27
|
+
<div class="human-presence-layer" aria-hidden="true">
|
|
28
|
+
{humans.map((human) => (
|
|
29
|
+
<div
|
|
30
|
+
key={human.clientId}
|
|
31
|
+
class={`human-cursor${human.grabbingNodeId ? ' is-grabbing' : ''}`}
|
|
32
|
+
data-client-id={human.clientId}
|
|
33
|
+
style={{ transform: `translate(${human.cursor!.x}px, ${human.cursor!.y}px)` }}
|
|
34
|
+
>
|
|
35
|
+
<div class="human-cursor-inner" style={{ transform: `scale(${counterScale})` }}>
|
|
36
|
+
<svg class="human-cursor-glyph" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
|
37
|
+
<path d="M3 2 L13 8 L8.5 9.5 L6.5 14 Z" fill="currentColor" stroke="var(--c-bg)" stroke-width="1" />
|
|
38
|
+
</svg>
|
|
39
|
+
<span class="human-cursor-tag">{human.name}</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -50,7 +50,7 @@ function isKnownNodeType(value: string | undefined): value is CanvasNodeState['t
|
|
|
50
50
|
function getNodeRect(nodeId: string | undefined): Rect | null {
|
|
51
51
|
if (!nodeId) return null;
|
|
52
52
|
const node = nodes.value.get(nodeId);
|
|
53
|
-
if (!node
|
|
53
|
+
if (!node) return null;
|
|
54
54
|
return { left: node.position.x, top: node.position.y, width: node.size.width, height: node.size.height };
|
|
55
55
|
}
|
|
56
56
|
|