pmx-canvas 0.7.3 → 0.8.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/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 +47 -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 +723 -672
- package/dist/canvas/index.js +90 -88
- package/dist/canvas/mcp-app.html +7878 -0
- package/dist/canvas/mermaid-entry.js +1 -1
- package/dist/canvas/surface-theme.css +151 -46
- package/dist/json-render/index.css +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/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 +208 -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 +214 -0
- package/docs/environment.md +1 -1
- package/docs/http-api.md +80 -15
- package/docs/mcp.md +198 -3
- 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 +10 -7
- 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/view.ts +1 -1
- package/src/cli/index.ts +8 -3
- 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/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 +723 -672
- 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/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 +6 -24
- 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 +86 -15
- package/src/server/demo-state.json +31 -31
- package/src/server/html-surface.ts +4 -5
- package/src/server/index.ts +19 -3
- 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/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 +1 -1
- 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 +38 -4
- 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
|
@@ -2,6 +2,7 @@ import { signal } from '@preact/signals';
|
|
|
2
2
|
import { useEffect, useState } from 'preact/hooks';
|
|
3
3
|
import { requestJson } from '../state/intent-bridge';
|
|
4
4
|
import { activeBoardId } from '../state/boards-store';
|
|
5
|
+
import { downloadWorkbenchExport, isHostedWorkbench } from '../state/workbench-transport';
|
|
5
6
|
|
|
6
7
|
interface ExportManifest {
|
|
7
8
|
boardId: string;
|
|
@@ -43,6 +44,7 @@ export function ExportDialog() {
|
|
|
43
44
|
const [manifest, setManifest] = useState<ExportManifest | null>(null);
|
|
44
45
|
const [result, setResult] = useState<ExportResult | null>(null);
|
|
45
46
|
const [busy, setBusy] = useState(false);
|
|
47
|
+
const [downloadError, setDownloadError] = useState('');
|
|
46
48
|
const open = exportDialogOpen.value;
|
|
47
49
|
const boardId = activeBoardId.value;
|
|
48
50
|
|
|
@@ -50,6 +52,7 @@ export function ExportDialog() {
|
|
|
50
52
|
if (!open || !boardId) return;
|
|
51
53
|
let current = true;
|
|
52
54
|
setResult(null);
|
|
55
|
+
setDownloadError('');
|
|
53
56
|
setManifest(null);
|
|
54
57
|
void requestJson<{ manifest?: ExportManifest } | null>(
|
|
55
58
|
'exportPreview',
|
|
@@ -174,13 +177,37 @@ export function ExportDialog() {
|
|
|
174
177
|
Saved ({sizeLabel(result.bytes)}): <code>{result.path}</code>
|
|
175
178
|
</div>
|
|
176
179
|
<div class="export-dialog-links">
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
{isHostedWorkbench() ? (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
disabled={busy}
|
|
184
|
+
onClick={async () => {
|
|
185
|
+
setBusy(true);
|
|
186
|
+
setDownloadError('');
|
|
187
|
+
try {
|
|
188
|
+
await downloadWorkbenchExport(result.url);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
setDownloadError(error instanceof Error ? error.message : 'Download failed. Try again.');
|
|
191
|
+
} finally {
|
|
192
|
+
setBusy(false);
|
|
193
|
+
}
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
{busy ? 'Downloading…' : 'Download'}
|
|
197
|
+
</button>
|
|
198
|
+
) : (
|
|
199
|
+
<>
|
|
200
|
+
<a href={result.url} target="_blank" rel="noopener noreferrer">
|
|
201
|
+
Open
|
|
202
|
+
</a>
|
|
203
|
+
<a class="is-primary" href={`${result.url}?download=1`}>
|
|
204
|
+
Download
|
|
205
|
+
</a>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
183
208
|
</div>
|
|
209
|
+
{isHostedWorkbench() && <p>Download the HTML file to open it outside ChatGPT.</p>}
|
|
210
|
+
{downloadError && <p role="alert">{downloadError}</p>}
|
|
184
211
|
</div>
|
|
185
212
|
)}
|
|
186
213
|
<div class="text-prompt-actions">
|
|
@@ -8,8 +8,8 @@ import type { CanvasNodeState } from '../types';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Ghost Cursor of Intent overlay. Renders the agent's pre-commit moves as faint
|
|
11
|
-
* placeholders in world space (it lives inside the canvas transform,
|
|
12
|
-
*
|
|
11
|
+
* placeholders in world space (it lives inside the canvas transform, so positions
|
|
12
|
+
* are world coords). Five kinds:
|
|
13
13
|
* create → dashed ghost node with icon + type badge
|
|
14
14
|
* move → ghost at the destination + a dashed trail from the current node
|
|
15
15
|
* connect → dashed bezier in the edge-type color
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { agentIdentityHue } from '../../shared/agent-presence.js';
|
|
2
1
|
import type { Signal } from '@preact/signals';
|
|
3
2
|
import { useCallback, useRef } from 'preact/hooks';
|
|
4
3
|
import { fenceRectFromNodes } from '../../shared/scope-fence.js';
|
|
5
|
-
import { selectedNodeIds } from '../state/canvas-store';
|
|
6
|
-
import {
|
|
4
|
+
import { contextPinnedNodeIds, selectedNodeIds } from '../state/canvas-store';
|
|
5
|
+
import {
|
|
6
|
+
agentPresences,
|
|
7
|
+
mutatingNodeIds,
|
|
8
|
+
presenceWorldPosition,
|
|
9
|
+
sessionActive,
|
|
10
|
+
writerColor,
|
|
11
|
+
} from '../state/presence-store';
|
|
7
12
|
import { scopeFence } from '../state/session-store';
|
|
8
13
|
import type { CanvasEdge, CanvasNodeState, ViewportState } from '../types';
|
|
9
|
-
import { KIND_COLOR } from './kind-colors';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Minimap v2 (rail-chrome-v2 phase 7, design item 19): a true-scale node map
|
|
@@ -141,6 +145,8 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
|
|
|
141
145
|
);
|
|
142
146
|
|
|
143
147
|
const selected = selectedNodeIds.value;
|
|
148
|
+
const pinned = contextPinnedNodeIds.value;
|
|
149
|
+
const agentAtWork = sessionActive.value ? mutatingNodeIds.value : new Set<string>();
|
|
144
150
|
const fence = scopeFence.value;
|
|
145
151
|
const fenceRect = fence
|
|
146
152
|
? fenceRectFromNodes(
|
|
@@ -177,14 +183,13 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
|
|
|
177
183
|
return (
|
|
178
184
|
<span
|
|
179
185
|
key={node.id}
|
|
180
|
-
class={`minimap-node${isGroup ? ' is-group' : ''}${selected.has(node.id) ? ' is-selected' : ''}`}
|
|
186
|
+
class={`minimap-node${isGroup ? ' is-group' : ''}${selected.has(node.id) ? ' is-selected' : ''}${pinned.has(node.id) ? ' is-pinned' : ''}${agentAtWork.has(node.id) ? ' is-agent' : ''}`}
|
|
181
187
|
data-kind={node.type}
|
|
182
188
|
style={{
|
|
183
189
|
left: `${toX(node.position.x)}px`,
|
|
184
190
|
top: `${toY(node.position.y)}px`,
|
|
185
191
|
width: `${Math.max(isGroup ? 6 : 4, node.size.width * scale)}px`,
|
|
186
192
|
height: `${Math.max(isGroup ? 5 : 3, node.size.height * scale)}px`,
|
|
187
|
-
'--kind': KIND_COLOR[node.type],
|
|
188
193
|
}}
|
|
189
194
|
/>
|
|
190
195
|
);
|
|
@@ -207,7 +212,7 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
|
|
|
207
212
|
style={{
|
|
208
213
|
left: `${toX(dot.at.x) - 4}px`,
|
|
209
214
|
top: `${toY(dot.at.y) - 4}px`,
|
|
210
|
-
'--identity-color':
|
|
215
|
+
'--identity-color': writerColor(dot.id),
|
|
211
216
|
}}
|
|
212
217
|
/>
|
|
213
218
|
))}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useState } from 'preact/hooks';
|
|
2
|
+
import { contextPinMeta, pinnedReadState, writerName } from '../state/context-status-store';
|
|
3
|
+
import { centerDistance } from '../../server/spatial-analysis.js';
|
|
4
|
+
import { nodes } from '../state/canvas-store';
|
|
5
|
+
import { nearPins } from '../state/near-pin-store';
|
|
6
|
+
import type { CanvasNodeState } from '../types';
|
|
7
|
+
import { BarHint } from './BarHint';
|
|
8
|
+
|
|
9
|
+
// Glyphs from docs/design/AgentContext.dc.html (24-unit paths).
|
|
10
|
+
export const GLYPHS = {
|
|
11
|
+
eye: 'M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12zM12 9.5a2.5 2.5 0 1 0 0 5a2.5 2.5 0 1 0 0-5',
|
|
12
|
+
eyeOff:
|
|
13
|
+
'M3 3l18 18M10.6 6.1A10 10 0 0 1 12 6c6.5 0 10 6 10 6a17 17 0 0 1-3 3.6M6.6 6.6C3.8 8.3 2 12 2 12s3.5 6 10 6a9.6 9.6 0 0 0 4.4-1',
|
|
14
|
+
spark: 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z',
|
|
15
|
+
pen: 'M4 20l4.2-1L19 8.2 15.8 5 5 15.8z',
|
|
16
|
+
warn: 'M12 4l9 16H3zM12 10v4M12 17v.5',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
interface Mark {
|
|
20
|
+
tone: 'agent' | 'muted' | 'warn';
|
|
21
|
+
glyph: keyof typeof GLYPHS;
|
|
22
|
+
word: string;
|
|
23
|
+
label: string;
|
|
24
|
+
body: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clock(iso: string): string {
|
|
28
|
+
const date = new Date(iso);
|
|
29
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function markFor(node: CanvasNodeState, pinned: boolean): Mark | null {
|
|
33
|
+
if (pinned) {
|
|
34
|
+
const pin = contextPinMeta.value[node.id];
|
|
35
|
+
const pinnedBy =
|
|
36
|
+
pin?.pinnedBy.actor === 'agent'
|
|
37
|
+
? ` Pinned by ${writerName(pin.pinnedBy)}${pin.reason ? `: ${pin.reason}` : ''}.`
|
|
38
|
+
: '';
|
|
39
|
+
const state = pinnedReadState(node.id, node.contentRevision ?? 0);
|
|
40
|
+
if (state.kind === 'not-read') {
|
|
41
|
+
return {
|
|
42
|
+
tone: 'muted',
|
|
43
|
+
glyph: 'eyeOff',
|
|
44
|
+
word: 'not read',
|
|
45
|
+
label: 'Not read yet',
|
|
46
|
+
body: `In context, but no agent has loaded it since it was pinned.${pinnedBy}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (state.kind === 'changed') {
|
|
50
|
+
return {
|
|
51
|
+
tone: 'warn',
|
|
52
|
+
glyph: 'warn',
|
|
53
|
+
word: 'changed since read',
|
|
54
|
+
label: "The agent's copy is out of date",
|
|
55
|
+
body: `Changed after ${state.by} read it at ${clock(state.at)}.${pinnedBy}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
tone: 'agent',
|
|
60
|
+
glyph: 'eye',
|
|
61
|
+
word: 'read',
|
|
62
|
+
label: 'Read by the agent',
|
|
63
|
+
body: `${state.count > 1 ? `Read ${state.count} times · last` : 'Read'} ${clock(state.at)} by ${state.by}.${pinnedBy}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// A person's edit ends the agent's byline (decided 2026-10-05).
|
|
67
|
+
if (node.lastEditedBy?.actor === 'human') return null;
|
|
68
|
+
if (node.createdBy?.actor === 'agent') {
|
|
69
|
+
const name = writerName(node.createdBy);
|
|
70
|
+
return {
|
|
71
|
+
tone: 'agent',
|
|
72
|
+
glyph: 'spark',
|
|
73
|
+
word: `by ${name}`,
|
|
74
|
+
label: `Created by ${name}`,
|
|
75
|
+
body: 'The byline stays until you edit the node.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (node.lastEditedBy?.actor === 'agent') {
|
|
79
|
+
const name = writerName(node.lastEditedBy);
|
|
80
|
+
return {
|
|
81
|
+
tone: 'agent',
|
|
82
|
+
glyph: 'pen',
|
|
83
|
+
word: 'edited',
|
|
84
|
+
label: `Edited by ${name}`,
|
|
85
|
+
body: 'The mark stays until you edit the node.',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The header chip saying what agents did with this node (one per node; glyph + word). */
|
|
92
|
+
export function NodeContextMark({ node, pinned }: { node: CanvasNodeState; pinned: boolean }) {
|
|
93
|
+
const mark = markFor(node, pinned);
|
|
94
|
+
if (!mark) return null;
|
|
95
|
+
return (
|
|
96
|
+
<BarHint label={mark.label} body={mark.body} align="end" tapToOpen>
|
|
97
|
+
<span class={`node-context-mark is-${mark.tone}`} data-mark={mark.word}>
|
|
98
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
99
|
+
<path d={GLYPHS[mark.glyph]} />
|
|
100
|
+
</svg>
|
|
101
|
+
{mark.word}
|
|
102
|
+
</span>
|
|
103
|
+
</BarHint>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Pinned by an agent: the violet dot on the pin badge. */
|
|
108
|
+
export function isAgentPin(nodeId: string): boolean {
|
|
109
|
+
return contextPinMeta.value[nodeId]?.pinnedBy.actor === 'agent';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Near a pin (docs/design/NearPin.dc.html): a dotted pin-blue chip on an
|
|
114
|
+
* unpinned node the brief carries as title + short summary. Weaker than "in
|
|
115
|
+
* context" on purpose: the agent never gets this node's full content.
|
|
116
|
+
*/
|
|
117
|
+
export function NearPinMark({ node, pinned }: { node: CanvasNodeState; pinned: boolean }) {
|
|
118
|
+
// The pins' titles join this node's DOM only while the chip is hovered or
|
|
119
|
+
// focused: at rest the node must not contain other nodes' titles.
|
|
120
|
+
const [open, setOpen] = useState(false);
|
|
121
|
+
const near = pinned ? undefined : nearPins.value.get(node.id);
|
|
122
|
+
if (!near || near.length === 0) return null;
|
|
123
|
+
const word = near.length > 1 ? `near ${near.length}` : 'near';
|
|
124
|
+
// Distances change every drag frame; read them only while the hint is open.
|
|
125
|
+
const where = open
|
|
126
|
+
? near
|
|
127
|
+
.map((pin) => {
|
|
128
|
+
const pinNode = nodes.value.get(pin.pinNodeId);
|
|
129
|
+
return pinNode ? `“${pin.pinTitle}” (${Math.round(centerDistance(node, pinNode))} px)` : `“${pin.pinTitle}”`;
|
|
130
|
+
})
|
|
131
|
+
.join(', ')
|
|
132
|
+
: '';
|
|
133
|
+
return (
|
|
134
|
+
<BarHint
|
|
135
|
+
label={open ? `Near ${where}` : 'Near a pin'}
|
|
136
|
+
body="The agent gets its title and a short summary, not its full content. Pin it to send its content."
|
|
137
|
+
align="end"
|
|
138
|
+
tapToOpen
|
|
139
|
+
>
|
|
140
|
+
<span
|
|
141
|
+
class="node-near-mark"
|
|
142
|
+
data-near={near.map((pin) => pin.pinNodeId).join(' ')}
|
|
143
|
+
onMouseEnter={() => setOpen(true)}
|
|
144
|
+
onMouseLeave={() => setOpen(false)}
|
|
145
|
+
onFocusIn={() => setOpen(true)}
|
|
146
|
+
onFocusOut={() => setOpen(false)}
|
|
147
|
+
>
|
|
148
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
149
|
+
<circle cx="12" cy="12" r="8" class="node-near-ring" />
|
|
150
|
+
<circle cx="12" cy="12" r="2.2" class="node-near-dot" />
|
|
151
|
+
</svg>
|
|
152
|
+
{word}
|
|
153
|
+
</span>
|
|
154
|
+
</BarHint>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState } from 'preact/hooks';
|
|
2
2
|
import { IconChevronRight, IconUndo } from '../icons';
|
|
3
3
|
import { useNow } from './use-now';
|
|
4
|
-
import {
|
|
4
|
+
import { agentPhaseLabel } from '../../shared/agent-presence.js';
|
|
5
5
|
import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
|
|
6
6
|
import { focusNode, selectedNodeIds } from '../state/canvas-store';
|
|
7
|
-
import { activeSession, agentPresences } from '../state/presence-store';
|
|
7
|
+
import { activeSession, agentPresences, writerColor } from '../state/presence-store';
|
|
8
8
|
import {
|
|
9
9
|
type ApprovalGateView,
|
|
10
10
|
endSession,
|
|
@@ -281,12 +281,9 @@ function TimelineRow({ entry }: { entry: TimelineEntry }) {
|
|
|
281
281
|
{entry.label}
|
|
282
282
|
</span>
|
|
283
283
|
{/* With several assistants on one board, "Assistant · 23:11" answers
|
|
284
|
-
nothing — every row names its writer
|
|
284
|
+
nothing — every row names its writer. */}
|
|
285
285
|
{entry.who && (
|
|
286
|
-
<span
|
|
287
|
-
class="session-timeline-who"
|
|
288
|
-
style={{ '--identity-color': `hsl(${agentIdentityHue(entry.who)} 65% 62%)` }}
|
|
289
|
-
>
|
|
286
|
+
<span class="session-timeline-who" style={{ '--identity-color': writerColor(entry.who) }}>
|
|
290
287
|
{writerDisplay(entry.who)}
|
|
291
288
|
</span>
|
|
292
289
|
)}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { useState } from 'preact/hooks';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
import { toggleContextPin } from '../state/canvas-store';
|
|
3
|
+
import {
|
|
4
|
+
activityLensNodeIds,
|
|
5
|
+
dismissSessionReceipt,
|
|
6
|
+
type ReceiptNode,
|
|
7
|
+
type SessionContextActivity,
|
|
8
|
+
sessionReceipt,
|
|
9
|
+
setActivityLens,
|
|
10
|
+
} from '../state/session-store';
|
|
11
|
+
import { IconClose, IconPin } from '../icons';
|
|
12
|
+
import { GLYPHS } from './NodeContextMark';
|
|
13
|
+
import { workbenchFetch } from '../state/workbench-transport';
|
|
4
14
|
|
|
5
15
|
/**
|
|
6
16
|
* Session receipt (rail-chrome-v2 phase 5, design item 2): a dismissible card
|
|
7
17
|
* at the canvas region's top-right after a session ends — what the session did
|
|
8
18
|
* (items / done / vetoed), the pre-session snapshot (taken at attach, so View
|
|
9
19
|
* diff shows the session's changes and a restore undoes them), and History
|
|
10
|
-
* (the snapshots panel). Client-side state, cleared on dismiss
|
|
20
|
+
* (the snapshots panel). Client-side state, cleared on dismiss (see
|
|
21
|
+
* applySessionReceipt for which endings show at all and how a burst merges
|
|
22
|
+
* into one card).
|
|
11
23
|
*/
|
|
12
24
|
|
|
13
25
|
export interface DiffSummary {
|
|
@@ -28,17 +40,96 @@ export function summarizeDiff(diff: unknown): DiffSummary | null {
|
|
|
28
40
|
};
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
const titles = (nodes: ReceiptNode[]) => nodes.map((node) => node.title).join(', ');
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What the session did with context (docs/design/AgentContext.dc.html): an
|
|
47
|
+
* amber line for pins it read that changed since, one row per kind of touch,
|
|
48
|
+
* and the lens switch that dims every node the session did not touch.
|
|
49
|
+
*/
|
|
50
|
+
function ReceiptContext({ context, label }: { context: SessionContextActivity; label: string }) {
|
|
51
|
+
const rows: Array<{ key: keyof SessionContextActivity; name: string; glyph: keyof typeof GLYPHS | 'pin' }> = [
|
|
52
|
+
{ key: 'read', name: 'Read', glyph: 'eye' },
|
|
53
|
+
{ key: 'pinned', name: 'Pinned', glyph: 'pin' },
|
|
54
|
+
{ key: 'created', name: 'Created', glyph: 'spark' },
|
|
55
|
+
{ key: 'edited', name: 'Edited', glyph: 'pen' },
|
|
56
|
+
];
|
|
57
|
+
const present = rows.filter((row) => context[row.key].length > 0);
|
|
58
|
+
if (present.length === 0 && context.changedSinceRead.length === 0) return null;
|
|
59
|
+
const lensOn = activityLensNodeIds.value !== null;
|
|
60
|
+
return (
|
|
61
|
+
<div class="session-receipt-context" data-testid="session-receipt-context">
|
|
62
|
+
{context.changedSinceRead.length > 0 && (
|
|
63
|
+
<div class="session-receipt-warn">
|
|
64
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
65
|
+
<path d={GLYPHS.warn} />
|
|
66
|
+
</svg>
|
|
67
|
+
<span>
|
|
68
|
+
{titles(context.changedSinceRead)} changed after {label} read{' '}
|
|
69
|
+
{context.changedSinceRead.length === 1 ? 'it' : 'them'}.
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
{present.map((row) => (
|
|
74
|
+
<div key={row.key} class="session-receipt-context-row" data-row={row.key}>
|
|
75
|
+
<div class="session-receipt-context-head">
|
|
76
|
+
{row.glyph === 'pin' ? (
|
|
77
|
+
<IconPin />
|
|
78
|
+
) : (
|
|
79
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
80
|
+
<path d={GLYPHS[row.glyph]} />
|
|
81
|
+
</svg>
|
|
82
|
+
)}
|
|
83
|
+
<span class="session-receipt-context-name">{row.name}</span>
|
|
84
|
+
<span class="session-receipt-context-count">{context[row.key].length}</span>
|
|
85
|
+
{row.key === 'pinned' && (
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="session-receipt-mini"
|
|
89
|
+
onClick={() => {
|
|
90
|
+
for (const node of context.pinned) toggleContextPin(node.id);
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
Unpin
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
<div class="session-receipt-context-items">
|
|
98
|
+
{row.key === 'pinned'
|
|
99
|
+
? context.pinned.map((node) => (node.reason ? `${node.title} — “${node.reason}”` : node.title)).join('; ')
|
|
100
|
+
: titles(context[row.key])}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
role="switch"
|
|
107
|
+
aria-checked={lensOn}
|
|
108
|
+
class={`session-receipt-lens${lensOn ? ' is-on' : ''}`}
|
|
109
|
+
onClick={() => setActivityLens(!lensOn)}
|
|
110
|
+
>
|
|
111
|
+
<span class="session-receipt-lens-track" aria-hidden="true">
|
|
112
|
+
<span class="session-receipt-lens-knob" />
|
|
113
|
+
</span>
|
|
114
|
+
Dim untouched nodes
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
31
120
|
export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => void }) {
|
|
32
121
|
const receipt = sessionReceipt.value;
|
|
33
122
|
const [diff, setDiff] = useState<DiffSummary | null>(null);
|
|
34
123
|
const [loadingDiff, setLoadingDiff] = useState(false);
|
|
124
|
+
// A new or merged receipt: the old diff no longer describes it.
|
|
125
|
+
useEffect(() => setDiff(null), [receipt]);
|
|
35
126
|
if (!receipt) return null;
|
|
36
127
|
|
|
37
128
|
const viewDiff = async () => {
|
|
38
129
|
if (!receipt.snapshot) return;
|
|
39
130
|
setLoadingDiff(true);
|
|
40
131
|
try {
|
|
41
|
-
const response = await
|
|
132
|
+
const response = await workbenchFetch(`/api/canvas/snapshots/${encodeURIComponent(receipt.snapshot.id)}/diff`, {
|
|
42
133
|
headers: { 'x-pmx-workbench': '1' },
|
|
43
134
|
});
|
|
44
135
|
if (response.ok) {
|
|
@@ -60,13 +151,15 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
|
|
|
60
151
|
<div class="session-receipt-head">
|
|
61
152
|
<span class="session-receipt-dot" aria-hidden="true" />
|
|
62
153
|
<span class="session-receipt-title">
|
|
63
|
-
{receipt.
|
|
64
|
-
?
|
|
65
|
-
: receipt.endedBy === '
|
|
66
|
-
? 'Session ended
|
|
67
|
-
: receipt.endedBy === '
|
|
68
|
-
? 'Session ended
|
|
69
|
-
:
|
|
154
|
+
{receipt.sessions > 1
|
|
155
|
+
? `${receipt.sessions} sessions ended`
|
|
156
|
+
: receipt.endedBy === 'human'
|
|
157
|
+
? 'Session ended by you'
|
|
158
|
+
: receipt.endedBy === 'idle-timeout'
|
|
159
|
+
? 'Session ended — idle timeout'
|
|
160
|
+
: receipt.endedBy === 'agent'
|
|
161
|
+
? 'Session ended by the agent'
|
|
162
|
+
: 'Session ended'}
|
|
70
163
|
{endedLabel ? ` · ${endedLabel}` : ''}
|
|
71
164
|
</span>
|
|
72
165
|
<button
|
|
@@ -78,6 +171,7 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
|
|
|
78
171
|
<IconClose />
|
|
79
172
|
</button>
|
|
80
173
|
</div>
|
|
174
|
+
{receipt.sessions > 1 && <div class="session-receipt-who">{receipt.labels.join(', ')}</div>}
|
|
81
175
|
<div class="session-receipt-tiles">
|
|
82
176
|
<div class="session-receipt-tile">
|
|
83
177
|
<span class="session-receipt-tile-label">Items</span>
|
|
@@ -108,12 +202,15 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
|
|
|
108
202
|
</div>
|
|
109
203
|
)}
|
|
110
204
|
</div>
|
|
205
|
+
<ReceiptContext context={receipt.context} label={receipt.labels.join(', ')} />
|
|
111
206
|
<div class="session-receipt-note">
|
|
112
|
-
{receipt.
|
|
113
|
-
? '
|
|
114
|
-
: receipt.
|
|
115
|
-
? '
|
|
116
|
-
:
|
|
207
|
+
{receipt.sessions > 1
|
|
208
|
+
? 'History has each session’s diff and the snapshot from before it.'
|
|
209
|
+
: receipt.snapshot
|
|
210
|
+
? 'A snapshot of the board from before this session is saved — restore it to undo the session.'
|
|
211
|
+
: receipt.context.created.length === 0 && receipt.context.edited.length === 0
|
|
212
|
+
? 'The board itself did not change — nothing to restore.'
|
|
213
|
+
: 'The board was empty when the session started — nothing to restore.'}
|
|
117
214
|
</div>
|
|
118
215
|
{diff && (
|
|
119
216
|
<div class="session-receipt-diff" data-testid="session-receipt-diff">
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isHostedWorkbench } from '../state/workbench-transport';
|
|
1
2
|
import { isMac, modChord } from '../utils/platform';
|
|
2
3
|
|
|
3
4
|
interface ShortcutGroup {
|
|
@@ -74,7 +75,11 @@ export function ShortcutOverlay({ onClose }: { onClose: () => void }) {
|
|
|
74
75
|
{group.shortcuts.map((s) => (
|
|
75
76
|
<div key={s.keys} class="shortcut-row">
|
|
76
77
|
<kbd class="shortcut-keys">{s.keys}</kbd>
|
|
77
|
-
<span class="shortcut-desc">
|
|
78
|
+
<span class="shortcut-desc">
|
|
79
|
+
{isHostedWorkbench() && s.desc.startsWith('Undo / redo')
|
|
80
|
+
? 'Undo / redo — available in the local workbench only'
|
|
81
|
+
: s.desc}
|
|
82
|
+
</span>
|
|
78
83
|
</div>
|
|
79
84
|
))}
|
|
80
85
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isHostedWorkbench } from '../state/workbench-transport';
|
|
1
2
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
3
|
import { IconClose } from '../icons';
|
|
3
4
|
import {
|
|
@@ -171,7 +172,11 @@ export function SnapshotPanel({
|
|
|
171
172
|
type="button"
|
|
172
173
|
class="snapshot-action-btn snapshot-action-restore"
|
|
173
174
|
onClick={() => setConfirming({ id: snap.id, action: 'restore' })}
|
|
174
|
-
title=
|
|
175
|
+
title={
|
|
176
|
+
isHostedWorkbench()
|
|
177
|
+
? 'Restore this snapshot — replaces the current canvas'
|
|
178
|
+
: 'Restore this snapshot — replaces the current canvas (undoable)'
|
|
179
|
+
}
|
|
175
180
|
disabled={restoringId !== null}
|
|
176
181
|
>
|
|
177
182
|
{restoringId === snap.id ? 'Restoring…' : restoreLabel}
|
|
@@ -219,7 +224,11 @@ export function SnapshotPanel({
|
|
|
219
224
|
{saving ? '…' : 'Save'}
|
|
220
225
|
</button>
|
|
221
226
|
</div>
|
|
222
|
-
<div class="snapshot-restore-note">
|
|
227
|
+
<div class="snapshot-restore-note">
|
|
228
|
+
{isHostedWorkbench()
|
|
229
|
+
? 'Restoring replaces the current canvas. Save a snapshot first to keep your current state.'
|
|
230
|
+
: 'Restoring replaces the current canvas. You can undo it if needed.'}
|
|
231
|
+
</div>
|
|
223
232
|
|
|
224
233
|
<div class="snapshot-list">
|
|
225
234
|
{loading && <div class="snapshot-empty">Loading…</div>}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
|
|
2
3
|
import {
|
|
3
4
|
IconArrange,
|
|
4
5
|
IconClearTrace,
|
|
@@ -229,7 +230,7 @@ export function ToolRail({
|
|
|
229
230
|
const activeTheme = normalizeCanvasThemeName(canvasTheme.value);
|
|
230
231
|
|
|
231
232
|
const sendIntent = (type: string, payload: Record<string, unknown> = {}) => {
|
|
232
|
-
|
|
233
|
+
workbenchFetch(`/api/workbench/intent?_ts=${Date.now()}`, {
|
|
233
234
|
method: 'POST',
|
|
234
235
|
headers: { 'Content-Type': 'application/json' },
|
|
235
236
|
body: JSON.stringify({ type, payload }),
|
|
@@ -398,14 +399,16 @@ export function ToolRail({
|
|
|
398
399
|
>
|
|
399
400
|
<IconArrange />
|
|
400
401
|
</RailButton>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
402
|
+
{!isHostedWorkbench() && (
|
|
403
|
+
<RailButton
|
|
404
|
+
label={isTraceOn ? 'Disable trace' : 'Enable trace'}
|
|
405
|
+
active={isTraceOn}
|
|
406
|
+
onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
|
|
407
|
+
>
|
|
408
|
+
<IconTrace />
|
|
409
|
+
</RailButton>
|
|
410
|
+
)}
|
|
411
|
+
{!isHostedWorkbench() && (isTraceOn || traceNodeCount > 0) && (
|
|
409
412
|
<RailButton label="Clear trace" onClick={() => sendIntent('trace-clear')}>
|
|
410
413
|
<IconClearTrace />
|
|
411
414
|
</RailButton>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { exportDialogOpen } from './ExportDialog';
|
|
2
2
|
import { activeBoardId } from '../state/boards-store';
|
|
3
3
|
import { BoardSwitcher } from './BoardSwitcher';
|
|
4
|
+
import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
|
|
4
5
|
import type { ComponentChildren } from 'preact';
|
|
5
6
|
import { presenting } from '../state/presentation';
|
|
6
7
|
import { useEffect, useState } from 'preact/hooks';
|
|
@@ -24,8 +25,7 @@ import { degradedState } from './ConnectionBanner';
|
|
|
24
25
|
import { ExternalWriterIndicator } from './ExternalWriters';
|
|
25
26
|
import { useNow } from './use-now';
|
|
26
27
|
import { agentPhaseLabel } from '../../shared/agent-presence.js';
|
|
27
|
-
import {
|
|
28
|
-
import { activeSession, agentPresences, attachedSessions, contextBudget } from '../state/presence-store';
|
|
28
|
+
import { activeSession, agentPresences, attachedSessions, contextBudget, writerColor } from '../state/presence-store';
|
|
29
29
|
import { endSession, pendingGates, startSession } from '../state/session-store';
|
|
30
30
|
import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
|
|
31
31
|
|
|
@@ -56,6 +56,7 @@ function AgentChip() {
|
|
|
56
56
|
!allSessions.some(
|
|
57
57
|
(candidate) =>
|
|
58
58
|
candidate.sessionId !== session.sessionId &&
|
|
59
|
+
!candidate.parentAgentId &&
|
|
59
60
|
(candidate.sessionId === session.parentAgentId || candidate.source === session.parentAgentId),
|
|
60
61
|
),
|
|
61
62
|
);
|
|
@@ -83,12 +84,16 @@ function AgentChip() {
|
|
|
83
84
|
<span class="agent-chips">
|
|
84
85
|
{visible.map((session) => {
|
|
85
86
|
// Fleet roll-up: workers declaring this session as their parent count
|
|
86
|
-
// into its chip instead of growing the bar one chip per worker.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// into its chip instead of growing the bar one chip per worker. A
|
|
88
|
+
// worker's own chip carries no roll-up: same-source siblings would
|
|
89
|
+
// otherwise match it through `session.source`.
|
|
90
|
+
const workers = session.parentAgentId
|
|
91
|
+
? 0
|
|
92
|
+
: agentPresences.value.filter(
|
|
93
|
+
(presence) =>
|
|
94
|
+
presence.parentAgentId != null &&
|
|
95
|
+
(presence.parentAgentId === session.sessionId || presence.parentAgentId === session.source),
|
|
96
|
+
).length;
|
|
92
97
|
return (
|
|
93
98
|
<BarHint
|
|
94
99
|
key={session.sessionId}
|
|
@@ -99,7 +104,7 @@ function AgentChip() {
|
|
|
99
104
|
<span
|
|
100
105
|
class={`agent-chip phase-${session.phase}`}
|
|
101
106
|
data-phase={session.phase}
|
|
102
|
-
style={{ '--identity-color':
|
|
107
|
+
style={{ '--identity-color': writerColor(session.sessionId) }}
|
|
103
108
|
>
|
|
104
109
|
<span class="agent-chip-dot" aria-hidden="true" />
|
|
105
110
|
<span class="agent-chip-label">{agentPhaseLabel(session)}</span>
|
|
@@ -272,7 +277,7 @@ export function TopBar() {
|
|
|
272
277
|
const [workspaceName, setWorkspaceName] = useState<string>('');
|
|
273
278
|
useEffect(() => {
|
|
274
279
|
let cancelled = false;
|
|
275
|
-
|
|
280
|
+
workbenchFetch('/health')
|
|
276
281
|
.then((r) => (r.ok ? r.json() : null))
|
|
277
282
|
.then((data: { workspace?: string } | null) => {
|
|
278
283
|
if (cancelled || typeof data?.workspace !== 'string' || !data.workspace) return;
|
|
@@ -327,7 +332,7 @@ export function TopBar() {
|
|
|
327
332
|
<GateBadge />
|
|
328
333
|
<ContextBudget />
|
|
329
334
|
<ExternalWriterIndicator />
|
|
330
|
-
<StartSessionButton />
|
|
335
|
+
{!isHostedWorkbench() && <StartSessionButton />}
|
|
331
336
|
|
|
332
337
|
<div class="top-bar-sep" />
|
|
333
338
|
|