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,20 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { timeAgo } from '../utils/time-ago';
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
3
|
+
import { isSelectionEscape } from '../utils/keyboard';
|
|
3
4
|
import {
|
|
4
5
|
boardList,
|
|
5
6
|
activeBoardId,
|
|
6
7
|
createAndOpenBoard,
|
|
7
8
|
deleteBoard,
|
|
8
9
|
openBoard,
|
|
10
|
+
openMemoryTarget,
|
|
11
|
+
setBoardPinned,
|
|
9
12
|
updateBoard,
|
|
10
13
|
type BoardSummary,
|
|
11
14
|
} from '../state/boards-store';
|
|
12
|
-
import {
|
|
15
|
+
import { folderAncestors, MIGRATED_SNAPSHOT_FOLDER, normalizeBoardFolder } from '../../shared/boards.js';
|
|
13
16
|
import { askText } from './TextPrompt';
|
|
14
17
|
import { BoardFolderDialog } from './BoardFolderDialog';
|
|
15
18
|
import { requestJson } from '../state/intent-bridge';
|
|
16
|
-
import {
|
|
19
|
+
import { approxTokens } from '../state/context-chip-store';
|
|
17
20
|
import { BoardCopyDialog } from './BoardCopyDialog';
|
|
21
|
+
import { BoardPinButton } from './BoardPinButton';
|
|
22
|
+
import { HomeMapView } from './HomeMapView';
|
|
23
|
+
import { HomeGraphView } from './HomeGraphView';
|
|
24
|
+
import { HomeReviewView } from './HomeReviewView';
|
|
25
|
+
import { KnowledgeDialog } from './KnowledgeDialog';
|
|
26
|
+
import { RelationPresetsDialog } from './RelationPresetsDialog';
|
|
27
|
+
import { knowledge, lifecycleFilter } from '../state/knowledge-store';
|
|
28
|
+
import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
|
|
29
|
+
import { lifecycleUiGeneration } from '../state/boards-store';
|
|
30
|
+
import type { LifecycleQualification } from '../../shared/lifecycle-status.js';
|
|
31
|
+
import { IconChevronDown, IconChevronRight, IconMore, IconPin, IconSearch } from '../icons';
|
|
18
32
|
|
|
19
33
|
interface LibraryHit {
|
|
20
34
|
boardId: string;
|
|
@@ -22,32 +36,32 @@ interface LibraryHit {
|
|
|
22
36
|
cardId: string;
|
|
23
37
|
title: string | null;
|
|
24
38
|
snippet: string;
|
|
39
|
+
qualification?: LifecycleQualification;
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
function
|
|
42
|
+
function openedAgo(iso: string | null): string {
|
|
28
43
|
if (!iso) return 'never opened';
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (hrs < 24) return `${hrs}h ago`;
|
|
34
|
-
const days = Math.floor(hrs / 24);
|
|
35
|
-
if (days < 30) return `${days}d ago`;
|
|
36
|
-
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
44
|
+
const days = (Date.now() - new Date(iso).getTime()) / 86_400_000;
|
|
45
|
+
if (days >= 30)
|
|
46
|
+
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
47
|
+
return timeAgo(iso);
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
51
|
+
|
|
52
|
+
/** Board-to-board links in either direction: the "↔ N links" a card shows. */
|
|
53
|
+
function linkCount(board: BoardSummary): number {
|
|
54
|
+
return (board.links?.length ?? 0) + (board.backlinks?.length ?? 0);
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
export async function promptNewBoard(): Promise<void> {
|
|
57
|
+
export async function promptNewBoard(folder?: string | null): Promise<void> {
|
|
44
58
|
const name = await askText('New board', 'Board name', { confirm: 'Create' });
|
|
45
|
-
if (name) await createAndOpenBoard(name);
|
|
59
|
+
if (name) await createAndOpenBoard(name, folder);
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
const COLLAPSED_KEY = 'pmx-canvas-home-collapsed';
|
|
49
63
|
|
|
50
|
-
/** Which
|
|
64
|
+
/** Which folders this viewer folded away in the tree; the migrated-snapshot shelf starts folded. */
|
|
51
65
|
function readCollapsed(): Set<string> {
|
|
52
66
|
try {
|
|
53
67
|
const raw = localStorage.getItem(COLLAPSED_KEY);
|
|
@@ -55,7 +69,7 @@ function readCollapsed(): Set<string> {
|
|
|
55
69
|
} catch {
|
|
56
70
|
// Storage can be unavailable (private windows, embedded panes): fall back to the default.
|
|
57
71
|
}
|
|
58
|
-
return new Set([
|
|
72
|
+
return new Set([MIGRATED_SNAPSHOT_FOLDER]);
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
function writeCollapsed(collapsed: Set<string>): void {
|
|
@@ -66,30 +80,117 @@ function writeCollapsed(collapsed: Set<string>): void {
|
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
83
|
+
const inFolder = (board: BoardSummary, path: string) => board.folder === path || !!board.folder?.startsWith(`${path}/`);
|
|
84
|
+
|
|
85
|
+
const leaf = (path: string) => path.split('/').at(-1) ?? path;
|
|
86
|
+
|
|
87
|
+
/** A board's layout in miniature: its cards as rectangles, pinned ones in pin blue. */
|
|
88
|
+
function BoardThumbnail({ board }: { board: BoardSummary }) {
|
|
89
|
+
return (
|
|
90
|
+
<div class="home-thumb" aria-hidden="true">
|
|
91
|
+
<div class="home-thumb-inner">
|
|
92
|
+
{(board.preview ?? []).map((rect, index) => (
|
|
93
|
+
<span
|
|
94
|
+
key={index}
|
|
95
|
+
class={`home-thumb-card${rect.pinned ? ' is-pinned' : ''}`}
|
|
96
|
+
style={{
|
|
97
|
+
left: `${rect.x * 100}%`,
|
|
98
|
+
top: `${rect.y * 100}%`,
|
|
99
|
+
width: `${rect.w * 100}%`,
|
|
100
|
+
height: `${rect.h * 100}%`,
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
69
109
|
/**
|
|
70
|
-
* Home (plan
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
110
|
+
* Home (docs/design/Home.dc.html, plan 016): the library when no board is
|
|
111
|
+
* open. A folder tree with pinned boards on the left, the selected folder's
|
|
112
|
+
* subfolders and board cards in the middle, the selected board's details on
|
|
113
|
+
* the right. At ≤ 760 px (Pane600.dc.html) the tree becomes a folder picker,
|
|
114
|
+
* the boards a list and the details a bottom sheet. Folders are board
|
|
115
|
+
* folder paths; folders carry no colour (decided 2026-10-07). Browser
|
|
116
|
+
* confirm dialogs are no-ops in agent panes, so Delete asks in place.
|
|
75
117
|
*/
|
|
76
118
|
export function HomeView() {
|
|
77
119
|
const boards = boardList.value;
|
|
78
|
-
const
|
|
120
|
+
const enabled = knowledgeLifecycleEnabled.value;
|
|
121
|
+
const [selectedView, setView] = useState<'Folders' | 'Map' | 'Graph' | 'Review'>('Folders');
|
|
122
|
+
const view = !enabled && selectedView === 'Review' ? 'Folders' : selectedView;
|
|
123
|
+
const [folder, setFolder] = useState<string>(() => {
|
|
124
|
+
const recent = [...boardList.value].sort((a, b) => (b.lastOpenedAt ?? '').localeCompare(a.lastOpenedAt ?? ''))[0];
|
|
125
|
+
return recent?.folder ?? '';
|
|
126
|
+
});
|
|
127
|
+
const [extraFolders, setExtraFolders] = useState<string[]>([]);
|
|
128
|
+
const [knowledgeDialog, setKnowledgeDialog] = useState<string | null>(null);
|
|
129
|
+
const [relationPresetsOpen, setRelationPresetsOpen] = useState(false);
|
|
130
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
131
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
132
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
79
133
|
const [moving, setMoving] = useState<string | null>(null);
|
|
80
134
|
const [copying, setCopying] = useState<string | null>(null);
|
|
81
135
|
const [search, setSearch] = useState('');
|
|
82
136
|
const [hits, setHits] = useState<LibraryHit[]>([]);
|
|
83
137
|
const [collapsed, setCollapsed] = useState<Set<string>>(readCollapsed);
|
|
84
|
-
const
|
|
138
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
139
|
+
const [pinsOpen, setPinsOpen] = useState(false);
|
|
140
|
+
const moreButton = useRef<HTMLButtonElement>(null);
|
|
141
|
+
const moreMenu = useRef<HTMLSpanElement>(null);
|
|
142
|
+
|
|
85
143
|
const folders = [
|
|
86
|
-
...new Set(
|
|
144
|
+
...new Set([
|
|
145
|
+
...boards.flatMap((board) => (board.folder ? folderAncestors(board.folder) : [])),
|
|
146
|
+
...(knowledge.value?.folders ?? []).flatMap((item) => folderAncestors(item.path)),
|
|
147
|
+
...extraFolders.flatMap(folderAncestors),
|
|
148
|
+
]),
|
|
87
149
|
].sort((a, b) => a.localeCompare(b));
|
|
150
|
+
const childrenOf = (path: string) =>
|
|
151
|
+
folders.filter((candidate) =>
|
|
152
|
+
path
|
|
153
|
+
? candidate.startsWith(`${path}/`) && !candidate.slice(path.length + 1).includes('/')
|
|
154
|
+
: !candidate.includes('/'),
|
|
155
|
+
);
|
|
88
156
|
const query = search.trim().toLocaleLowerCase();
|
|
89
|
-
const
|
|
90
|
-
const
|
|
157
|
+
const pinned = boards.filter((board) => board.pin);
|
|
158
|
+
const pinnedChars = pinned.reduce((sum, board) => sum + (board.mapChars ?? 0), 0);
|
|
159
|
+
const subfolders = childrenOf(folder);
|
|
160
|
+
const here = boards
|
|
161
|
+
.filter((board) => (board.folder ?? '') === folder)
|
|
162
|
+
.sort((a, b) => (b.lastOpenedAt ?? '').localeCompare(a.lastOpenedAt ?? ''));
|
|
163
|
+
const matches = query
|
|
164
|
+
? boards.filter((board) => `${board.name} ${board.folder ?? ''}`.toLocaleLowerCase().includes(query))
|
|
165
|
+
: [];
|
|
166
|
+
const selected = boards.find((board) => board.id === selectedId) ?? null;
|
|
91
167
|
const movingBoard = boards.find((board) => board.id === moving);
|
|
92
168
|
const copyingBoard = boards.find((board) => board.id === copying);
|
|
169
|
+
const reviewBoardIds = new Set(boards.filter((board) => !folder || inFolder(board, folder)).map((board) => board.id));
|
|
170
|
+
const reviewCards = Object.entries(knowledge.value?.cards ?? {})
|
|
171
|
+
.filter(([key]) => reviewBoardIds.has((JSON.parse(key) as [string, string])[0]))
|
|
172
|
+
.map(([, status]) => status);
|
|
173
|
+
const reviewNeeded = reviewCards.filter((status) => !status.retired && (status.due || status.sources.length)).length;
|
|
174
|
+
const withoutPolicy = reviewCards.filter((status) => status.unknown).length;
|
|
175
|
+
const folderCategory = knowledge.value?.resolvedFolders[folder]?.category;
|
|
176
|
+
const reviewTargets = new Map<string, string[]>();
|
|
177
|
+
for (const [key, status] of Object.entries(knowledge.value?.cards ?? {})) {
|
|
178
|
+
if (status.retired || !(status.due || status.sources.length)) continue;
|
|
179
|
+
const [boardId, nodeId] = JSON.parse(key) as [string, string];
|
|
180
|
+
const targets = reviewTargets.get(boardId) ?? [];
|
|
181
|
+
targets.push(nodeId);
|
|
182
|
+
reviewTargets.set(boardId, targets);
|
|
183
|
+
}
|
|
184
|
+
const shown = query ? matches : here;
|
|
185
|
+
const reviewBoard = async (id: string) => {
|
|
186
|
+
if (!knowledgeLifecycleEnabled.value) return;
|
|
187
|
+
const generation = lifecycleUiGeneration;
|
|
188
|
+
const cardId = reviewTargets.get(id)?.[0];
|
|
189
|
+
if (!cardId) return;
|
|
190
|
+
const opened = await openMemoryTarget({ boardId: id, cardId });
|
|
191
|
+
if (opened && knowledgeLifecycleEnabled.value && generation === lifecycleUiGeneration && activeBoardId.value === id)
|
|
192
|
+
lifecycleFilter.value = 'To review';
|
|
193
|
+
};
|
|
93
194
|
|
|
94
195
|
useEffect(() => {
|
|
95
196
|
const q = search.trim();
|
|
@@ -113,165 +214,591 @@ export function HomeView() {
|
|
|
113
214
|
};
|
|
114
215
|
}, [search]);
|
|
115
216
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (sequence !== openHitSequence.current || activeBoardId.value !== hit.boardId) return;
|
|
121
|
-
if (nodes.value.has(hit.cardId)) {
|
|
122
|
-
focusNode(hit.cardId);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
await new Promise((resolve) => window.setTimeout(resolve, 25));
|
|
126
|
-
}
|
|
127
|
-
};
|
|
217
|
+
// The ⋯ menu takes focus on its first item when it opens.
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (menuOpen) moreMenu.current?.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
|
|
220
|
+
}, [menuOpen]);
|
|
128
221
|
|
|
129
|
-
|
|
222
|
+
// A selection that left this folder (moved, deleted) closes the details.
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (selectedId && !boards.some((board) => board.id === selectedId)) setSelectedId(null);
|
|
225
|
+
}, [boards, selectedId]);
|
|
226
|
+
|
|
227
|
+
useLayoutEffect(() => {
|
|
228
|
+
if (view !== 'Folders' || !selectedId) return;
|
|
229
|
+
const onKey = (event: KeyboardEvent) => {
|
|
230
|
+
if (!isSelectionEscape(event)) return;
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
setSelectedId(null);
|
|
233
|
+
setConfirmingDelete(false);
|
|
234
|
+
};
|
|
235
|
+
window.addEventListener('keydown', onKey);
|
|
236
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
237
|
+
}, [view, selectedId]);
|
|
238
|
+
|
|
239
|
+
const openHit = (hit: LibraryHit) => openMemoryTarget(hit);
|
|
240
|
+
|
|
241
|
+
const toggle = (path: string) => {
|
|
130
242
|
const next = new Set(collapsed);
|
|
131
|
-
if (next.has(
|
|
132
|
-
else next.add(
|
|
243
|
+
if (next.has(path)) next.delete(path);
|
|
244
|
+
else next.add(path);
|
|
133
245
|
setCollapsed(next);
|
|
134
246
|
writeCollapsed(next);
|
|
135
247
|
};
|
|
136
248
|
|
|
249
|
+
const goTo = (path: string) => {
|
|
250
|
+
setFolder(path);
|
|
251
|
+
setSearch('');
|
|
252
|
+
setSelectedId(null);
|
|
253
|
+
setPickerOpen(false);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const select = (id: string | null) => {
|
|
257
|
+
setSelectedId(id);
|
|
258
|
+
setMenuOpen(false);
|
|
259
|
+
setConfirmingDelete(false);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const newFolder = async () => {
|
|
263
|
+
const name = await askText('New folder', 'Folder name', { confirm: 'Create' });
|
|
264
|
+
if (!name) return;
|
|
265
|
+
let path: string | null;
|
|
266
|
+
try {
|
|
267
|
+
path = normalizeBoardFolder(folder ? `${folder}/${name}` : name);
|
|
268
|
+
} catch {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (!path) return;
|
|
272
|
+
// A folder exists once a board is filed in it; until then it lives in this view.
|
|
273
|
+
setExtraFolders((list) => [...list, path]);
|
|
274
|
+
goTo(path);
|
|
275
|
+
};
|
|
276
|
+
|
|
137
277
|
const rename = async (board: BoardSummary) => {
|
|
138
278
|
const name = await askText('Rename board', 'Board name', { initial: board.name, confirm: 'Rename' });
|
|
139
279
|
if (name && name !== board.name) await updateBoard(board.id, { name });
|
|
140
280
|
};
|
|
141
281
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
<div class=
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
282
|
+
const treeRow = (path: string, depth: number) => {
|
|
283
|
+
const kids = childrenOf(path);
|
|
284
|
+
const open = !collapsed.has(path);
|
|
285
|
+
const count = boards.filter((board) => inFolder(board, path)).length;
|
|
286
|
+
const hasPin = boards.some((board) => board.pin && inFolder(board, path));
|
|
287
|
+
return (
|
|
288
|
+
<li key={path}>
|
|
289
|
+
<div class={`home-tree-row${folder === path && !query ? ' is-selected' : ''}`} data-folder={path}>
|
|
290
|
+
<span class="home-tree-indent" style={{ width: `${depth * 16}px` }} />
|
|
291
|
+
{kids.length > 0 ? (
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
class="home-tree-chevron"
|
|
295
|
+
aria-label={open ? `Fold ${leaf(path)}` : `Unfold ${leaf(path)}`}
|
|
296
|
+
aria-expanded={open}
|
|
297
|
+
onClick={() => toggle(path)}
|
|
298
|
+
>
|
|
299
|
+
{open ? <IconChevronDown /> : <IconChevronRight />}
|
|
300
|
+
</button>
|
|
301
|
+
) : (
|
|
302
|
+
<span class="home-tree-chevron" />
|
|
303
|
+
)}
|
|
304
|
+
<button type="button" class="home-tree-name" onClick={() => goTo(path)}>
|
|
305
|
+
{leaf(path)}
|
|
162
306
|
</button>
|
|
307
|
+
{hasPin && <span class="home-tree-pin" aria-label="Has a board in the agent's context" />}
|
|
308
|
+
<span class="home-tree-count">{count}</span>
|
|
163
309
|
</div>
|
|
310
|
+
{open && kids.length > 0 && <ul class="home-tree">{kids.map((kid) => treeRow(kid, depth + 1))}</ul>}
|
|
164
311
|
</li>
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<button type="button" class="home-
|
|
175
|
-
|
|
176
|
-
</button>
|
|
177
|
-
<button type="button" class="home-board-action" aria-haspopup="dialog" onClick={() => setCopying(board.id)}>
|
|
178
|
-
Create from…
|
|
179
|
-
</button>
|
|
180
|
-
<button type="button" class="home-board-action" onClick={() => void rename(board)}>
|
|
181
|
-
Rename
|
|
182
|
-
</button>
|
|
183
|
-
<button type="button" class="home-board-action" onClick={() => setConfirmingDelete(board.id)}>
|
|
184
|
-
Delete
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const tree = (
|
|
316
|
+
<ul class="home-tree" aria-label="Folders">
|
|
317
|
+
{childrenOf('').map((path) => treeRow(path, 0))}
|
|
318
|
+
<li>
|
|
319
|
+
<div class={`home-tree-row is-unfiled${folder === '' && !query ? ' is-selected' : ''}`}>
|
|
320
|
+
<span class="home-tree-chevron" />
|
|
321
|
+
<button type="button" class="home-tree-name" onClick={() => goTo('')}>
|
|
322
|
+
Unfiled
|
|
185
323
|
</button>
|
|
324
|
+
<span class="home-tree-count">{boards.filter((board) => !board.folder).length}</span>
|
|
186
325
|
</div>
|
|
187
326
|
</li>
|
|
188
|
-
|
|
327
|
+
</ul>
|
|
328
|
+
);
|
|
189
329
|
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
330
|
+
const pinnedList = (
|
|
331
|
+
<ul class="home-pinned" aria-label="Pinned boards">
|
|
332
|
+
{pinned.map((board) => (
|
|
333
|
+
<li key={board.id}>
|
|
334
|
+
<button type="button" class="home-pinned-row" onClick={() => goTo(board.folder ?? '')}>
|
|
335
|
+
<span class="home-pin-dot" aria-hidden="true">
|
|
336
|
+
<IconPin />
|
|
337
|
+
</span>
|
|
338
|
+
<span class="home-pinned-name">{board.name}</span>
|
|
339
|
+
<span class="home-tree-count">{board.folder ? leaf(board.folder) : 'Unfiled'}</span>
|
|
340
|
+
</button>
|
|
341
|
+
</li>
|
|
342
|
+
))}
|
|
343
|
+
</ul>
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const searchField = (
|
|
347
|
+
<label class="home-search">
|
|
348
|
+
<IconSearch />
|
|
349
|
+
<span class="sr-only">Find boards, cards and folders</span>
|
|
350
|
+
<input
|
|
351
|
+
type="search"
|
|
352
|
+
placeholder="Boards, cards, folders…"
|
|
353
|
+
value={search}
|
|
354
|
+
onInput={(event) => setSearch(event.currentTarget.value)}
|
|
355
|
+
/>
|
|
356
|
+
</label>
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const card = (board: BoardSummary) => (
|
|
360
|
+
<li key={board.id}>
|
|
361
|
+
<article
|
|
362
|
+
class={`home-card${board.pin ? ' is-pinned' : ''}${selectedId === board.id ? ' is-selected' : ''}`}
|
|
363
|
+
data-testid="home-board"
|
|
364
|
+
data-board-id={board.id}
|
|
365
|
+
>
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
class="home-card-select"
|
|
369
|
+
aria-label={board.name}
|
|
370
|
+
aria-pressed={selectedId === board.id}
|
|
371
|
+
onClick={() => select(board.id)}
|
|
372
|
+
onDblClick={() => void openBoard(board.id)}
|
|
373
|
+
>
|
|
374
|
+
<BoardThumbnail board={board} />
|
|
375
|
+
<span class="home-card-body">
|
|
376
|
+
<span class="home-card-name">{board.name}</span>
|
|
377
|
+
<span class="home-card-readme">{board.summary ?? 'No README yet.'}</span>
|
|
378
|
+
<span class="home-card-meta">
|
|
379
|
+
<span>{plural(board.nodeCount, 'card')}</span>
|
|
380
|
+
{linkCount(board) > 0 && <span>↔ {plural(linkCount(board), 'link')}</span>}
|
|
381
|
+
<span class="home-card-when">{openedAgo(board.lastOpenedAt)}</span>
|
|
382
|
+
</span>
|
|
383
|
+
</span>
|
|
208
384
|
</button>
|
|
209
|
-
{
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)}
|
|
217
|
-
</
|
|
385
|
+
{enabled && reviewTargets.has(board.id) && (
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
class="home-card-review"
|
|
389
|
+
aria-label={`Review ${board.name}`}
|
|
390
|
+
onClick={() => void reviewBoard(board.id)}
|
|
391
|
+
>
|
|
392
|
+
⧗ Review {plural(reviewTargets.get(board.id)!.length, 'card')}
|
|
393
|
+
</button>
|
|
218
394
|
)}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
395
|
+
<BoardPinButton board={board} />
|
|
396
|
+
</article>
|
|
397
|
+
</li>
|
|
398
|
+
);
|
|
222
399
|
|
|
223
|
-
|
|
224
|
-
<
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
400
|
+
const details = selected && (
|
|
401
|
+
<aside class="home-details" aria-label="Board details" data-testid="home-details">
|
|
402
|
+
<button type="button" class="home-details-grip" aria-label="Close details" onClick={() => select(null)} />
|
|
403
|
+
<div class="home-details-head">
|
|
404
|
+
<span class="home-details-folder">{selected.folder ? leaf(selected.folder) : 'Unfiled'}</span>
|
|
405
|
+
<span class="home-details-name">{selected.name}</span>
|
|
406
|
+
</div>
|
|
407
|
+
<div class={`home-details-context${selected.pin ? ' is-on' : ''}`}>
|
|
408
|
+
<span class="home-details-context-text">
|
|
409
|
+
<span class="home-details-context-label">
|
|
410
|
+
{selected.pin ? "In the agent's context" : "Not in the agent's context"}
|
|
411
|
+
</span>
|
|
412
|
+
</span>
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
role="switch"
|
|
416
|
+
aria-checked={!!selected.pin}
|
|
417
|
+
aria-label="In the agent's context"
|
|
418
|
+
class="home-switch"
|
|
419
|
+
onClick={() => void setBoardPinned(selected.id, !selected.pin)}
|
|
420
|
+
>
|
|
421
|
+
<span class="home-switch-knob" />
|
|
422
|
+
</button>
|
|
423
|
+
</div>
|
|
424
|
+
{confirmingDelete ? (
|
|
425
|
+
<div class="home-details-confirm" role="alert">
|
|
426
|
+
<span>
|
|
427
|
+
Delete <strong>{selected.name}</strong> and its {plural(selected.nodeCount, 'card')} and snapshots? This
|
|
428
|
+
can't be undone.
|
|
429
|
+
</span>
|
|
430
|
+
<span class="home-details-actions">
|
|
431
|
+
<button type="button" class="home-button" onClick={() => setConfirmingDelete(false)}>
|
|
432
|
+
Cancel
|
|
433
|
+
</button>
|
|
434
|
+
<button
|
|
435
|
+
type="button"
|
|
436
|
+
class="home-button is-danger"
|
|
437
|
+
onClick={() => {
|
|
438
|
+
setConfirmingDelete(false);
|
|
439
|
+
void deleteBoard(selected.id);
|
|
440
|
+
}}
|
|
441
|
+
>
|
|
442
|
+
Delete board
|
|
443
|
+
</button>
|
|
444
|
+
</span>
|
|
445
|
+
</div>
|
|
446
|
+
) : (
|
|
447
|
+
<div class="home-details-actions">
|
|
448
|
+
<button type="button" class="home-button is-primary" onClick={() => void openBoard(selected.id)}>
|
|
449
|
+
Open board
|
|
230
450
|
</button>
|
|
451
|
+
{enabled && reviewTargets.has(selected.id) && (
|
|
452
|
+
<button type="button" class="home-button" onClick={() => void reviewBoard(selected.id)}>
|
|
453
|
+
Review cards
|
|
454
|
+
</button>
|
|
455
|
+
)}
|
|
456
|
+
<span class="home-more">
|
|
457
|
+
<button
|
|
458
|
+
ref={moreButton}
|
|
459
|
+
type="button"
|
|
460
|
+
class="home-button is-icon"
|
|
461
|
+
aria-label="More: move, rename, create from, delete"
|
|
462
|
+
aria-haspopup="menu"
|
|
463
|
+
aria-expanded={menuOpen}
|
|
464
|
+
onClick={() => setMenuOpen(!menuOpen)}
|
|
465
|
+
>
|
|
466
|
+
<IconMore />
|
|
467
|
+
</button>
|
|
468
|
+
{menuOpen && (
|
|
469
|
+
<span
|
|
470
|
+
ref={moreMenu}
|
|
471
|
+
class="home-more-menu"
|
|
472
|
+
role="menu"
|
|
473
|
+
onKeyDown={(event) => {
|
|
474
|
+
const items = [...(moreMenu.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])];
|
|
475
|
+
const at = items.indexOf(document.activeElement as HTMLElement);
|
|
476
|
+
if (event.key === 'Escape') {
|
|
477
|
+
event.preventDefault();
|
|
478
|
+
setMenuOpen(false);
|
|
479
|
+
moreButton.current?.focus();
|
|
480
|
+
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
481
|
+
event.preventDefault();
|
|
482
|
+
const step = event.key === 'ArrowDown' ? 1 : -1;
|
|
483
|
+
items[(at + step + items.length) % items.length]?.focus();
|
|
484
|
+
}
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
{(
|
|
488
|
+
[
|
|
489
|
+
['Move…', () => setMoving(selected.id)],
|
|
490
|
+
['Rename…', () => void rename(selected)],
|
|
491
|
+
['Create from…', () => setCopying(selected.id)],
|
|
492
|
+
['Delete…', () => setConfirmingDelete(true)],
|
|
493
|
+
] as const
|
|
494
|
+
).map(([label, run]) => (
|
|
495
|
+
<button
|
|
496
|
+
key={label}
|
|
497
|
+
type="button"
|
|
498
|
+
role="menuitem"
|
|
499
|
+
class="home-more-item"
|
|
500
|
+
onClick={() => {
|
|
501
|
+
setMenuOpen(false);
|
|
502
|
+
run();
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
{label}
|
|
506
|
+
</button>
|
|
507
|
+
))}
|
|
508
|
+
</span>
|
|
509
|
+
)}
|
|
510
|
+
</span>
|
|
231
511
|
</div>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
512
|
+
)}
|
|
513
|
+
</aside>
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div class="home-shell">
|
|
518
|
+
{knowledgeDialog !== null && (
|
|
519
|
+
<KnowledgeDialog
|
|
520
|
+
key={knowledgeDialog}
|
|
521
|
+
folder={knowledgeDialog || undefined}
|
|
522
|
+
onClose={() => setKnowledgeDialog(null)}
|
|
239
523
|
/>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
524
|
+
)}
|
|
525
|
+
{relationPresetsOpen && <RelationPresetsDialog onClose={() => setRelationPresetsOpen(false)} />}
|
|
526
|
+
<nav class="home-view-tabs" aria-label="Library view">
|
|
527
|
+
{(['Folders', 'Map', 'Graph', 'Review'] as const)
|
|
528
|
+
.filter((tab) => enabled || tab !== 'Review')
|
|
529
|
+
.map((tab) => (
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
class="home-button"
|
|
533
|
+
aria-pressed={view === tab}
|
|
534
|
+
onClick={() => setView(tab)}
|
|
535
|
+
key={tab}
|
|
536
|
+
>
|
|
537
|
+
{tab}
|
|
538
|
+
</button>
|
|
539
|
+
))}
|
|
540
|
+
<button type="button" class="home-button" onClick={() => setRelationPresetsOpen(true)}>
|
|
541
|
+
Relation presets
|
|
542
|
+
</button>
|
|
543
|
+
</nav>
|
|
544
|
+
{view === 'Map' || view === 'Graph' ? (
|
|
545
|
+
view === 'Map' ? (
|
|
546
|
+
<HomeMapView />
|
|
259
547
|
) : (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
548
|
+
<HomeGraphView />
|
|
549
|
+
)
|
|
550
|
+
) : (
|
|
551
|
+
<div class={`home-view${selected && view === 'Folders' ? ' has-details' : ''}`} data-testid="home-view">
|
|
552
|
+
<aside class={`home-library${pickerOpen ? ' is-open' : ''}`} aria-label="Library">
|
|
553
|
+
<div class="home-library-head">
|
|
554
|
+
<span class="home-library-title">Library</span>
|
|
555
|
+
<span class="home-tree-count">{plural(boards.length, 'board')}</span>
|
|
556
|
+
{enabled && (
|
|
557
|
+
<button
|
|
558
|
+
type="button"
|
|
559
|
+
class="home-button"
|
|
560
|
+
onClick={() => {
|
|
561
|
+
if (knowledgeLifecycleEnabled.value) setKnowledgeDialog('');
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
Categories
|
|
565
|
+
</button>
|
|
566
|
+
)}
|
|
567
|
+
</div>
|
|
568
|
+
{searchField}
|
|
569
|
+
{pinned.length > 0 && (
|
|
570
|
+
<section class="home-library-section">
|
|
571
|
+
<span class="home-section-label">Pinned boards</span>
|
|
572
|
+
{pinnedList}
|
|
573
|
+
<span class="home-pinned-cost">
|
|
574
|
+
≈ {approxTokens(pinnedChars)} tokens · {plural(pinned.length, 'board')}
|
|
575
|
+
</span>
|
|
266
576
|
</section>
|
|
267
577
|
)}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
578
|
+
<section class="home-library-section is-folders">
|
|
579
|
+
<span class="home-section-label">Folders</span>
|
|
580
|
+
{tree}
|
|
581
|
+
<button type="button" class="home-tree-new" onClick={() => void newFolder()}>
|
|
582
|
+
+ New folder
|
|
583
|
+
</button>
|
|
584
|
+
</section>
|
|
585
|
+
</aside>
|
|
586
|
+
|
|
587
|
+
<main class="home-main">
|
|
588
|
+
{/* Pane600.dc.html: at the narrow end the tree and the pins fold into these two rows. */}
|
|
589
|
+
<div class="home-narrow">
|
|
590
|
+
{searchField}
|
|
591
|
+
{pinned.length > 0 && (
|
|
592
|
+
<>
|
|
593
|
+
<button
|
|
594
|
+
type="button"
|
|
595
|
+
class="home-narrow-context"
|
|
596
|
+
aria-expanded={pinsOpen}
|
|
597
|
+
onClick={() => setPinsOpen(!pinsOpen)}
|
|
598
|
+
>
|
|
599
|
+
<span class="home-pin-dot" aria-hidden="true">
|
|
600
|
+
<IconPin />
|
|
601
|
+
</span>
|
|
602
|
+
<span class="home-narrow-context-label">In context: {plural(pinned.length, 'board')}</span>
|
|
603
|
+
<span class="home-narrow-context-cost">≈ {approxTokens(pinnedChars)} tokens</span>
|
|
604
|
+
<span>{pinsOpen ? 'Hide ▴' : 'Show ▾'}</span>
|
|
605
|
+
</button>
|
|
606
|
+
{pinsOpen && pinnedList}
|
|
607
|
+
</>
|
|
608
|
+
)}
|
|
609
|
+
<button
|
|
610
|
+
type="button"
|
|
611
|
+
class="home-narrow-picker"
|
|
612
|
+
aria-expanded={pickerOpen}
|
|
613
|
+
onClick={() => setPickerOpen(!pickerOpen)}
|
|
614
|
+
>
|
|
615
|
+
{folder ? leaf(folder) : 'Library'}
|
|
616
|
+
<span class="home-tree-count">
|
|
617
|
+
· {folder ? boards.filter((board) => inFolder(board, folder)).length : here.length} ▾
|
|
618
|
+
</span>
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
{view === 'Review' ? (
|
|
623
|
+
<HomeReviewView folder={folder} folders={folders} onScopeChange={goTo} />
|
|
624
|
+
) : (
|
|
625
|
+
<>
|
|
626
|
+
<div class="home-main-head">
|
|
627
|
+
<div class="home-main-title">
|
|
628
|
+
<nav class="home-crumbs" aria-label="Folder path">
|
|
629
|
+
<button type="button" onClick={() => goTo('')}>
|
|
630
|
+
Library
|
|
631
|
+
</button>
|
|
632
|
+
{folder &&
|
|
633
|
+
folderAncestors(folder).map((path) => (
|
|
634
|
+
<span key={path}>
|
|
635
|
+
<span class="home-crumb-sep">/</span>
|
|
636
|
+
<button type="button" onClick={() => goTo(path)}>
|
|
637
|
+
{leaf(path)}
|
|
638
|
+
</button>
|
|
639
|
+
</span>
|
|
640
|
+
))}
|
|
641
|
+
</nav>
|
|
642
|
+
<h1 class="home-view-title">
|
|
643
|
+
{query ? `Results for “${search.trim()}”` : folder ? leaf(folder) : 'Library'}
|
|
644
|
+
</h1>
|
|
645
|
+
</div>
|
|
646
|
+
{enabled && (
|
|
647
|
+
<button
|
|
648
|
+
type="button"
|
|
649
|
+
class="home-button"
|
|
650
|
+
onClick={() => {
|
|
651
|
+
if (knowledgeLifecycleEnabled.value) setKnowledgeDialog(folder);
|
|
652
|
+
}}
|
|
653
|
+
>
|
|
654
|
+
{' '}
|
|
655
|
+
{folder ? 'Folder settings' : 'Categories'}
|
|
656
|
+
</button>
|
|
657
|
+
)}
|
|
658
|
+
<button
|
|
659
|
+
type="button"
|
|
660
|
+
class="home-button is-primary"
|
|
661
|
+
onClick={() => void promptNewBoard(folder || null)}
|
|
662
|
+
>
|
|
663
|
+
{folder ? 'New board here' : 'New board'}
|
|
664
|
+
</button>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
{enabled && knowledge.value && !query && (
|
|
668
|
+
<section class="home-main-section" aria-label="Folder review summary">
|
|
669
|
+
<span class="home-section-label">
|
|
670
|
+
Card review · {folder ? 'In this folder and subfolders' : 'Across the library'}
|
|
671
|
+
</span>
|
|
672
|
+
<div class="home-card-meta">
|
|
673
|
+
{folderCategory && (
|
|
674
|
+
<span>
|
|
675
|
+
Category: {folderCategory.glyph} {folderCategory.name}
|
|
676
|
+
</span>
|
|
677
|
+
)}
|
|
678
|
+
<button
|
|
679
|
+
type="button"
|
|
680
|
+
class={`home-review-link${reviewNeeded ? ' has-review' : ''}`}
|
|
681
|
+
onClick={() => {
|
|
682
|
+
setView('Review');
|
|
683
|
+
setSelectedId(null);
|
|
684
|
+
}}
|
|
685
|
+
>
|
|
686
|
+
Review needed: {plural(reviewNeeded, 'card')}
|
|
687
|
+
</button>
|
|
688
|
+
<span>No review policy: {plural(withoutPolicy, 'card')}</span>
|
|
689
|
+
</div>
|
|
690
|
+
</section>
|
|
691
|
+
)}
|
|
692
|
+
|
|
693
|
+
{boards.length === 0 ? (
|
|
694
|
+
<div class="home-view-empty">
|
|
695
|
+
No boards yet. Create one, or let an agent start writing — it opens one.
|
|
696
|
+
</div>
|
|
697
|
+
) : query ? (
|
|
698
|
+
<>
|
|
699
|
+
{hits.length > 0 && (
|
|
700
|
+
<section class="home-main-section">
|
|
701
|
+
<span class="home-section-label">Cards · {hits.length}</span>
|
|
702
|
+
<ul class="home-hits" aria-label="Library search results">
|
|
703
|
+
{hits.map((hit) => (
|
|
704
|
+
<li key={`${hit.boardId}:${hit.cardId}`}>
|
|
705
|
+
<button type="button" class="home-hit" onClick={() => void openHit(hit)}>
|
|
706
|
+
<span class="home-card-name">{hit.title || hit.boardTitle}</span>
|
|
707
|
+
{enabled && hit.qualification?.retired && <span class="category-tag">Retired</span>}
|
|
708
|
+
<span class="home-card-meta">
|
|
709
|
+
{hit.boardTitle} · {hit.snippet}
|
|
710
|
+
</span>
|
|
711
|
+
</button>
|
|
712
|
+
</li>
|
|
713
|
+
))}
|
|
714
|
+
</ul>
|
|
715
|
+
</section>
|
|
716
|
+
)}
|
|
717
|
+
{matches.length > 0 ? (
|
|
718
|
+
<section class="home-main-section">
|
|
719
|
+
<span class="home-section-label">Boards · {matches.length}</span>
|
|
720
|
+
<ul class="home-cards">{matches.map(card)}</ul>
|
|
721
|
+
</section>
|
|
722
|
+
) : (
|
|
723
|
+
hits.length === 0 && (
|
|
724
|
+
<div class="home-view-empty" role="status">
|
|
725
|
+
No boards match “{search.trim()}”.
|
|
726
|
+
</div>
|
|
727
|
+
)
|
|
728
|
+
)}
|
|
729
|
+
</>
|
|
730
|
+
) : (
|
|
731
|
+
<>
|
|
732
|
+
{subfolders.length > 0 && (
|
|
733
|
+
<section class="home-main-section">
|
|
734
|
+
<span class="home-section-label">Folders · {subfolders.length}</span>
|
|
735
|
+
<ul class="home-folders">
|
|
736
|
+
{subfolders.map((path) => {
|
|
737
|
+
const inside = boards.filter((board) => inFolder(board, path));
|
|
738
|
+
const latest = inside
|
|
739
|
+
.map((board) => board.lastOpenedAt ?? '')
|
|
740
|
+
.sort()
|
|
741
|
+
.at(-1);
|
|
742
|
+
const inContext = inside.filter((board) => board.pin).length;
|
|
743
|
+
return (
|
|
744
|
+
<li key={path}>
|
|
745
|
+
<button
|
|
746
|
+
type="button"
|
|
747
|
+
class="home-folder"
|
|
748
|
+
data-testid="home-section"
|
|
749
|
+
data-folder={path}
|
|
750
|
+
onClick={() => goTo(path)}
|
|
751
|
+
>
|
|
752
|
+
<span class="home-folder-stack" aria-hidden="true">
|
|
753
|
+
<span />
|
|
754
|
+
<span />
|
|
755
|
+
<span />
|
|
756
|
+
</span>
|
|
757
|
+
{inContext > 0 && <span class="home-folder-context">{inContext} in context</span>}
|
|
758
|
+
<span class="home-folder-name">{leaf(path)}</span>
|
|
759
|
+
<span class="home-folder-meta">
|
|
760
|
+
{plural(inside.length, 'board')}
|
|
761
|
+
{latest ? ` · opened ${openedAgo(latest)}` : ''}
|
|
762
|
+
</span>
|
|
763
|
+
</button>
|
|
764
|
+
</li>
|
|
765
|
+
);
|
|
766
|
+
})}
|
|
767
|
+
</ul>
|
|
768
|
+
</section>
|
|
769
|
+
)}
|
|
770
|
+
<section class="home-main-section">
|
|
771
|
+
<span class="home-section-label">
|
|
772
|
+
{folder ? `Boards in ${leaf(folder)}` : 'Unfiled boards'} · {shown.length}
|
|
773
|
+
</span>
|
|
774
|
+
{shown.length > 0 ? (
|
|
775
|
+
<ul class="home-cards" aria-label={folder ? `Boards in ${folder}` : 'Unfiled boards'}>
|
|
776
|
+
{shown.map(card)}
|
|
777
|
+
</ul>
|
|
778
|
+
) : (
|
|
779
|
+
<div class="home-view-empty">No boards here yet.</div>
|
|
780
|
+
)}
|
|
781
|
+
</section>
|
|
782
|
+
</>
|
|
783
|
+
)}
|
|
784
|
+
</>
|
|
785
|
+
)}
|
|
786
|
+
</main>
|
|
787
|
+
|
|
788
|
+
{view === 'Folders' && details}
|
|
789
|
+
{movingBoard && (
|
|
790
|
+
<BoardFolderDialog
|
|
791
|
+
key={movingBoard.id}
|
|
792
|
+
board={movingBoard}
|
|
793
|
+
folders={folders}
|
|
794
|
+
onClose={() => setMoving(null)}
|
|
795
|
+
/>
|
|
796
|
+
)}
|
|
797
|
+
{copyingBoard && (
|
|
798
|
+
<BoardCopyDialog key={copyingBoard.id} board={copyingBoard} onClose={() => setCopying(null)} />
|
|
799
|
+
)}
|
|
800
|
+
</div>
|
|
273
801
|
)}
|
|
274
|
-
{copyingBoard && <BoardCopyDialog key={copyingBoard.id} board={copyingBoard} onClose={() => setCopying(null)} />}
|
|
275
802
|
</div>
|
|
276
803
|
);
|
|
277
804
|
}
|