@theihtisham/agent-shadow-brain 7.0.0 → 7.1.3
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/CHANGELOG.md +31 -0
- package/README.md +47 -385
- package/dist/activity-cli.d.ts +3 -0
- package/dist/activity-cli.d.ts.map +1 -0
- package/dist/activity-cli.js +78 -0
- package/dist/activity-cli.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +3 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/brain/activity-journal.d.ts +82 -0
- package/dist/brain/activity-journal.d.ts.map +1 -0
- package/dist/brain/activity-journal.js +314 -0
- package/dist/brain/activity-journal.js.map +1 -0
- package/dist/brain/activity-reporting.d.ts +14 -0
- package/dist/brain/activity-reporting.d.ts.map +1 -0
- package/dist/brain/activity-reporting.js +17 -0
- package/dist/brain/activity-reporting.js.map +1 -0
- package/dist/brain/auto-update.js +1 -1
- package/dist/brain/brain-portability.js +1 -1
- package/dist/brain/client-integrations.d.ts +61 -0
- package/dist/brain/client-integrations.d.ts.map +1 -0
- package/dist/brain/client-integrations.js +98 -0
- package/dist/brain/client-integrations.js.map +1 -0
- package/dist/brain/context-assembly.d.ts +76 -0
- package/dist/brain/context-assembly.d.ts.map +1 -0
- package/dist/brain/context-assembly.js +305 -0
- package/dist/brain/context-assembly.js.map +1 -0
- package/dist/brain/feature-settings.d.ts +58 -0
- package/dist/brain/feature-settings.d.ts.map +1 -0
- package/dist/brain/feature-settings.js +241 -0
- package/dist/brain/feature-settings.js.map +1 -0
- package/dist/brain/global-brain.d.ts +7 -0
- package/dist/brain/global-brain.d.ts.map +1 -1
- package/dist/brain/global-brain.js +19 -4
- package/dist/brain/global-brain.js.map +1 -1
- package/dist/brain/mcp-server.d.ts +23 -1
- package/dist/brain/mcp-server.d.ts.map +1 -1
- package/dist/brain/mcp-server.js +224 -24
- package/dist/brain/mcp-server.js.map +1 -1
- package/dist/brain/mcp-stdio.d.ts +12 -0
- package/dist/brain/mcp-stdio.d.ts.map +1 -0
- package/dist/brain/mcp-stdio.js +134 -0
- package/dist/brain/mcp-stdio.js.map +1 -0
- package/dist/brain/memory-annotations.d.ts +97 -0
- package/dist/brain/memory-annotations.d.ts.map +1 -0
- package/dist/brain/memory-annotations.js +388 -0
- package/dist/brain/memory-annotations.js.map +1 -0
- package/dist/brain/memory-graph.d.ts.map +1 -1
- package/dist/brain/memory-graph.js +12 -23
- package/dist/brain/memory-graph.js.map +1 -1
- package/dist/brain/memory-sources.d.ts +8 -0
- package/dist/brain/memory-sources.d.ts.map +1 -0
- package/dist/brain/memory-sources.js +27 -0
- package/dist/brain/memory-sources.js.map +1 -0
- package/dist/brain/orchestrator.js +1 -1
- package/dist/brain/subconscious.d.ts +21 -24
- package/dist/brain/subconscious.d.ts.map +1 -1
- package/dist/brain/subconscious.js +82 -218
- package/dist/brain/subconscious.js.map +1 -1
- package/dist/brain/task-workflow.d.ts +22 -0
- package/dist/brain/task-workflow.d.ts.map +1 -0
- package/dist/brain/task-workflow.js +182 -0
- package/dist/brain/task-workflow.js.map +1 -0
- package/dist/brain/workflow-skills.d.ts +86 -0
- package/dist/brain/workflow-skills.d.ts.map +1 -0
- package/dist/brain/workflow-skills.js +514 -0
- package/dist/brain/workflow-skills.js.map +1 -0
- package/dist/cli.js +21 -2
- package/dist/cli.js.map +1 -1
- package/dist/context-cli.d.ts +3 -0
- package/dist/context-cli.d.ts.map +1 -0
- package/dist/context-cli.js +56 -0
- package/dist/context-cli.js.map +1 -0
- package/dist/curation-cli.d.ts +4 -0
- package/dist/curation-cli.d.ts.map +1 -0
- package/dist/curation-cli.js +118 -0
- package/dist/curation-cli.js.map +1 -0
- package/dist/dashboard/live-workspace-html.d.ts +2 -0
- package/dist/dashboard/live-workspace-html.d.ts.map +1 -0
- package/dist/dashboard/live-workspace-html.js +893 -0
- package/dist/dashboard/live-workspace-html.js.map +1 -0
- package/dist/dashboard/server.d.ts +1 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +53 -18
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/v6-dashboard-html.d.ts +1 -1
- package/dist/dashboard/v6-dashboard-html.d.ts.map +1 -1
- package/dist/dashboard/v6-dashboard-html.js +7 -29
- package/dist/dashboard/v6-dashboard-html.js.map +1 -1
- package/dist/dashboard/workspace-api.d.ts +16 -0
- package/dist/dashboard/workspace-api.d.ts.map +1 -0
- package/dist/dashboard/workspace-api.js +309 -0
- package/dist/dashboard/workspace-api.js.map +1 -0
- package/dist/dashboard/workspace-html.d.ts +2 -0
- package/dist/dashboard/workspace-html.d.ts.map +1 -0
- package/dist/dashboard/workspace-html.js +2132 -0
- package/dist/dashboard/workspace-html.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-cli.d.ts +3 -0
- package/dist/integration-cli.d.ts.map +1 -0
- package/dist/integration-cli.js +14 -0
- package/dist/integration-cli.js.map +1 -0
- package/dist/types.d.ts +12 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/workflow-cli.d.ts +3 -0
- package/dist/workflow-cli.d.ts.map +1 -0
- package/dist/workflow-cli.js +100 -0
- package/dist/workflow-cli.js.map +1 -0
- package/docs/WORKFLOW_INTEGRATION.md +105 -0
- package/docs/WORKSPACE.md +111 -0
- package/docs/assets/context-flow-v7.2.png +0 -0
- package/docs/assets/live-v7.1.3.png +0 -0
- package/docs/assets/memory-detail-v7.1.3.png +0 -0
- package/docs/assets/overview-v7.1.3.png +0 -0
- package/docs/assets/workspace-graph-v7.2.png +0 -0
- package/docs/assets/workspace-overview-v7.2.png +0 -0
- package/docs/launch/RELEASE_NOTES_v7.1.0.md +32 -0
- package/docs/launch/RELEASE_NOTES_v7.1.3.md +40 -0
- package/docs/launch/RELEASE_NOTES_v7.2.0.md +24 -0
- package/package.json +112 -207
- package/skills/shadow-debug/SKILL.md +16 -0
- package/skills/shadow-implement/SKILL.md +16 -0
- package/skills/shadow-release/SKILL.md +16 -0
- package/skills/shadow-research/SKILL.md +12 -0
- package/skills/shadow-review/SKILL.md +14 -0
- package/skills/shadow-workflow/SKILL.md +31 -0
|
@@ -0,0 +1,2132 @@
|
|
|
1
|
+
// Dependency-free local workspace. The advanced dashboard is served separately.
|
|
2
|
+
export const WORKSPACE_HTML = String.raw `<!doctype html>
|
|
3
|
+
<html lang="en" data-theme="light" data-motion="system">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<meta name="color-scheme" content="light dark">
|
|
8
|
+
<title>Agent Shadow Brain · Workspace</title>
|
|
9
|
+
<script>
|
|
10
|
+
try {
|
|
11
|
+
const theme = localStorage.getItem('shadow-brain:workspace:theme');
|
|
12
|
+
const motion = localStorage.getItem('shadow-brain:workspace:motion');
|
|
13
|
+
if (theme === 'dark') document.documentElement.dataset.theme = theme;
|
|
14
|
+
if (motion === 'reduce') document.documentElement.dataset.motion = motion;
|
|
15
|
+
} catch {}
|
|
16
|
+
</script>
|
|
17
|
+
<style>
|
|
18
|
+
:root {
|
|
19
|
+
color-scheme: light;
|
|
20
|
+
--canvas: #f4f6f9; --surface: #fff; --inset: #eef2f6; --hover: #f2f5f8;
|
|
21
|
+
--ink: #182539; --muted: #4e6176; --line: #dbe2e9; --control: #8a9bae;
|
|
22
|
+
--teal: #096459; --teal-fill: #076b5e; --teal-hover: #05584d; --tint: #dff1ec;
|
|
23
|
+
--bad: #a32940; --bad-bg: #fbeaf0; --focus: #167b9d; --amber: #815116; --amber-bg: #f9edd8;
|
|
24
|
+
--rail: #172539; --rail-ink: #f0f5fc; --rail-muted: #b5c5d7; --rail-line: #35475b;
|
|
25
|
+
--rail-selected: #284657; --rail-highlight: #8ed9cb;
|
|
26
|
+
--agent: #57539a; --agent-bg: #eeecfa; --project: var(--teal); --project-bg: var(--tint);
|
|
27
|
+
--memory: #475f7e; --memory-bg: #eaf0f8;
|
|
28
|
+
--graph-line: #6d8195; --graph-project: #478779; --graph-agent: #746aa5; --graph-dot: #ccd7e0; --accent-ink: #fff;
|
|
29
|
+
--skeleton: #e5ebf0; --r: 9px; --ease: 175ms cubic-bezier(.2,.75,.25,1);
|
|
30
|
+
--header: 62px; --sidebar: 222px; --sticky: 2; --notice: 3;
|
|
31
|
+
font-family: Inter, "Segoe UI", system-ui, sans-serif;
|
|
32
|
+
font-size: 15px;
|
|
33
|
+
}
|
|
34
|
+
html[data-theme="dark"] {
|
|
35
|
+
color-scheme: dark;
|
|
36
|
+
--canvas: #111b2a; --surface: #172437; --inset: #1e2e43; --hover: #23344a;
|
|
37
|
+
--ink: #edf4fb; --muted: #b3c1d1; --line: #334459; --control: #74889e;
|
|
38
|
+
--teal: #94e1d3; --teal-fill: #9be6d6; --teal-hover: #baf2e6; --tint: #203f43;
|
|
39
|
+
--bad: #ffadbd; --bad-bg: #492a3a; --focus: #8cd6f3; --amber: #f4cb89; --amber-bg: #43352a;
|
|
40
|
+
--rail: #101a28; --rail-line: #2e4157; --rail-selected: #233e4f;
|
|
41
|
+
--agent: #c7bffc; --agent-bg: #333451; --memory: #bccfea; --memory-bg: #2a3a50;
|
|
42
|
+
--graph-line: #8295ac; --graph-project: #82bcb1; --graph-agent: #aaa0db; --graph-dot: #34465b; --accent-ink: #15372e;
|
|
43
|
+
--skeleton: #2b3a4d;
|
|
44
|
+
}
|
|
45
|
+
* { box-sizing: border-box; }
|
|
46
|
+
[hidden] { display: none !important; }
|
|
47
|
+
body { margin: 0; color: var(--ink); background: var(--canvas); font-size: .94rem; line-height: 1.5; }
|
|
48
|
+
a { color: var(--teal); text-decoration: none; }
|
|
49
|
+
p, h1, h2, h3, ul, dl, dd { margin: 0; }
|
|
50
|
+
p { max-width: 69ch; overflow-wrap: anywhere; }
|
|
51
|
+
h1, h2, h3 { line-height: 1.22; text-wrap: balance; overflow-wrap: anywhere; }
|
|
52
|
+
h1 { font-size: 2rem; letter-spacing: -.035em; font-weight: 710; }
|
|
53
|
+
h2 { font-size: 1.14rem; letter-spacing: -.015em; font-weight: 690; }
|
|
54
|
+
h3 { font-size: 1rem; font-weight: 650; }
|
|
55
|
+
.muted { color: var(--muted); }
|
|
56
|
+
.small { font-size: .86rem; }
|
|
57
|
+
.mono { font: .81rem/1.52 "Cascadia Code", Consolas, monospace; overflow-wrap: anywhere; }
|
|
58
|
+
.icon { width: 18px; height: 18px; flex: none; vertical-align: middle; }
|
|
59
|
+
.icon.small-icon { width: 15px; height: 15px; }
|
|
60
|
+
:is(a,button,input,textarea,select,summary,[tabindex]):focus-visible {
|
|
61
|
+
outline: 2px solid var(--focus); outline-offset: 3px;
|
|
62
|
+
}
|
|
63
|
+
.skip { position: fixed; top: 10px; left: 10px; z-index: var(--notice); transform: translateY(-160%); }
|
|
64
|
+
.skip:focus { transform: none; }
|
|
65
|
+
.shell { display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); min-height: 100vh; }
|
|
66
|
+
.rail {
|
|
67
|
+
position: sticky; top: 0; height: 100vh; height: 100dvh; display: flex; flex-direction: column;
|
|
68
|
+
align-items: stretch; gap: 32px; padding: 24px 15px 18px; background: var(--rail); color: var(--rail-ink);
|
|
69
|
+
}
|
|
70
|
+
.brand { color: var(--rail-ink); display: flex; align-items: center; gap: 11px; padding: 0 7px; font-weight: 710; }
|
|
71
|
+
.brand-mark { width: 33px; height: 33px; display: grid; place-items: center; background: #294354; color: #9ce6d6; border-radius: 8px; }
|
|
72
|
+
.brand-mark .icon { width: 21px; height: 21px; }
|
|
73
|
+
.brand-name { letter-spacing: -.02em; line-height: 1.13; }
|
|
74
|
+
.rail-nav { display: grid; gap: 4px; }
|
|
75
|
+
.nav-link { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 7px; color: var(--rail-muted); font-weight: 580; }
|
|
76
|
+
.nav-link:hover { color: var(--rail-ink); background: #24384d; }
|
|
77
|
+
.nav-link[aria-current="page"] { color: var(--rail-ink); background: var(--rail-selected); }
|
|
78
|
+
.nav-link[aria-current="page"] .icon { color: var(--rail-highlight); }
|
|
79
|
+
.rail :focus-visible { outline-color: #9ce6d6; }
|
|
80
|
+
.rail-foot { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--rail-line); }
|
|
81
|
+
.rail-foot .arrow { margin-left: auto; width: 14px; height: 14px; }
|
|
82
|
+
.rail-build { color: var(--rail-muted); padding: 10px 10px 0; font-size: .78rem; }
|
|
83
|
+
.main { min-width: 0; }
|
|
84
|
+
.top {
|
|
85
|
+
min-height: var(--header); display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
|
86
|
+
padding: 10px 34px; border-bottom: 1px solid var(--line); background: var(--surface);
|
|
87
|
+
}
|
|
88
|
+
.crumb { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
|
89
|
+
.crumb .icon { width: 14px; height: 14px; color: var(--muted); }
|
|
90
|
+
.top-actions { display: flex; align-items: center; gap: 14px; }
|
|
91
|
+
.live-link { white-space: nowrap; }
|
|
92
|
+
.connection { display: inline-flex; gap: 6px; align-items: center; white-space: nowrap; }
|
|
93
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; background: currentColor; }
|
|
94
|
+
.connection[data-state="connected"] .dot { color: var(--teal); }
|
|
95
|
+
.connection[data-state="error"] .dot { color: var(--bad); }
|
|
96
|
+
.content { max-width: 1510px; margin: 0 auto; padding: 33px 34px 52px; min-width: 0; }
|
|
97
|
+
.page-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 24px; }
|
|
98
|
+
.page-head p { margin-top: 8px; }
|
|
99
|
+
.page-head .button { align-self: center; }
|
|
100
|
+
.panel { border: 1px solid var(--line); background: var(--surface); border-radius: var(--r); min-width: 0; }
|
|
101
|
+
.panel-head { display: flex; justify-content: space-between; gap: 14px; align-items: center; padding: 17px 19px; border-bottom: 1px solid var(--line); }
|
|
102
|
+
.panel-head p { margin-top: 3px; }
|
|
103
|
+
.panel-body { padding: 18px 19px; min-width: 0; }
|
|
104
|
+
.stack { display: grid; gap: 17px; align-content: start; min-width: 0; }
|
|
105
|
+
.split { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); align-items: start; gap: 18px; }
|
|
106
|
+
.button {
|
|
107
|
+
appearance: none; color: var(--ink); background: var(--surface); border: 1px solid var(--control);
|
|
108
|
+
min-height: 37px; padding: 6px 11px; font: inherit; font-size: .88rem; font-weight: 650; border-radius: 7px;
|
|
109
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px; text-align: center; line-height: 1.22;
|
|
110
|
+
text-decoration: none; cursor: pointer; transition: background var(--ease), color var(--ease), border-color var(--ease);
|
|
111
|
+
}
|
|
112
|
+
.button:hover, .button[aria-pressed="true"] { background: var(--hover); }
|
|
113
|
+
.button[aria-pressed="true"] { color: var(--teal); border-color: var(--teal); }
|
|
114
|
+
.button.primary { background: var(--teal-fill); color: var(--accent-ink); border-color: var(--teal-fill); }
|
|
115
|
+
.button.primary:hover { background: var(--teal-hover); border-color: var(--teal-hover); }
|
|
116
|
+
.button.quiet { background: transparent; border-color: transparent; color: var(--teal); }
|
|
117
|
+
.button.quiet:hover { background: var(--hover); }
|
|
118
|
+
.button.icon-button { width: 37px; padding: 0; }
|
|
119
|
+
.button:disabled { opacity: .65; cursor: default; }
|
|
120
|
+
.button:active:not(:disabled) { transform: translateY(1px); }
|
|
121
|
+
.label { display: grid; gap: 5px; font-size: .88rem; font-weight: 630; min-width: 0; }
|
|
122
|
+
:is(input:not([type=checkbox]),select,textarea) {
|
|
123
|
+
min-width: 0; width: 100%; color: var(--ink); background: var(--surface); font: inherit; font-weight: 450;
|
|
124
|
+
border: 1px solid var(--control); border-radius: 7px; padding: 8px 10px; min-height: 38px;
|
|
125
|
+
}
|
|
126
|
+
:is(input,textarea)::placeholder { color: var(--muted); opacity: 1; }
|
|
127
|
+
textarea { resize: vertical; }
|
|
128
|
+
.search-field { position: relative; min-width: 0; }
|
|
129
|
+
.search-field > [data-icon] { position: absolute; bottom: 10px; left: 10px; color: var(--muted); pointer-events: none; line-height: 1; }
|
|
130
|
+
.search-field input { padding-left: 34px; }
|
|
131
|
+
.inline-check { display: flex; align-items: flex-start; gap: 7px; min-height: 28px; }
|
|
132
|
+
.inline-check input { accent-color: var(--teal-fill); margin-top: 4px; }
|
|
133
|
+
.form-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
134
|
+
.filters { display: grid; grid-template-columns: minmax(155px,1.45fr) repeat(4,minmax(96px,1fr)) auto; gap: 10px; align-items: end; }
|
|
135
|
+
.filter-bar { display: flex; align-items: end; gap: 10px; flex-wrap: wrap; }
|
|
136
|
+
.filter-bar .label { flex: 1 1 155px; }
|
|
137
|
+
.status { padding: 13px 17px; border: 1px solid var(--line); border-radius: 7px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; background: var(--surface); }
|
|
138
|
+
.status.error { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
|
|
139
|
+
.status.error .button { color: var(--bad); }
|
|
140
|
+
.status.error .button:hover { background: var(--surface); }
|
|
141
|
+
.blank { display: grid; gap: 6px; padding: 29px 19px; }
|
|
142
|
+
.blank > .icon { color: var(--teal); margin-bottom: 4px; }
|
|
143
|
+
.blank a { justify-self: start; margin-top: 4px; }
|
|
144
|
+
.skeleton { display: grid; gap: 12px; padding: 19px; }
|
|
145
|
+
.skeleton i { width: 82%; height: 12px; display: block; border-radius: 4px; background: var(--skeleton); }
|
|
146
|
+
.skeleton i:nth-child(2) { width: 63%; }
|
|
147
|
+
.skeleton i:nth-child(3) { width: 71%; }
|
|
148
|
+
.tag {
|
|
149
|
+
background: var(--inset); color: var(--muted); padding: 3px 7px; border-radius: 5px;
|
|
150
|
+
font-size: .79rem; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere;
|
|
151
|
+
}
|
|
152
|
+
.tag.project { background: var(--project-bg); color: var(--project); }
|
|
153
|
+
.tag.agent { background: var(--agent-bg); color: var(--agent); }
|
|
154
|
+
.tag.memory { background: var(--memory-bg); color: var(--memory); }
|
|
155
|
+
.tag.state-active, .tag.check-ok { background: var(--tint); color: var(--teal); }
|
|
156
|
+
.tag.state-contested, .tag.check-warn { background: var(--amber-bg); color: var(--amber); }
|
|
157
|
+
.tag.state-superseded { background: var(--agent-bg); color: var(--agent); }
|
|
158
|
+
.tag.check-error { background: var(--bad-bg); color: var(--bad); }
|
|
159
|
+
.tags { display: flex; align-items: center; flex-wrap: wrap; gap: 5px 7px; }
|
|
160
|
+
.summary {
|
|
161
|
+
display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); background: var(--surface);
|
|
162
|
+
border: 1px solid var(--line); border-radius: var(--r); margin-bottom: 18px;
|
|
163
|
+
}
|
|
164
|
+
.summary-item { display: flex; align-items: center; gap: 13px; min-width: 0; padding: 18px 19px; }
|
|
165
|
+
.summary-item + .summary-item { border-left: 1px solid var(--line); }
|
|
166
|
+
.tile-icon { display: grid; place-items: center; width: 37px; height: 37px; border-radius: 7px; flex: none; background: var(--tint); color: var(--teal); }
|
|
167
|
+
.summary dt { color: var(--muted); font-size: .84rem; font-weight: 600; }
|
|
168
|
+
.summary dd { font-size: 1.3rem; font-weight: 700; line-height: 1.12; font-variant-numeric: tabular-nums; margin-top: 4px; }
|
|
169
|
+
.list { list-style: none; padding: 0; }
|
|
170
|
+
.list > li + li { border-top: 1px solid var(--line); }
|
|
171
|
+
.project-row { display: grid; grid-template-columns: minmax(0,1.5fr) minmax(65px,.55fr) auto; align-items: center; gap: 12px; padding: 12px 19px; }
|
|
172
|
+
.project-name { display: flex; gap: 7px; align-items: center; min-width: 0; font-weight: 650; overflow-wrap: anywhere; }
|
|
173
|
+
.project-name:hover { text-decoration: underline; }
|
|
174
|
+
.project-detail { margin: 3px 0 0 22px; }
|
|
175
|
+
.project-count { color: var(--muted); font-size: .81rem; text-align: right; font-variant-numeric: tabular-nums; }
|
|
176
|
+
.project-meter { display: block; width: 100%; height: 5px; margin-top: 4px; border-radius: 3px; overflow: hidden; background: var(--inset); }
|
|
177
|
+
.project-meter span { display: block; height: 100%; background: var(--teal); border-radius: inherit; opacity: .75; }
|
|
178
|
+
.event { display: grid; grid-template-columns: 27px minmax(0,1fr) auto; gap: 9px; align-items: start; padding: 12px 19px; }
|
|
179
|
+
.event .tile-icon { width: 26px; height: 26px; }
|
|
180
|
+
.event strong { display: block; font-size: .89rem; font-weight: 630; overflow-wrap: anywhere; }
|
|
181
|
+
.event time { color: var(--muted); font-size: .77rem; }
|
|
182
|
+
.event-meta { margin-top: 2px; }
|
|
183
|
+
.overview-memory { display: grid; gap: 5px; padding: 12px 19px; }
|
|
184
|
+
.overview-memory strong { overflow-wrap: anywhere; }
|
|
185
|
+
.toolbar-end { margin-left: auto; }
|
|
186
|
+
.list-footer { border-top: 1px solid var(--line); display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 12px 18px; }
|
|
187
|
+
.memory-row { padding: 14px 19px; display: grid; gap: 8px; }
|
|
188
|
+
.memory-head { display: flex; justify-content: space-between; align-items: start; gap: 10px; }
|
|
189
|
+
.memory-head time { white-space: nowrap; }
|
|
190
|
+
.record-link { color: var(--ink); font-weight: 680; text-decoration: underline; text-decoration-color: var(--control); text-underline-offset: 3px; }
|
|
191
|
+
.record-link:hover { color: var(--teal); text-decoration-color: currentColor; }
|
|
192
|
+
.memory-content { white-space: pre-wrap; overflow-wrap: anywhere; max-width: none; }
|
|
193
|
+
.detail-head { display: grid; gap: 9px; }
|
|
194
|
+
.detail-head > .button { justify-self: start; }
|
|
195
|
+
.detail-layout { grid-template-columns: minmax(0,1.45fr) minmax(265px,1fr); }
|
|
196
|
+
.detail-form, .detail-sources, .check-result { display: grid; gap: 9px; }
|
|
197
|
+
.detail-note { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
198
|
+
.detail-row { display: grid; gap: 7px; padding: 13px 19px; }
|
|
199
|
+
.link-search { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: end; gap: 8px; }
|
|
200
|
+
.save-status:empty { display: none; }
|
|
201
|
+
.inline-error { color: var(--bad); }
|
|
202
|
+
.quality-track { display: flex; width: 100%; height: 12px; background: var(--inset); border-radius: 5px; overflow: hidden; }
|
|
203
|
+
.quality-track > span { flex: none; }
|
|
204
|
+
.quality-active { background: var(--teal-fill); }
|
|
205
|
+
.quality-contested { background: var(--amber); }
|
|
206
|
+
.quality-archived { background: var(--control); }
|
|
207
|
+
.quality-superseded { background: var(--agent); }
|
|
208
|
+
.quality-labels { display: flex; flex-wrap: wrap; gap: 8px 13px; }
|
|
209
|
+
.quality-labels a { display: inline-flex; align-items: center; gap: 6px; color: var(--ink); font-size: .82rem; }
|
|
210
|
+
.quality-labels a:hover { text-decoration: underline; }
|
|
211
|
+
.quality-labels i { width: 8px; height: 8px; border-radius: 2px; }
|
|
212
|
+
.quality-source { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }
|
|
213
|
+
.check-row { display: grid; grid-template-columns: minmax(130px,.9fr) minmax(0,1.7fr) auto; gap: 10px; align-items: start; padding: 13px 19px; }
|
|
214
|
+
.check-row .tag { justify-self: end; }
|
|
215
|
+
.memory-row p { max-width: 90ch; white-space: pre-wrap; }
|
|
216
|
+
.memory-row details summary { color: var(--teal); font-size: .85rem; font-weight: 650; cursor: pointer; width: fit-content; max-width: 100%; }
|
|
217
|
+
.memory-row details ul { padding-left: 19px; margin-top: 4px; }
|
|
218
|
+
.memory-row details li { overflow-wrap: anywhere; }
|
|
219
|
+
.source { display: flex; align-items: baseline; gap: 6px; color: var(--muted); min-width: 0; }
|
|
220
|
+
.source .icon { align-self: center; }
|
|
221
|
+
.graph-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(225px, 27%); min-width: 0; }
|
|
222
|
+
.graph-stage { position: relative; min-width: 0; height: 480px; background: var(--canvas); overflow: hidden; }
|
|
223
|
+
#graph-svg { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; user-select: none; }
|
|
224
|
+
#graph-svg.dragging { cursor: grabbing; }
|
|
225
|
+
.cluster-area { fill: var(--project-bg); fill-opacity: .49; stroke: var(--graph-project); stroke-opacity: .25; }
|
|
226
|
+
.graph-node { color: var(--memory); cursor: pointer; }
|
|
227
|
+
.graph-node.project { color: var(--project); }
|
|
228
|
+
.graph-node.agent { color: var(--agent); }
|
|
229
|
+
.graph-node .node-surface { fill: var(--surface); stroke: var(--line); stroke-width: 1.25; }
|
|
230
|
+
.graph-node.project .node-surface { fill: var(--project-bg); stroke: var(--graph-project); }
|
|
231
|
+
.graph-node.agent .node-surface { fill: var(--agent-bg); stroke: var(--graph-agent); }
|
|
232
|
+
.graph-node.dot-node .node-surface { stroke: var(--memory); }
|
|
233
|
+
.graph-node .node-glyph { fill: currentColor; }
|
|
234
|
+
.graph-node text { fill: var(--ink); font-size: 12px; font-weight: 600; }
|
|
235
|
+
.graph-node .node-count { fill: var(--muted); font-size: 11px; font-weight: 550; }
|
|
236
|
+
.graph-node:is(:hover,:focus-visible,[aria-pressed="true"]) .node-surface { stroke: currentColor; stroke-width: 2.2; }
|
|
237
|
+
.graph-node.dot-node:is(:hover,:focus-visible,[aria-pressed="true"]) .node-surface { fill: var(--memory-bg); }
|
|
238
|
+
.graph-node:focus-visible { outline: none; }
|
|
239
|
+
.graph-node.unrelated { opacity: .34; }
|
|
240
|
+
.graph-edge { fill: none; stroke: var(--graph-line); stroke-width: 1.15; opacity: .53; transition: opacity var(--ease); }
|
|
241
|
+
.graph-edge.project-edge { stroke: var(--graph-project); }
|
|
242
|
+
.graph-edge.agent-edge { stroke: var(--graph-agent); stroke-dasharray: 4 3; opacity: .35; }
|
|
243
|
+
.graph-edge.active { stroke: var(--teal); stroke-width: 2.1; opacity: 1; }
|
|
244
|
+
.graph-edge.agent-edge.active { stroke: var(--agent); }
|
|
245
|
+
.graph-edge.recorded-edge { stroke: var(--agent); stroke-width: 1.8; opacity: .8; }
|
|
246
|
+
.graph-edge.contradicts { stroke: var(--bad); stroke-dasharray: 5 3; }
|
|
247
|
+
.graph-edge.supersedes { stroke: var(--teal); stroke-dasharray: 2 2; }
|
|
248
|
+
.graph-edge.recorded-edge.active { stroke-width: 3; opacity: 1; }
|
|
249
|
+
.graph-edge.unrelated { opacity: .13; }
|
|
250
|
+
.graph-edge-hit { fill: none; stroke: transparent; stroke-width: 14; pointer-events: stroke; cursor: pointer; }
|
|
251
|
+
.graph-link-group:is(:hover,:focus-visible) .graph-edge.recorded-edge { opacity: 1; stroke-width: 3; }
|
|
252
|
+
.graph-link-group:focus-visible { outline: none; }
|
|
253
|
+
.graph-arrow { fill: var(--agent); }
|
|
254
|
+
.graph-tooltip { pointer-events: none; }
|
|
255
|
+
.graph-tooltip rect { fill: var(--surface); stroke: var(--memory); }
|
|
256
|
+
.graph-tooltip text { fill: var(--ink); font-size: 12px; font-weight: 650; }
|
|
257
|
+
.graph-tools { position: absolute; bottom: 12px; left: 12px; display: flex; gap: 5px; align-items: center; }
|
|
258
|
+
.graph-tools .button { min-height: 32px; }
|
|
259
|
+
.graph-tools .icon-button { width: 32px; }
|
|
260
|
+
.graph-tools output { padding: 4px 6px; background: var(--surface); color: var(--muted); border-radius: 5px; }
|
|
261
|
+
.graph-inspector { min-width: 0; border-left: 1px solid var(--line); max-height: 480px; overflow-y: auto; }
|
|
262
|
+
.graph-inspector .panel-body { display: grid; gap: 11px; }
|
|
263
|
+
.graph-inspector p { white-space: pre-wrap; }
|
|
264
|
+
.related { display: grid; gap: 5px; }
|
|
265
|
+
.related .button { justify-content: start; overflow-wrap: anywhere; text-align: left; }
|
|
266
|
+
.legend { display: flex; align-items: center; flex-wrap: wrap; gap: 7px 14px; padding: 11px 18px; border-top: 1px solid var(--line); color: var(--muted); font-size: .82rem; }
|
|
267
|
+
.legend > span { display: inline-flex; align-items: center; gap: 5px; }
|
|
268
|
+
.legend-glyph { display: inline-block; width: 9px; height: 9px; background: currentColor; flex: none; }
|
|
269
|
+
.legend-glyph.project { border-radius: 2px; color: var(--project); }
|
|
270
|
+
.legend-glyph.agent { border-radius: 50%; color: var(--agent); }
|
|
271
|
+
.legend-glyph.memory { width: 7px; height: 7px; transform: rotate(45deg); color: var(--memory); margin-right: 2px; }
|
|
272
|
+
.legend-line { display: inline-block; width: 16px; border-top: 2px solid var(--graph-line); }
|
|
273
|
+
.legend-line.project-edge { border-color: var(--graph-project); }
|
|
274
|
+
.legend-line.agent-edge { border-color: var(--graph-agent); border-top-style: dashed; }
|
|
275
|
+
.legend-line.recorded-edge { border-color: var(--agent); }
|
|
276
|
+
.graph-list { min-width: 0; max-height: 480px; overflow-y: auto; }
|
|
277
|
+
.graph-list .graph-row { padding: 9px 12px; }
|
|
278
|
+
.graph-list .button { justify-content: flex-start; text-align: left; width: 100%; }
|
|
279
|
+
.graph-row .tag { margin-left: auto; }
|
|
280
|
+
.context-layout { grid-template-columns: minmax(250px,.9fr) minmax(0,1.35fr); }
|
|
281
|
+
.context-config { display: grid; gap: 12px; }
|
|
282
|
+
.context-preview { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--inset); border-radius: 7px; padding: 13px; margin: 0; max-height: 420px; overflow: auto; }
|
|
283
|
+
.context-reasons { display: grid; gap: 10px; }
|
|
284
|
+
.context-reasons summary { cursor: pointer; font-weight: 650; }
|
|
285
|
+
.context-reasons li { padding: 8px 0; }
|
|
286
|
+
.context-reasons li strong { overflow-wrap: anywhere; }
|
|
287
|
+
.context-reasons li p { margin-top: 2px; }
|
|
288
|
+
.settings { max-width: 930px; }
|
|
289
|
+
.setting { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding: 16px 19px; }
|
|
290
|
+
.setting p { margin-top: 4px; }
|
|
291
|
+
.switch { display: inline-flex; align-items: center; justify-content: end; gap: 7px; min-width: 87px; min-height: 36px; border-color: transparent; background: transparent; }
|
|
292
|
+
.switch-track { width: 32px; height: 18px; border-radius: 11px; position: relative; background: var(--control); }
|
|
293
|
+
.switch-track::after {
|
|
294
|
+
content: ""; width: 12px; height: 12px; position: absolute; left: 3px; top: 3px; border-radius: 50%;
|
|
295
|
+
background: white; transition: transform var(--ease);
|
|
296
|
+
}
|
|
297
|
+
.switch[aria-checked="true"] .switch-track { background: var(--teal-fill); }
|
|
298
|
+
.switch[aria-checked="true"] .switch-track::after { transform: translateX(14px); background: var(--accent-ink); }
|
|
299
|
+
.setting select { width: 131px; flex: none; }
|
|
300
|
+
.save-status { padding: 8px 19px; min-height: 35px; border-top: 1px solid var(--line); }
|
|
301
|
+
.save-status[data-error="true"] { color: var(--bad); }
|
|
302
|
+
.notice { margin: 0 0 16px; }
|
|
303
|
+
.entered { animation: enter var(--ease) both; }
|
|
304
|
+
@keyframes enter { from { opacity: .7; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
|
|
305
|
+
html[data-motion="reduce"] *, html[data-motion="reduce"] *::before, html[data-motion="reduce"] *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
|
306
|
+
@media (prefers-reduced-motion: reduce) {
|
|
307
|
+
*, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
|
308
|
+
}
|
|
309
|
+
@media (max-width: 1140px) {
|
|
310
|
+
:root { --sidebar: 188px; }
|
|
311
|
+
.top, .content { padding-left: 24px; padding-right: 24px; }
|
|
312
|
+
.filters { grid-template-columns: repeat(4,minmax(0,1fr)); }
|
|
313
|
+
.filters .search-field { grid-column: span 3; }
|
|
314
|
+
.filters .button { grid-column: 4; grid-row: 1; }
|
|
315
|
+
.overview-columns { grid-template-columns: minmax(0,1.25fr) minmax(0,1fr); }
|
|
316
|
+
.event { grid-template-columns: 27px minmax(0,1fr); }
|
|
317
|
+
.event time { grid-column: 2; }
|
|
318
|
+
}
|
|
319
|
+
@media (max-width: 900px) {
|
|
320
|
+
:root { --sidebar: 66px; }
|
|
321
|
+
.rail { padding-left: 10px; padding-right: 10px; }
|
|
322
|
+
.rail .brand { justify-content: center; padding: 0; }
|
|
323
|
+
.nav-link { justify-content: center; min-height: 41px; }
|
|
324
|
+
.brand-name, .nav-label, .rail-foot .arrow, .rail-build { display: none; }
|
|
325
|
+
.context-layout { grid-template-columns: minmax(0,1fr); }
|
|
326
|
+
}
|
|
327
|
+
@media (max-width: 690px) {
|
|
328
|
+
:root { --header: 48px; font-size: 14px; }
|
|
329
|
+
.shell { display: block; }
|
|
330
|
+
.rail { z-index: var(--sticky); height: auto; flex-direction: row; align-items: center; gap: 7px; padding: 5px 10px; }
|
|
331
|
+
.rail-nav { display: flex; gap: 3px; }
|
|
332
|
+
.nav-link { padding: 7px 9px; min-height: 35px; }
|
|
333
|
+
.rail-foot { margin: 0 0 0 auto; padding: 0; border: 0; }
|
|
334
|
+
.brand-mark { width: 30px; height: 30px; }
|
|
335
|
+
.top { padding: 7px 13px; }
|
|
336
|
+
.top-actions { gap: 8px; }
|
|
337
|
+
.content { padding: 22px 12px 32px; }
|
|
338
|
+
h1 { font-size: 1.68rem; }
|
|
339
|
+
.page-head { gap: 8px; margin-bottom: 17px; flex-wrap: wrap; }
|
|
340
|
+
.summary { margin-bottom: 13px; }
|
|
341
|
+
.summary-item { gap: 6px; padding: 11px 9px; }
|
|
342
|
+
.summary-item .tile-icon { display: none; }
|
|
343
|
+
.split { grid-template-columns: minmax(0,1fr); gap: 13px; }
|
|
344
|
+
.filters { grid-template-columns: repeat(2,minmax(0,1fr)); }
|
|
345
|
+
.filters .search-field { grid-column: 1; }
|
|
346
|
+
.filters .button { grid-column: 2; grid-row: 1; }
|
|
347
|
+
.check-row { grid-template-columns: minmax(0,1fr) auto; padding-left: 12px; padding-right: 12px; }
|
|
348
|
+
.check-row .check-result { grid-column: 1 / -1; grid-row: 2; }
|
|
349
|
+
.check-row .tag { grid-column: 2; grid-row: 1; }
|
|
350
|
+
.graph-body { grid-template-columns: minmax(0,1fr); }
|
|
351
|
+
.graph-stage, .graph-list { height: 365px; }
|
|
352
|
+
.graph-inspector { border-left: 0; border-top: 1px solid var(--line); max-height: 340px; }
|
|
353
|
+
.panel-head, .panel-body, .memory-row, .setting { padding-left: 12px; padding-right: 12px; }
|
|
354
|
+
.project-row, .event { padding-left: 12px; padding-right: 12px; }
|
|
355
|
+
.list-footer { flex-wrap: wrap; }
|
|
356
|
+
.graph-mode { margin-left: 0; }
|
|
357
|
+
}
|
|
358
|
+
@media (max-width: 355px) {
|
|
359
|
+
.rail { gap: 3px; }
|
|
360
|
+
.nav-link { padding-left: 7px; padding-right: 7px; }
|
|
361
|
+
.crumb > .muted, .crumb .icon { display: none; }
|
|
362
|
+
.filters { grid-template-columns: repeat(2,minmax(0,1fr)); }
|
|
363
|
+
.filters .search-field { grid-column: 1 / -1; }
|
|
364
|
+
.filters .button { grid-row: auto; grid-column: auto; }
|
|
365
|
+
}
|
|
366
|
+
</style>
|
|
367
|
+
</head>
|
|
368
|
+
<body>
|
|
369
|
+
<a class="skip button" href="#main-content">Skip to content</a>
|
|
370
|
+
<div class="shell">
|
|
371
|
+
<aside class="rail">
|
|
372
|
+
<a class="brand" href="#overview" aria-label="Agent Shadow Brain overview">
|
|
373
|
+
<span class="brand-mark" data-icon="brand"></span><span class="brand-name">Agent Shadow Brain</span>
|
|
374
|
+
</a>
|
|
375
|
+
<nav class="rail-nav" aria-label="Workspace">
|
|
376
|
+
<a class="nav-link" href="#overview" aria-label="Overview" title="Overview" data-view="overview" aria-current="page"><span data-icon="overview"></span><span class="nav-label">Overview</span></a>
|
|
377
|
+
<a class="nav-link" href="#memories" aria-label="Memories" title="Memories" data-view="memories"><span data-icon="memory"></span><span class="nav-label">Memories</span></a>
|
|
378
|
+
<a class="nav-link" href="#graph" aria-label="Graph" title="Graph" data-view="graph"><span data-icon="graph"></span><span class="nav-label">Graph</span></a>
|
|
379
|
+
<a class="nav-link" href="#context" aria-label="Context" title="Context" data-view="context"><span data-icon="context"></span><span class="nav-label">Context</span></a>
|
|
380
|
+
<a class="nav-link" href="#integrations" aria-label="Integrations and health" title="Integrations and health" data-view="integrations"><span data-icon="integrations"></span><span class="nav-label">Integrations</span></a>
|
|
381
|
+
<a class="nav-link" href="#features" aria-label="Features" title="Features" data-view="features"><span data-icon="settings"></span><span class="nav-label">Features</span></a>
|
|
382
|
+
</nav>
|
|
383
|
+
<div class="rail-foot">
|
|
384
|
+
<a class="nav-link" href="/advanced" aria-label="Advanced dashboard" title="Advanced dashboard"><span data-icon="advanced"></span><span class="nav-label">Advanced tools</span><span class="arrow" data-icon="diagonal"></span></a>
|
|
385
|
+
<p class="rail-build" id="rail-version">Local workspace</p>
|
|
386
|
+
</div>
|
|
387
|
+
</aside>
|
|
388
|
+
<div class="main">
|
|
389
|
+
<header class="top">
|
|
390
|
+
<div class="crumb"><span class="muted">Workspace</span><span data-icon="chevron"></span><strong id="crumb">Overview</strong></div>
|
|
391
|
+
<div class="top-actions">
|
|
392
|
+
<a class="button quiet live-link" href="/live"><span data-icon="activity"></span>Live</a>
|
|
393
|
+
<span class="connection muted small" id="connection" data-state="loading"><span class="dot"></span><span id="connection-label">Connecting</span></span>
|
|
394
|
+
<button type="button" class="button icon-button quiet" id="theme-toggle" aria-label="Switch to dark theme" title="Switch to dark theme"><span data-icon="moon"></span></button>
|
|
395
|
+
</div>
|
|
396
|
+
</header>
|
|
397
|
+
<main id="main-content" class="content" tabindex="-1">
|
|
398
|
+
<div class="notice" id="workspace-notice" role="status" hidden></div>
|
|
399
|
+
<section id="page-overview" aria-labelledby="title-overview">
|
|
400
|
+
<div class="page-head">
|
|
401
|
+
<div><h1 id="title-overview" tabindex="-1">Overview</h1><p class="muted">See what your agents remember and what they’ve done recently.</p></div>
|
|
402
|
+
<a href="#context" class="button primary"><span data-icon="context"></span>Assemble context</a>
|
|
403
|
+
</div>
|
|
404
|
+
<div id="overview-body" aria-busy="true"></div>
|
|
405
|
+
</section>
|
|
406
|
+
<section id="page-memories" aria-labelledby="title-memories" hidden>
|
|
407
|
+
<div class="page-head"><div><h1 id="title-memories" tabindex="-1">Memories</h1><p class="muted">Search saved lessons and see why they were recalled.</p></div></div>
|
|
408
|
+
<div class="stack">
|
|
409
|
+
<form id="memory-form" class="panel panel-body filters" role="search" aria-label="Search memories">
|
|
410
|
+
<label class="label search-field" for="memory-query">Search<span data-icon="search"></span><input id="memory-query" type="search" maxlength="500" placeholder="Search memories"></label>
|
|
411
|
+
<label class="label" for="memory-project">Project<select id="memory-project"><option value="">All projects</option></select></label>
|
|
412
|
+
<label class="label" for="memory-category">Category<select id="memory-category"><option value="">All categories</option></select></label>
|
|
413
|
+
<label class="label" for="memory-agent">Agent<select id="memory-agent"><option value="">All agents</option></select></label>
|
|
414
|
+
<label class="label" for="memory-state">Status<select id="memory-state"><option value="">All statuses</option><option value="active">Active</option><option value="contested">Needs review</option><option value="archived">Archived</option><option value="superseded">Superseded</option></select></label>
|
|
415
|
+
<button class="button primary" id="memory-search" type="submit"><span data-icon="search"></span>Search</button>
|
|
416
|
+
</form>
|
|
417
|
+
<div id="memory-results" class="panel" aria-busy="true"></div>
|
|
418
|
+
</div>
|
|
419
|
+
</section>
|
|
420
|
+
<section id="page-memory" aria-labelledby="title-memory" hidden>
|
|
421
|
+
<div class="page-head">
|
|
422
|
+
<div class="detail-head"><a id="detail-back" href="#memories" class="button quiet"><span data-icon="left"></span>Memories</a><h1 id="title-memory" tabindex="-1">Memory detail</h1><div id="detail-meta"></div></div>
|
|
423
|
+
<div class="form-actions" id="detail-actions" hidden>
|
|
424
|
+
<a id="detail-graph" class="button" href="#graph"><span data-icon="graph"></span>View on graph</a>
|
|
425
|
+
<a id="detail-context" class="button" href="#context"><span data-icon="context"></span>Assemble context</a>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
<div id="detail-body" aria-busy="true"></div>
|
|
429
|
+
</section>
|
|
430
|
+
<section id="page-graph" aria-labelledby="title-graph" hidden>
|
|
431
|
+
<div class="page-head"><div><h1 id="title-graph" tabindex="-1">Graph</h1><p class="muted">Explore project membership, recording agents, and links between memories.</p></div></div>
|
|
432
|
+
<div class="stack">
|
|
433
|
+
<div class="panel panel-body filter-bar">
|
|
434
|
+
<label class="label search-field" for="graph-query">Find a node<span data-icon="search"></span><input id="graph-query" type="search" maxlength="200" placeholder="Filter by name"></label>
|
|
435
|
+
<label class="label" for="graph-project">Project<select id="graph-project"><option value="">All projects</option></select></label>
|
|
436
|
+
<label class="label" for="graph-kind">Node type<select id="graph-kind"><option value="">All types</option><option value="project">Projects</option><option value="agent">Agents</option><option value="memory">Memories</option></select></label>
|
|
437
|
+
<label class="label" for="graph-relation">Relationship<select id="graph-relation"><option value="">All relationships</option><option value="recorded">Memory links</option><option value="belongs-to">Project membership</option><option value="recorded-by">Recording agent</option><option value="related">Related</option><option value="supports">Supports</option><option value="contradicts">Contradicts</option><option value="supersedes">Supersedes</option></select></label>
|
|
438
|
+
<div class="form-actions toolbar-end graph-mode" role="group" aria-label="Graph view">
|
|
439
|
+
<button type="button" class="button" id="graph-mode-map" aria-pressed="true"><span data-icon="graph"></span>Map</button>
|
|
440
|
+
<button type="button" class="button" id="graph-mode-list" aria-pressed="false"><span data-icon="list"></span>List</button>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="panel" id="graph-panel" aria-busy="true">
|
|
444
|
+
<div class="panel-head"><h2>Memory relationships</h2><span id="graph-count" class="muted small" role="status"></span></div>
|
|
445
|
+
<div id="graph-status"></div>
|
|
446
|
+
<div class="graph-body" id="graph-body" hidden>
|
|
447
|
+
<div class="graph-stage" id="graph-stage">
|
|
448
|
+
<svg id="graph-svg" xmlns="http://www.w3.org/2000/svg" role="group" aria-label="Memory relationship map. Use arrow keys to navigate nodes or links."></svg>
|
|
449
|
+
<div class="graph-tools" role="group" aria-label="Map zoom">
|
|
450
|
+
<button class="button icon-button" type="button" id="graph-out" aria-label="Zoom out" title="Zoom out"><span data-icon="minus"></span></button>
|
|
451
|
+
<button class="button icon-button" type="button" id="graph-in" aria-label="Zoom in" title="Zoom in"><span data-icon="plus"></span></button>
|
|
452
|
+
<button class="button" type="button" id="graph-fit">Fit</button>
|
|
453
|
+
<output id="graph-scale" class="small" aria-label="Zoom level">100%</output>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
<div id="graph-list" class="graph-list" hidden></div>
|
|
457
|
+
<aside id="graph-inspector" class="graph-inspector" aria-label="Selected node details"></aside>
|
|
458
|
+
</div>
|
|
459
|
+
<div id="graph-legend" class="legend" hidden>
|
|
460
|
+
<span><i class="legend-glyph project"></i> Project</span><span><i class="legend-glyph agent"></i> Agent</span><span><i class="legend-glyph memory"></i> Memory</span>
|
|
461
|
+
<span><i class="legend-line project-edge"></i> Belongs to project</span><span><i class="legend-line agent-edge"></i> Recorded by agent</span>
|
|
462
|
+
<span><i class="legend-line recorded-edge"></i> Recorded memory link</span>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</section>
|
|
467
|
+
<section id="page-context" aria-labelledby="title-context" hidden>
|
|
468
|
+
<div class="page-head"><div><h1 id="title-context" tabindex="-1">Context</h1><p class="muted">Build a project brief for a specific task and review what it includes.</p></div></div>
|
|
469
|
+
<div class="split context-layout">
|
|
470
|
+
<div class="panel">
|
|
471
|
+
<div class="panel-head"><h2>Task details</h2></div>
|
|
472
|
+
<form class="panel-body context-config" id="context-form">
|
|
473
|
+
<label class="label" for="context-project">Project<select id="context-project" required><option value="">Select a project</option></select></label>
|
|
474
|
+
<label class="label" for="context-task">Task<textarea id="context-task" rows="5" maxlength="4096" required placeholder="Describe what the agent will work on."></textarea></label>
|
|
475
|
+
<label class="label" for="context-budget">Estimated token budget<input id="context-budget" type="number" inputmode="numeric" min="0" max="12000" step="1" value="2000" required></label>
|
|
476
|
+
<label class="inline-check small" id="context-cross-row" hidden><input type="checkbox" id="context-cross">Include eligible memories from other projects</label>
|
|
477
|
+
<div id="context-gate"></div>
|
|
478
|
+
<div class="form-actions"><button class="button primary" id="context-submit" type="submit" disabled><span data-icon="context"></span>Assemble context</button></div>
|
|
479
|
+
</form>
|
|
480
|
+
</div>
|
|
481
|
+
<div class="panel" id="context-result"><div class="panel-head"><h2>Context preview</h2></div><div id="context-output"></div></div>
|
|
482
|
+
</div>
|
|
483
|
+
</section>
|
|
484
|
+
<section id="page-integrations" aria-labelledby="title-integrations" hidden>
|
|
485
|
+
<div class="page-head">
|
|
486
|
+
<div><h1 id="title-integrations" tabindex="-1">Integrations & health</h1><p class="muted">Check local storage and see which capabilities each agent supports.</p></div>
|
|
487
|
+
<button class="button" type="button" id="integrations-refresh"><span data-icon="refresh"></span>Run checks</button>
|
|
488
|
+
</div>
|
|
489
|
+
<div id="integrations-body" aria-busy="true"></div>
|
|
490
|
+
</section>
|
|
491
|
+
<section id="page-features" aria-labelledby="title-features" hidden>
|
|
492
|
+
<div class="page-head"><div><h1 id="title-features" tabindex="-1">Features</h1><p class="muted">Control how Shadow Brain assembles context and updates the workspace.</p></div></div>
|
|
493
|
+
<div class="stack settings">
|
|
494
|
+
<div class="panel"><div class="panel-head"><h2>Workspace features</h2></div><div id="feature-list" aria-busy="true"></div><p class="save-status small muted" role="status" id="feature-status"></p></div>
|
|
495
|
+
<div class="panel">
|
|
496
|
+
<div class="panel-head"><h2>Display</h2></div>
|
|
497
|
+
<ul class="list">
|
|
498
|
+
<li class="setting"><div><h3><label for="theme-setting">Theme</label></h3><p class="small muted">Choose the workspace color scheme on this device.</p></div><select id="theme-setting"><option value="light">Light</option><option value="dark">Dark</option></select></li>
|
|
499
|
+
<li class="setting"><div><h3><label for="motion-setting">Motion</label></h3><p class="small muted">Follow your system preference or reduce interface motion.</p></div><select id="motion-setting"><option value="system">System</option><option value="reduce">Reduced</option></select></li>
|
|
500
|
+
</ul>
|
|
501
|
+
<p id="display-status" class="save-status small muted" role="status" hidden></p>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</section>
|
|
505
|
+
</main>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
<script>
|
|
509
|
+
'use strict';
|
|
510
|
+
|
|
511
|
+
const glyphs = {
|
|
512
|
+
brand: '<path d="M12 5 5 9v6l7 4 7-4V9l-7-4v7m-7-3 7 3 7-3m-7 3v7"/><circle cx="12" cy="5" r="1.4" fill="currentColor"/><circle cx="12" cy="12" r="1.4" fill="currentColor"/><circle cx="12" cy="19" r="1.4" fill="currentColor"/>',
|
|
513
|
+
overview: '<rect x="3.5" y="4" width="17" height="16" rx="2"/><path d="M3.5 9.5h17m-11 0V20"/>',
|
|
514
|
+
memory: '<path d="M4 7.5h16M6 7.5v11a1.5 1.5 0 0 0 1.5 1.5h9a1.5 1.5 0 0 0 1.5-1.5v-11M9 12h6"/><rect x="3" y="4" width="18" height="4" rx="1"/>',
|
|
515
|
+
graph: '<path d="m6 6 6 6m0 0 6-6m-6 6v6"/><circle cx="5" cy="5" r="2"/><circle cx="19" cy="5" r="2"/><circle cx="12" cy="19" r="2"/><circle cx="12" cy="12" r="1.6" fill="currentColor"/>',
|
|
516
|
+
context: '<path d="M9 8h6m-6 4h6m-6 4h4M6 3h9l4 4v14H6zM15 3v5h4"/>',
|
|
517
|
+
settings: '<path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="2.4" fill="var(--icon-bg, var(--rail))"/><circle cx="15" cy="17" r="2.4" fill="var(--icon-bg, var(--rail))"/>',
|
|
518
|
+
advanced: '<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="m7.5 9 3 3-3 3m5.5 0h3.5"/>',
|
|
519
|
+
diagonal: '<path d="M6 18 18 6M8 6h10v10"/>',
|
|
520
|
+
chevron: '<path d="m9 6 6 6-6 6"/>',
|
|
521
|
+
left: '<path d="m14 6-6 6 6 6"/>',
|
|
522
|
+
moon: '<path d="M20 15a8.5 8.5 0 0 1-11-11 8.5 8.5 0 1 0 11 11Z"/>',
|
|
523
|
+
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19m0-14-1.5 1.5m-11 11L5 19"/>',
|
|
524
|
+
project: '<path d="M3.5 7V6A1.5 1.5 0 0 1 5 4.5h4l2 2h8A1.5 1.5 0 0 1 20.5 8v10A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18Z"/>',
|
|
525
|
+
agent: '<rect x="4.5" y="7" width="15" height="12" rx="3"/><path d="M12 7V4m-8 9H2m20 0h-2m-11-.5h.02m5.98 0H15m-5 3h4"/>',
|
|
526
|
+
activity: '<path d="M3 12h4l3-7 4 14 3-7h4"/>',
|
|
527
|
+
integrations: '<path d="M9 3v4m6-4v4m-8 0h10v4a5 5 0 0 1-5 5h0a5 5 0 0 1-5-5zm5 9v5"/>',
|
|
528
|
+
search: '<circle cx="10.5" cy="10.5" r="6"/><path d="m15 15 5 5"/>',
|
|
529
|
+
source: '<path d="M9.5 14.5 14.5 9m-6 8.5-1 1a3.5 3.5 0 0 1-5-5L7 9a3.5 3.5 0 0 1 5 0m3.5-2.5 1-1a3.5 3.5 0 0 1 5 5L17 15a3.5 3.5 0 0 1-5 0"/>',
|
|
530
|
+
list: '<path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01"/>',
|
|
531
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
532
|
+
minus: '<path d="M5 12h14"/>',
|
|
533
|
+
copy: '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>',
|
|
534
|
+
warning: '<path d="m12 3 10 17H2L12 3zm0 6v4m0 3h.01"/>',
|
|
535
|
+
refresh: '<path d="M19.5 9a8 8 0 1 0 .3 5.5M20 4v5h-5"/>'
|
|
536
|
+
};
|
|
537
|
+
const viewNames = { overview: 'Overview', memories: 'Memories', memory: 'Memory detail', graph: 'Graph', context: 'Context', integrations: 'Integrations', features: 'Features' };
|
|
538
|
+
const reviewStates = { active: 'Active', contested: 'Needs review', archived: 'Archived', superseded: 'Superseded' };
|
|
539
|
+
const linkTypes = { related: 'Related', supports: 'Supports', contradicts: 'Contradicts', supersedes: 'Supersedes' };
|
|
540
|
+
const definitions = [
|
|
541
|
+
{ id: 'contextAssembly', label: 'Context assembly', description: 'Build a task-specific brief from saved project memories.' },
|
|
542
|
+
{ id: 'crossProjectContext', label: 'Cross-project context', description: 'Allow context assembly to include eligible memories from other projects when requested.' },
|
|
543
|
+
{ id: 'automaticBriefing', label: 'Automatic briefing', description: 'Include a context brief when a supported agent starts work in a project.' },
|
|
544
|
+
{ id: 'liveActivity', label: 'Live activity', description: 'Refresh the workspace activity feed automatically.' },
|
|
545
|
+
{ id: 'activityRecording', label: 'Record agent activity', description: 'Record bounded tool timing and explicitly reported session phases.' }
|
|
546
|
+
];
|
|
547
|
+
const $ = (id) => document.getElementById(id);
|
|
548
|
+
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
549
|
+
const icon = (name, small = false) => '<svg class="icon' + (small ? ' small-icon' : '') + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (glyphs[name] || '') + '</svg>';
|
|
550
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
551
|
+
const compact = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
552
|
+
const shorten = (value, limit) => value.length > limit ? value.slice(0, limit - 1) + '…' : value;
|
|
553
|
+
const number = (value) => Number(value || 0).toLocaleString();
|
|
554
|
+
const countLabel = (value, singular, plural = singular + 's') => number(value) + ' ' + (Number(value) === 1 ? singular : plural);
|
|
555
|
+
const nice = (value) => String(value ?? '').replace(/[-_]/g, ' ').replace(/^./, (char) => char.toUpperCase());
|
|
556
|
+
const tags = (items) => '<div class="tags">' + items.filter(([, value]) => value).map(([kind, value]) =>
|
|
557
|
+
'<span class="tag ' + esc(kind) + '">' + esc(value) + '</span>').join('') + '</div>';
|
|
558
|
+
const DATE = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
559
|
+
const TIME = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
560
|
+
const state = {
|
|
561
|
+
view: '', route: '', projects: [], overviewLoaded: false, overviewPending: null, overviewScope: '', overviewRequest: 0, overviewRequestScope: '',
|
|
562
|
+
activity: [], activityEnabled: null, activityPending: false, features: null, featurePending: null, featureSaving: false,
|
|
563
|
+
memory: { query: null, request: 0, controller: null, limit: 20, offset: 0 },
|
|
564
|
+
detail: { snapshot: null, id: '', project: '', back: '#memories', request: 0, targetRequest: 0, saving: false },
|
|
565
|
+
graph: { snapshot: null, project: null, mode: matchMedia('(max-width: 690px)').matches ? 'list' : 'map', selected: '', selectedEdge: '', focusMemory: '', nodes: [], edges: [], layout: null, zoom: 1, panX: 0, panY: 0, request: 0, controller: null, drag: null },
|
|
566
|
+
context: { pack: null, pending: false, dirty: false, routeProject: '' },
|
|
567
|
+
integrations: { snapshot: null, pending: false }
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
document.querySelectorAll('[data-icon]').forEach((element) => { element.innerHTML = icon(element.dataset.icon); });
|
|
571
|
+
|
|
572
|
+
function dateMarkup(value, full = false) {
|
|
573
|
+
if (!value) return '';
|
|
574
|
+
const date = new Date(value);
|
|
575
|
+
if (!Number.isFinite(date.getTime())) return '';
|
|
576
|
+
const today = new Date();
|
|
577
|
+
const dateText = full || date.getFullYear() !== today.getFullYear() ? DATE.format(date) : TIME.format(date);
|
|
578
|
+
return '<time class="small muted" datetime="' + esc(date.toISOString()) + '" title="' + esc(date.toLocaleString()) + '">' + esc(dateText) + '</time>';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function blank(iconName, title, detail, action = '') {
|
|
582
|
+
return '<div class="blank">' + icon(iconName) + '<h3>' + esc(title) + '</h3><p class="small muted">' + esc(detail) + '</p>' + action + '</div>';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function skeleton(label) {
|
|
586
|
+
return '<div class="skeleton" role="status"><span class="muted small">' + esc(label) + '</span><i></i><i></i><i></i></div>';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function errorMarkup(message, retry) {
|
|
590
|
+
return '<div class="status error" role="alert">' + icon('warning') + '<span>' + esc(message) + '</span>' +
|
|
591
|
+
(retry ? '<button class="button quiet" data-retry="' + retry + '" type="button">Try again</button>' : '') + '</div>';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function setBusy(element, isBusy) {
|
|
595
|
+
element.setAttribute('aria-busy', String(isBusy));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function connection(value) {
|
|
599
|
+
$('connection').dataset.state = value;
|
|
600
|
+
$('connection-label').textContent = value === 'connected' ? 'Local server' : value === 'error' ? 'Disconnected' : 'Connecting';
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function api(path, { body, signal } = {}) {
|
|
604
|
+
let response;
|
|
605
|
+
try {
|
|
606
|
+
response = await fetch('/api/workspace/' + path, {
|
|
607
|
+
headers: body ? { Accept: 'application/json', 'Content-Type': 'application/json' } : { Accept: 'application/json' },
|
|
608
|
+
method: body ? 'POST' : 'GET',
|
|
609
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
610
|
+
cache: 'no-store',
|
|
611
|
+
signal: signal || AbortSignal.timeout(12000)
|
|
612
|
+
});
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error.name === 'AbortError') throw error;
|
|
615
|
+
connection('error');
|
|
616
|
+
throw new Error('Could not reach the local Shadow Brain server.');
|
|
617
|
+
}
|
|
618
|
+
connection('connected');
|
|
619
|
+
const data = await response.json().catch(() => null);
|
|
620
|
+
if (!response.ok || !data) {
|
|
621
|
+
const error = new Error(response.ok ? 'The server returned an unreadable response.' : data?.error || 'The server could not complete this request.');
|
|
622
|
+
error.status = response.status;
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
return data;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function optionSet(id, placeholder, values, preferred) {
|
|
629
|
+
const select = $(id);
|
|
630
|
+
const previous = preferred ?? select.value;
|
|
631
|
+
const choices = values.filter((item) => item && item.id);
|
|
632
|
+
const signature = JSON.stringify(choices);
|
|
633
|
+
if (select.dataset.signature !== signature) {
|
|
634
|
+
select.replaceChildren(new Option(placeholder, ''), ...choices.map((item) => new Option(item.name || item.id, item.id)));
|
|
635
|
+
select.dataset.signature = signature;
|
|
636
|
+
}
|
|
637
|
+
if (previous && Array.from(select.options).some((option) => option.value === previous)) select.value = previous;
|
|
638
|
+
else select.value = '';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function queryPath(name, params) {
|
|
642
|
+
const search = new URLSearchParams();
|
|
643
|
+
Object.entries(params).forEach(([key, value]) => { if (value !== '' && value != null) search.set(key, String(value)); });
|
|
644
|
+
return name + (search.size ? '?' + search : '');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function routeLink(view, params) {
|
|
648
|
+
return '#' + queryPath(view, params);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function memoryLink(id, project, label, className = 'record-link') {
|
|
652
|
+
if (!id || !project) return esc(label);
|
|
653
|
+
return '<a class="' + className + '" href="' + esc(routeLink('memory', { id, project })) + '">' + esc(label) + '</a>';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function reviewLabel(value) {
|
|
657
|
+
return Object.hasOwn(reviewStates, value) ? reviewStates[value] : '';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function reviewTag(value) {
|
|
661
|
+
const label = reviewLabel(value);
|
|
662
|
+
return label ? '<span class="tag state-' + value + '">' + label + '</span>' : '';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderSummary(totals) {
|
|
666
|
+
return '<dl class="summary">' + [
|
|
667
|
+
['memory', 'Stored memories', totals.memories], ['project', 'Projects', totals.projects], ['agent', 'Contributing agents', totals.agents]
|
|
668
|
+
].map(([image, label, value]) => '<div class="summary-item"><span class="tile-icon">' + icon(image) +
|
|
669
|
+
'</span><div><dt>' + label + '</dt><dd>' + number(value) + '</dd></div></div>').join('') + '</dl>';
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function renderProjects(projects) {
|
|
673
|
+
if (!projects.length) return blank('project', 'No projects yet', 'Projects appear after an agent stores a memory with a project scope.');
|
|
674
|
+
const max = Math.max(1, ...projects.map((project) => Number(project.memories) || 0));
|
|
675
|
+
return '<ul class="list">' + [...projects].sort((a, b) => b.memories - a.memories).slice(0, 8).map((project) => {
|
|
676
|
+
const size = Math.max(0, Math.min(100, Math.round((Number(project.memories) || 0) / max * 100)));
|
|
677
|
+
const href = routeLink('memories', { project: project.id });
|
|
678
|
+
return '<li class="project-row"><div><a class="project-name" href="' + esc(href) + '">' + icon('project', true) +
|
|
679
|
+
'<span>' + esc(project.name || project.id) + '</span></a>' +
|
|
680
|
+
(project.updatedAt ? '<div class="project-detail">' + dateMarkup(project.updatedAt, true) + '</div>' : '') +
|
|
681
|
+
'</div><div class="project-count">' + countLabel(project.memories, 'memory', 'memories') +
|
|
682
|
+
'<span class="project-meter" aria-hidden="true"><span style="width:' + size + '%"></span></span></div>' +
|
|
683
|
+
'<a class="button icon-button quiet" aria-label="' + esc('View memories for ' + (project.name || project.id)) + '" href="' + esc(href) + '">' + icon('chevron') + '</a></li>';
|
|
684
|
+
}).join('') + '</ul>';
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function renderOverviewMemories(memories) {
|
|
688
|
+
if (!memories.length) return '';
|
|
689
|
+
return '<div class="panel"><div class="panel-head"><h2>Latest memories</h2><a class="button quiet" href="#memories">Browse ' + icon('chevron', true) +
|
|
690
|
+
'</a></div><ul class="list">' + memories.map((item) => '<li class="overview-memory"><div class="memory-head"><strong>' +
|
|
691
|
+
memoryLink(item.id, item.project, item.title || nice(item.category) || 'Memory') + '</strong>' + dateMarkup(item.updatedAt, true) + '</div>' +
|
|
692
|
+
tags([['project', item.projectName || item.project], ['agent', item.agent], ['memory', nice(item.category)], ['state-' + (item.effectiveState || ''), reviewLabel(item.effectiveState)]]) + '</li>').join('') + '</ul></div>';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function renderQuality(data) {
|
|
696
|
+
const quality = data.quality;
|
|
697
|
+
if (!quality || !Number.isFinite(quality.total)) return '';
|
|
698
|
+
const project = data.scope?.projectId || '';
|
|
699
|
+
const scope = data.truncated ? 'Based on the latest ' + number(data.inventoryLimit) + ' stored memories.' :
|
|
700
|
+
project ? 'Stored memories in this project.' : 'Stored memories across projects.';
|
|
701
|
+
const options = state.projects.map((item) => '<option value="' + esc(item.id) + '"' +
|
|
702
|
+
(item.id === project ? ' selected' : '') + '>' + esc(item.name || item.id) + '</option>').join('');
|
|
703
|
+
const statusCounts = Object.entries(reviewStates).map(([key, label]) => ({ key, label, count: Number(quality[key]) || 0 }));
|
|
704
|
+
const coverage = quality.total ? Math.max(0, Math.min(100, Number(quality.sourceBacked || 0) / quality.total * 100)) : 0;
|
|
705
|
+
const statusBars = statusCounts.map(({ key, count }) =>
|
|
706
|
+
'<span class="quality-' + key + '" style="width:' + Math.max(0, Math.min(100, count / quality.total * 100)) + '%"></span>').join('');
|
|
707
|
+
const statusLinks = statusCounts.map(({ key, label, count }) => '<a href="' + esc(routeLink('memories', { project, state: key })) +
|
|
708
|
+
'"><i aria-hidden="true" class="quality-' + key + '"></i>' + esc(label) + ' <strong>' + number(count) + '</strong></a>').join('');
|
|
709
|
+
return '<div class="panel"><div class="panel-head"><div><h2>Review and sources</h2><p class="small muted">' + esc(scope) +
|
|
710
|
+
'</p></div><label class="label" for="overview-quality-project">Project<select id="overview-quality-project"><option value="">All projects</option>' +
|
|
711
|
+
options + '</select></label></div>' + (quality.total ? '<div class="panel-body stack"><div class="stack" style="gap:8px"><h3>Memory status</h3>' +
|
|
712
|
+
'<div class="quality-track" aria-hidden="true">' + statusBars + '</div><div class="quality-labels">' + statusLinks + '</div></div>' +
|
|
713
|
+
'<div class="stack" style="gap:8px"><div class="quality-source"><h3>Recorded sources</h3><span class="small muted">' +
|
|
714
|
+
number(quality.sourceBacked) + ' of ' + number(quality.total) + ' memories</span></div>' +
|
|
715
|
+
'<div class="quality-track" aria-hidden="true"><span class="quality-active" style="width:' + coverage + '%"></span></div></div>' +
|
|
716
|
+
(Number.isFinite(quality.reviewed) ? '<p class="small muted">' + number(quality.reviewed) + ' with a recorded review</p>' : '') +
|
|
717
|
+
'</div>' : blank('memory', 'No memories in scope', 'Save a memory to see review and source counts here.')) + '</div>';
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function renderActivity() {
|
|
721
|
+
const target = $('activity-body');
|
|
722
|
+
if (!target) return;
|
|
723
|
+
const enabled = state.activityEnabled;
|
|
724
|
+
$('activity-status').textContent = enabled === true ? 'Live' : enabled === false ? 'Paused' : '';
|
|
725
|
+
if (enabled === false) {
|
|
726
|
+
target.innerHTML = blank('activity', 'Live activity is paused', 'Turn it back on in Features to see recent events.',
|
|
727
|
+
'<a class="button quiet" href="#features">Open Features ' + icon('chevron', true) + '</a>');
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (!state.activity.length) {
|
|
731
|
+
target.innerHTML = blank('activity', 'No recent activity', 'Memory and agent events appear here as work happens.');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
target.innerHTML = '<ul class="list">' + state.activity.slice(0, 8).map((event) => {
|
|
735
|
+
const image = String(event.type ?? '').includes('memory') ? 'memory' : 'activity';
|
|
736
|
+
const meta = [event.agent, event.project].filter(Boolean).join(' · ');
|
|
737
|
+
return '<li class="event"><span class="tile-icon">' + icon(image, true) + '</span><div><strong>' +
|
|
738
|
+
esc(event.label || nice(event.type) || 'Activity') + '</strong>' +
|
|
739
|
+
(meta ? '<p class="event-meta small muted">' + esc(meta) + '</p>' : '') + '</div>' + dateMarkup(event.timestamp) + '</li>';
|
|
740
|
+
}).join('') + '</ul>';
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function renderOverview(data) {
|
|
744
|
+
const sample = data.truncated ? '<p class="status small muted notice">Project and agent counts reflect the latest ' +
|
|
745
|
+
number(data.inventoryLimit) + ' stored memories.</p>' : '';
|
|
746
|
+
const pending = data.storageHealth?.status === 'pending' ? '<p class="status small muted notice">The memory store has pending writes.</p>' : '';
|
|
747
|
+
const projectNote = state.projects.length > 8 ? 'Top projects by saved memory count' : 'Saved memories by project';
|
|
748
|
+
$('overview-body').innerHTML = '<div id="overview-refresh"></div>' + pending + sample + renderSummary(data.totals || {}) +
|
|
749
|
+
'<div class="split overview-columns"><div class="stack">' + renderQuality(data) + '<div class="panel"><div class="panel-head"><div><h2>Project inventory</h2>' +
|
|
750
|
+
'<p class="muted small">' + projectNote + '</p></div><a class="button quiet" href="#memories">Browse ' + icon('chevron', true) +
|
|
751
|
+
'</a></div>' + renderProjects(state.projects) + '</div>' + renderOverviewMemories(list(data.recentMemories)) +
|
|
752
|
+
'</div><div class="panel"><div class="panel-head"><h2>Recent activity</h2><span id="activity-status" class="small muted"></span></div><div id="activity-body"></div></div></div>';
|
|
753
|
+
renderActivity();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function syncProjectOptions() {
|
|
757
|
+
const contextProject = state.context.routeProject || $('context-project').value;
|
|
758
|
+
const graphProject = state.view === 'graph' ? routeParams().get('project') : null;
|
|
759
|
+
optionSet('context-project', 'Select a project', state.projects, contextProject);
|
|
760
|
+
if ($('context-project').value === state.context.routeProject) state.context.routeProject = '';
|
|
761
|
+
optionSet('graph-project', 'All projects', state.projects, $('graph-project').value || graphProject);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function loadOverview(force = false, project = '') {
|
|
765
|
+
if (state.overviewPending && state.overviewRequestScope === project) return state.overviewPending;
|
|
766
|
+
if (state.overviewLoaded && !force && state.overviewScope === project) return;
|
|
767
|
+
const request = ++state.overviewRequest;
|
|
768
|
+
state.overviewRequestScope = project;
|
|
769
|
+
const target = $('overview-body');
|
|
770
|
+
if (!state.overviewLoaded) target.innerHTML = '<div class="panel">' + skeleton('Loading workspace…') + '</div>';
|
|
771
|
+
setBusy(target, true);
|
|
772
|
+
state.overviewPending = (async () => {
|
|
773
|
+
try {
|
|
774
|
+
const data = await api(queryPath('overview', { project }));
|
|
775
|
+
if (request !== state.overviewRequest) return;
|
|
776
|
+
state.overviewLoaded = true;
|
|
777
|
+
state.overviewScope = project;
|
|
778
|
+
state.projects = list(data.projects);
|
|
779
|
+
state.activity = list(data.activity);
|
|
780
|
+
if (typeof data.features?.liveActivity === 'boolean') state.activityEnabled = data.features.liveActivity;
|
|
781
|
+
if (data.version) $('rail-version').textContent = 'Version ' + data.version;
|
|
782
|
+
syncProjectOptions();
|
|
783
|
+
renderOverview(data);
|
|
784
|
+
renderContextControls();
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (request !== state.overviewRequest) return;
|
|
787
|
+
if (!state.overviewLoaded) target.innerHTML = errorMarkup(error.message, 'overview');
|
|
788
|
+
else if ($('overview-refresh')) $('overview-refresh').innerHTML = '<div class="notice">' + errorMarkup('Could not refresh the workspace. Showing the last loaded overview.', 'overview') + '</div>';
|
|
789
|
+
renderContextControls();
|
|
790
|
+
} finally {
|
|
791
|
+
if (request === state.overviewRequest) {
|
|
792
|
+
setBusy(target, false);
|
|
793
|
+
state.overviewPending = null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
})();
|
|
797
|
+
return state.overviewPending;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async function refreshActivity() {
|
|
801
|
+
if (document.hidden || state.view !== 'overview' || state.activityEnabled === false || state.activityPending || !state.overviewLoaded) return;
|
|
802
|
+
state.activityPending = true;
|
|
803
|
+
try {
|
|
804
|
+
const data = await api('activity');
|
|
805
|
+
if (state.features?.features?.liveActivity === false) return;
|
|
806
|
+
state.activityEnabled = data.enabled;
|
|
807
|
+
state.activity = list(data.activity);
|
|
808
|
+
renderActivity();
|
|
809
|
+
} catch {
|
|
810
|
+
const status = $('activity-status');
|
|
811
|
+
if (status) status.textContent = 'Refresh unavailable';
|
|
812
|
+
} finally {
|
|
813
|
+
state.activityPending = false;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function sourceMarkup(values) {
|
|
818
|
+
const sources = Array.from(new Set(list(values).filter((source) => typeof source === 'string' && source.trim())));
|
|
819
|
+
if (!sources.length) return '<p class="source small">' + icon('source', true) + '<span>No source recorded</span></p>';
|
|
820
|
+
return sources.map((source) => '<p class="source small">' + icon('source', true) + '<span class="mono">' + esc(source) + '</span></p>').join('');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function memoryQuery() {
|
|
824
|
+
return {
|
|
825
|
+
q: $('memory-query').value.trim(),
|
|
826
|
+
project: $('memory-project').value,
|
|
827
|
+
category: $('memory-category').value,
|
|
828
|
+
agent: $('memory-agent').value,
|
|
829
|
+
state: $('memory-state').value
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function renderMemories(data) {
|
|
834
|
+
const target = $('memory-results');
|
|
835
|
+
const items = list(data.items);
|
|
836
|
+
const total = Number.isFinite(data.total) ? data.total : items.length;
|
|
837
|
+
const { offset } = state.memory;
|
|
838
|
+
const activeFilters = Object.values(state.memory.query || {}).some(Boolean);
|
|
839
|
+
const subtitle = state.memory.query?.q ? 'Ranked search results' : 'Most recent first';
|
|
840
|
+
const heading = '<div class="panel-head"><div><h2>' + esc(activeFilters ? 'Results' : 'Recent memories') +
|
|
841
|
+
'</h2><p class="small muted">' + subtitle + '</p></div><span class="small muted" role="status">' + countLabel(total, 'memory', 'memories') + '</span></div>' +
|
|
842
|
+
(data.truncated ? '<p class="panel-body small muted">Filters and counts may be limited to ' + number(data.inventoryLimit) + ' memories.</p>' : '');
|
|
843
|
+
if (!items.length) {
|
|
844
|
+
target.innerHTML = heading + (activeFilters
|
|
845
|
+
? blank('search', 'No matching memories', 'Try a different query or clear one of the filters.', '<button class="button quiet" data-memory-clear type="button">Clear filters</button>')
|
|
846
|
+
: blank('memory', 'No memories yet', 'Saved memories appear here after an agent stores a lesson or observation.'));
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
target.innerHTML = heading + '<ul class="list">' + items.map((item) => {
|
|
850
|
+
const title = item.title || nice(item.category) || 'Memory';
|
|
851
|
+
const reasons = list(item.reasons);
|
|
852
|
+
const detail = reasons.length ? '<details><summary>Why this matched</summary><ul class="small muted">' +
|
|
853
|
+
reasons.map((reason) => '<li>' + esc(reason) + '</li>').join('') + '</ul></details>' : '';
|
|
854
|
+
const snippet = compact(item.snippet) && compact(item.snippet) !== compact(title) ? '<p>' + esc(item.snippet) + '</p>' : '';
|
|
855
|
+
return '<li class="memory-row"><div class="memory-head"><h3>' + memoryLink(item.id, item.project, title) + '</h3>' + dateMarkup(item.updatedAt, true) + '</div>' +
|
|
856
|
+
tags([['project', item.projectName || item.project], ['agent', item.agent], ['memory', nice(item.category)], ['state-' + (item.effectiveState || ''), reviewLabel(item.effectiveState)]]) + snippet +
|
|
857
|
+
sourceMarkup([...list(item.sources), item.source]) + detail + '</li>';
|
|
858
|
+
}).join('') + '</ul>' + (offset || total > items.length ? '<div class="list-footer"><span class="muted small">' +
|
|
859
|
+
number(offset + 1) + '–' + number(offset + items.length) + ' of ' + number(total) + '</span><div class="form-actions">' +
|
|
860
|
+
'<button type="button" class="button" data-memory-page="prev"' + (offset ? '' : ' disabled') + '>' + icon('left', true) +
|
|
861
|
+
'Previous</button><button type="button" class="button" data-memory-page="next"' + (offset + items.length < total ? '' : ' disabled') +
|
|
862
|
+
'>Next' + icon('chevron', true) + '</button></div></div>' : '');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function loadMemories(query, offset = 0) {
|
|
866
|
+
const target = $('memory-results');
|
|
867
|
+
const store = state.memory;
|
|
868
|
+
const seq = ++store.request;
|
|
869
|
+
store.controller?.abort();
|
|
870
|
+
const controller = new AbortController();
|
|
871
|
+
store.controller = controller;
|
|
872
|
+
store.query = query;
|
|
873
|
+
store.offset = offset;
|
|
874
|
+
setBusy(target, true);
|
|
875
|
+
target.innerHTML = skeleton('Loading memories…');
|
|
876
|
+
try {
|
|
877
|
+
const data = await api(queryPath('memories', { ...query, limit: store.limit, offset }), {
|
|
878
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(12000)])
|
|
879
|
+
});
|
|
880
|
+
if (seq !== store.request) return;
|
|
881
|
+
const facets = data.facets || {};
|
|
882
|
+
const toOptions = (values) => list(values).map((value) => ({ id: value, name: nice(value) }));
|
|
883
|
+
const projects = list(facets.projects);
|
|
884
|
+
optionSet('memory-project', 'All projects', projects.length ? projects : state.projects, query.project);
|
|
885
|
+
optionSet('memory-category', 'All categories', toOptions(facets.categories), query.category);
|
|
886
|
+
optionSet('memory-agent', 'All agents', toOptions(facets.agents), query.agent);
|
|
887
|
+
renderMemories(data);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (seq === store.request && error.name !== 'AbortError') target.innerHTML = errorMarkup(error.message, 'memories');
|
|
890
|
+
} finally {
|
|
891
|
+
if (seq === store.request) setBusy(target, false);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function linkCaption(link, memoryId) {
|
|
896
|
+
if (link.type === 'related') return 'Related to';
|
|
897
|
+
if (link.fromId === memoryId) return linkTypes[link.type] || nice(link.type);
|
|
898
|
+
return { supports: 'Supported by', contradicts: 'Contradicted by', supersedes: 'Superseded by' }[link.type] || nice(link.type);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function renderReviewHistory(events) {
|
|
902
|
+
const recent = [...list(events)].sort((a, b) => Number(b.revision) - Number(a.revision)).slice(0, 12);
|
|
903
|
+
if (!recent.length) return '';
|
|
904
|
+
const rows = recent.map((event) => {
|
|
905
|
+
const command = event.command || {};
|
|
906
|
+
const label = command.kind === 'review' ? 'Recorded review · ' + reviewLabel(command.state) :
|
|
907
|
+
command.kind === 'link' ? 'Added link · ' + (linkTypes[command.type] || nice(command.type)) :
|
|
908
|
+
command.kind === 'unlink' ? 'Removed link' : 'Memory change';
|
|
909
|
+
return '<li class="detail-row"><div class="memory-head"><strong>' + esc(label) + '</strong>' + dateMarkup(event.at) + '</div>' +
|
|
910
|
+
(command.note ? '<p class="detail-note small muted">' + esc(command.note) + '</p>' : '') + '</li>';
|
|
911
|
+
}).join('');
|
|
912
|
+
return '<section class="panel"><div class="panel-head"><h2>Recent changes</h2></div><ul class="list">' + rows + '</ul></section>';
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function renderDetail(snapshot) {
|
|
916
|
+
const { memory, review, links } = snapshot;
|
|
917
|
+
const project = memory.project || memory.projectId || state.detail.project;
|
|
918
|
+
const effective = snapshot.effectiveState || memory.effectiveState || review?.state;
|
|
919
|
+
const title = memory.title || nice(memory.category) || 'Memory detail';
|
|
920
|
+
const content = memory.content ?? memory.snippet;
|
|
921
|
+
const ownState = review?.state || 'active';
|
|
922
|
+
const reviewOptions = Object.entries(reviewStates).filter(([key]) => key !== 'superseded')
|
|
923
|
+
.map(([key, label]) => '<option value="' + key + '"' + (key === ownState ? ' selected' : '') + '>' + label + '</option>').join('');
|
|
924
|
+
const relationOptions = Object.entries(linkTypes).map(([key, label]) => '<option value="' + key + '">' + label + '</option>').join('');
|
|
925
|
+
const replacement = list(links).find((link) => link.type === 'supersedes' && link.toId === memory.id);
|
|
926
|
+
const titleEl = $('title-memory');
|
|
927
|
+
titleEl.textContent = title;
|
|
928
|
+
$('detail-meta').innerHTML = tags([
|
|
929
|
+
['project', memory.projectName || project], ['agent', memory.agent], ['memory', nice(memory.category)],
|
|
930
|
+
['state-' + (effective || ''), reviewLabel(effective)]
|
|
931
|
+
]) + (memory.createdAt || memory.updatedAt ? '<div class="small muted">' + dateMarkup(memory.createdAt || memory.updatedAt, true) + '</div>' : '');
|
|
932
|
+
$('detail-graph').href = routeLink('graph', { project, memory: memory.id });
|
|
933
|
+
$('detail-context').href = routeLink('context', { project });
|
|
934
|
+
$('detail-actions').hidden = false;
|
|
935
|
+
const linkRows = list(links).map((link) => {
|
|
936
|
+
const outgoing = link.fromId === memory.id;
|
|
937
|
+
const endpoint = outgoing ? link.toId : link.fromId;
|
|
938
|
+
const endpointTitle = (outgoing ? link.toTitle : link.fromTitle) || endpoint;
|
|
939
|
+
const endpointLink = link.orphaned ? esc(endpointTitle) : memoryLink(endpoint, project, endpointTitle);
|
|
940
|
+
return '<li class="detail-row"><div class="tags"><span class="tag memory">' + esc(linkCaption(link, memory.id)) + '</span>' +
|
|
941
|
+
endpointLink + '</div>' +
|
|
942
|
+
(link.note ? '<p class="detail-note small muted">' + esc(link.note) + '</p>' : '') +
|
|
943
|
+
'<details><summary class="small muted">Remove link</summary><div class="form-actions"><button class="button" type="button" data-unlink="' +
|
|
944
|
+
esc(link.id) + '">Remove this link</button></div></details></li>';
|
|
945
|
+
}).join('');
|
|
946
|
+
const warnings = list(snapshot.warnings).map((warning) => '<p class="status small muted notice">' + esc(warning) + '</p>').join('');
|
|
947
|
+
$('detail-body').innerHTML = warnings + '<div class="split detail-layout"><div class="stack">' +
|
|
948
|
+
'<section class="panel"><div class="panel-head"><h2>Saved memory</h2></div><div class="panel-body">' +
|
|
949
|
+
'<p class="memory-content">' + esc(content || 'No saved text.') + '</p></div></section>' +
|
|
950
|
+
'<section class="panel"><div class="panel-head"><h2>Recorded sources</h2></div><div class="panel-body detail-sources">' +
|
|
951
|
+
sourceMarkup([...list(memory.sources), ...list(snapshot.sources), memory.source]) + '</div></section>' +
|
|
952
|
+
'<section class="panel"><div class="panel-head"><h2>Connections</h2><span class="small muted">' + countLabel(list(links).length, 'link') +
|
|
953
|
+
'</span></div>' + (linkRows ? '<ul class="list">' + linkRows + '</ul>' :
|
|
954
|
+
'<div class="panel-body"><p class="small muted">No links recorded for this memory.</p></div>') +
|
|
955
|
+
'<div class="panel-body"><details id="link-editor"><summary>Add a memory link</summary><div class="detail-form" style="margin-top:10px">' +
|
|
956
|
+
'<form id="link-search" class="link-search"><label class="label" for="link-query">Find a memory in this project<input id="link-query" type="search" maxlength="500" placeholder="Search by keyword"></label>' +
|
|
957
|
+
'<button type="submit" class="button">Find</button></form>' +
|
|
958
|
+
'<form id="link-form" class="detail-form"><label class="label" for="link-target">Target memory<select id="link-target" required><option value="">Choose a memory</option></select></label>' +
|
|
959
|
+
'<p id="link-search-status" class="small muted" role="status"></p>' +
|
|
960
|
+
'<label class="label" for="link-type">Relationship from this memory<select id="link-type">' + relationOptions + '</select></label>' +
|
|
961
|
+
'<label class="label" for="link-note">Note<textarea id="link-note" maxlength="1000" rows="2" placeholder="Describe the relationship"></textarea></label>' +
|
|
962
|
+
'<div class="form-actions"><button class="button primary" id="link-submit" type="submit">Save link</button></div></form></div></details></div>' +
|
|
963
|
+
'<p id="link-edit-status" class="save-status small muted" role="status"></p></section>' +
|
|
964
|
+
'</div><div class="stack"><section class="panel"><div class="panel-head"><h2>Recorded review</h2>' + reviewTag(effective) + '</div>' +
|
|
965
|
+
'<div class="panel-body detail-form">' +
|
|
966
|
+
(effective === 'superseded' ? '<p class="small muted">This memory is superseded' +
|
|
967
|
+
(replacement?.fromId && !replacement.orphaned ? ' by ' + memoryLink(replacement.fromId, project, replacement.fromTitle || replacement.fromId) : '') + '.</p>' : '') +
|
|
968
|
+
(review?.updatedAt ? '<div class="small muted">Reviewed ' + dateMarkup(review.updatedAt, true) + '</div>' :
|
|
969
|
+
'<p class="small muted">No review recorded.</p>') +
|
|
970
|
+
'<form id="review-form" class="detail-form"><label class="label" for="review-state">Manual status<select id="review-state">' +
|
|
971
|
+
reviewOptions + '</select></label><label class="label" for="review-note">Review note<textarea id="review-note" maxlength="1000" rows="3" placeholder="Record what you checked">' +
|
|
972
|
+
esc(review?.note) + '</textarea></label><div class="form-actions"><button class="button primary" id="review-submit" type="submit">Save review</button></div></form>' +
|
|
973
|
+
'</div><p id="detail-status" class="save-status small muted" role="status"></p></section>' +
|
|
974
|
+
renderReviewHistory(snapshot.history) + '</div></div>';
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function loadDetail(id, project, failurePrefix = '') {
|
|
978
|
+
const store = state.detail;
|
|
979
|
+
const request = ++store.request;
|
|
980
|
+
store.id = id;
|
|
981
|
+
store.project = project;
|
|
982
|
+
store.snapshot = null;
|
|
983
|
+
$('detail-actions').hidden = true;
|
|
984
|
+
$('title-memory').textContent = 'Memory detail';
|
|
985
|
+
$('detail-meta').replaceChildren();
|
|
986
|
+
const target = $('detail-body');
|
|
987
|
+
setBusy(target, true);
|
|
988
|
+
target.innerHTML = skeleton('Loading memory…');
|
|
989
|
+
try {
|
|
990
|
+
if (!id || !project) {
|
|
991
|
+
target.innerHTML = '<div class="panel">' + blank('memory', 'Choose a memory', 'Open a record from your memories, context, or graph.',
|
|
992
|
+
'<a href="#memories" class="button quiet">Browse memories</a>') + '</div>';
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
const snapshot = await api(queryPath('memory', { id, project }));
|
|
996
|
+
if (request !== store.request) return false;
|
|
997
|
+
if (snapshot.memory?.id !== id || (snapshot.memory.project || snapshot.memory.projectId) !== project ||
|
|
998
|
+
!Number.isSafeInteger(snapshot.revision)) throw new Error('The server returned an unreadable memory.');
|
|
999
|
+
store.snapshot = snapshot;
|
|
1000
|
+
renderDetail(snapshot);
|
|
1001
|
+
return true;
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (request !== store.request) return false;
|
|
1004
|
+
target.innerHTML = errorMarkup(failurePrefix + error.message, error.status === 404 ? null : 'detail');
|
|
1005
|
+
return false;
|
|
1006
|
+
} finally {
|
|
1007
|
+
if (request === store.request) setBusy(target, false);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function searchLinkTargets() {
|
|
1012
|
+
const store = state.detail;
|
|
1013
|
+
if (!store.snapshot || store.saving) return;
|
|
1014
|
+
const request = ++store.targetRequest;
|
|
1015
|
+
const detailRequest = store.request;
|
|
1016
|
+
const target = $('link-search-status');
|
|
1017
|
+
target.textContent = 'Looking for memories…';
|
|
1018
|
+
try {
|
|
1019
|
+
const q = $('link-query').value.trim();
|
|
1020
|
+
const result = await api(queryPath('memories', { q, project: store.project, limit: 15 }));
|
|
1021
|
+
if (request !== store.targetRequest || detailRequest !== store.request) return;
|
|
1022
|
+
const choices = list(result.items).filter((item) => item.id !== store.id && (item.project || item.projectId) === store.project)
|
|
1023
|
+
.map((item) => ({ id: item.id, name: item.title || item.id }));
|
|
1024
|
+
optionSet('link-target', 'Choose a memory', choices, '');
|
|
1025
|
+
target.textContent = choices.length ? countLabel(choices.length, 'memory', 'memories') + ' found.' : 'No other memories matched.';
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (request === store.targetRequest && detailRequest === store.request) target.textContent = error.message;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function detailStatus(id, message, error = false) {
|
|
1032
|
+
const status = $(id);
|
|
1033
|
+
if (!status) return;
|
|
1034
|
+
status.textContent = message;
|
|
1035
|
+
status.dataset.error = String(error);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function saveDetail(kind, payload, focus) {
|
|
1039
|
+
const store = state.detail;
|
|
1040
|
+
if (!store.snapshot || store.saving) return;
|
|
1041
|
+
const { id, project, request } = store;
|
|
1042
|
+
const status = kind === 'memory-review' ? 'detail-status' : 'link-edit-status';
|
|
1043
|
+
const controls = () => $('detail-body').querySelectorAll('button, select, textarea');
|
|
1044
|
+
store.saving = true;
|
|
1045
|
+
controls().forEach((element) => { element.disabled = true; });
|
|
1046
|
+
detailStatus(status, 'Saving…');
|
|
1047
|
+
try {
|
|
1048
|
+
await api(kind, { body: { projectId: project, expectedRevision: store.snapshot.revision, ...payload } });
|
|
1049
|
+
state.graph.snapshot = null;
|
|
1050
|
+
state.overviewLoaded = false;
|
|
1051
|
+
if (state.context.pack) {
|
|
1052
|
+
state.context.dirty = true;
|
|
1053
|
+
renderContext();
|
|
1054
|
+
}
|
|
1055
|
+
if (request !== store.request || state.view !== 'memory') return;
|
|
1056
|
+
if (await loadDetail(id, project, 'Saved. Could not reload the memory. ')) detailStatus(status, 'Saved.');
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (request !== store.request || state.view !== 'memory') return;
|
|
1059
|
+
if (error.status === 409) {
|
|
1060
|
+
if (await loadDetail(id, project, 'This memory changed elsewhere. ')) detailStatus(status, 'This memory changed elsewhere. Review the latest state and try again.', true);
|
|
1061
|
+
} else {
|
|
1062
|
+
detailStatus(status, error.message, true);
|
|
1063
|
+
}
|
|
1064
|
+
} finally {
|
|
1065
|
+
store.saving = false;
|
|
1066
|
+
if (state.view === 'memory' && id === store.id && project === store.project) {
|
|
1067
|
+
controls().forEach((element) => { element.disabled = false; });
|
|
1068
|
+
if (focus === 'link-editor' && $(focus)) $(focus).querySelector('summary')?.focus();
|
|
1069
|
+
else ($(focus) || $('title-memory')).focus();
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const graphKinds = { project: 'Project', agent: 'Agent', memory: 'Memory' };
|
|
1075
|
+
const graphEdgeLabels = { 'belongs-to': 'Belongs to project', 'recorded-by': 'Recorded by agent', ...linkTypes };
|
|
1076
|
+
const graphEdgeClass = { 'belongs-to': ' project-edge', 'recorded-by': ' agent-edge' };
|
|
1077
|
+
const recordedEdge = (edge) => edge?.id && Object.hasOwn(linkTypes, edge.kind);
|
|
1078
|
+
const graphMemoryId = (node) => node.memoryId || (node.kind === 'memory' && node.id.startsWith('memory:') ? node.id.slice(7) : '');
|
|
1079
|
+
|
|
1080
|
+
function graphNeighbors(edges, id) {
|
|
1081
|
+
return edges.filter((edge) => edge.source === id || edge.target === id).map((edge) => ({
|
|
1082
|
+
id: edge.source === id ? edge.target : edge.source,
|
|
1083
|
+
edge
|
|
1084
|
+
}));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function graphPillGrid(positions, nodes, width, top, pitch, nodeWidth = pitch - 18) {
|
|
1088
|
+
const columns = Math.max(1, Math.floor((width - 28) / pitch));
|
|
1089
|
+
for (let index = 0; index < nodes.length; index += columns) {
|
|
1090
|
+
const row = nodes.slice(index, index + columns);
|
|
1091
|
+
const offset = (width - row.length * pitch) / 2 + (pitch - nodeWidth) / 2;
|
|
1092
|
+
row.forEach((node, column) => {
|
|
1093
|
+
positions.set(node.id, { x: offset + column * pitch, y: top + Math.floor(index / columns) * 45, width: nodeWidth, height: 33 });
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
return Math.ceil(nodes.length / columns) * 45;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function projectCluster(hub, memories) {
|
|
1100
|
+
const rings = [];
|
|
1101
|
+
for (let start = 0, ring = 0; start < memories.length; ring++) {
|
|
1102
|
+
const capacity = 13 + ring * 8;
|
|
1103
|
+
const members = memories.slice(start, start + capacity);
|
|
1104
|
+
rings.push({ members, rx: 86 + ring * 32, ry: 61 + ring * 29 });
|
|
1105
|
+
start += members.length;
|
|
1106
|
+
}
|
|
1107
|
+
const outer = rings.at(-1);
|
|
1108
|
+
return { hub, rings, count: memories.length, rx: (outer?.rx || 75) + 17, ry: (outer?.ry || 36) + 17 };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function packClusters(clusters, viewport, agentBands) {
|
|
1112
|
+
let best;
|
|
1113
|
+
for (let columns = 1; columns <= Math.min(clusters.length, 8); columns++) {
|
|
1114
|
+
const rows = [];
|
|
1115
|
+
for (let index = 0; index < clusters.length; index += columns) rows.push(clusters.slice(index, index + columns));
|
|
1116
|
+
const rowWidths = rows.map((row) => row.reduce((sum, cluster) => sum + cluster.rx * 2 + 19, 0));
|
|
1117
|
+
const rowHeights = rows.map((row) => Math.max(...row.map((cluster) => cluster.ry)) * 2 + 20);
|
|
1118
|
+
const width = Math.max(agentBands.width, 36 + Math.max(...rowWidths));
|
|
1119
|
+
const height = 46 + agentBands.height + rowHeights.reduce((sum, value) => sum + value, 0);
|
|
1120
|
+
const scale = Math.min(viewport.width / width, (viewport.height - 45) / height);
|
|
1121
|
+
if (!best || scale > best.scale) best = { rows, rowWidths, rowHeights, width, height, scale };
|
|
1122
|
+
}
|
|
1123
|
+
return best;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function graphLayout(nodes, edges) {
|
|
1127
|
+
const positions = new Map();
|
|
1128
|
+
const clusters = [];
|
|
1129
|
+
const order = (a, b) => String(a.label || '').localeCompare(String(b.label || '')) || String(a.id).localeCompare(String(b.id));
|
|
1130
|
+
const projects = nodes.filter((node) => node.kind === 'project').sort(order);
|
|
1131
|
+
const agents = nodes.filter((node) => node.kind === 'agent').sort(order);
|
|
1132
|
+
const memories = nodes.filter((node) => node.kind === 'memory').sort(order);
|
|
1133
|
+
const byId = new Map(memories.map((node) => [node.id, node]));
|
|
1134
|
+
const expanded = state.graph.project && projects.length === 1 && memories.length <= 25;
|
|
1135
|
+
if (!projects.length || !memories.length || expanded) {
|
|
1136
|
+
const width = Math.max(450, Math.min(5, Math.ceil(Math.sqrt(memories.length * 1.1))) * 184 + 30);
|
|
1137
|
+
let height = 28;
|
|
1138
|
+
height += graphPillGrid(positions, projects, width, height, 178, 155);
|
|
1139
|
+
if (expanded) height += 14;
|
|
1140
|
+
height += graphPillGrid(positions, memories, width, height, 184);
|
|
1141
|
+
if (expanded) height += 14;
|
|
1142
|
+
height += graphPillGrid(positions, agents, width, height, 140, 118);
|
|
1143
|
+
return { positions, clusters, width, height: height + 18 };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const groupMap = new Map(projects.map((node) => [node.id, []]));
|
|
1147
|
+
const grouped = new Set();
|
|
1148
|
+
for (const edge of edges) {
|
|
1149
|
+
if (edge.kind !== 'belongs-to') continue;
|
|
1150
|
+
const memory = byId.get(edge.target);
|
|
1151
|
+
const group = groupMap.get(edge.source);
|
|
1152
|
+
if (!memory || !group || grouped.has(memory.id)) continue;
|
|
1153
|
+
group.push(memory);
|
|
1154
|
+
grouped.add(memory.id);
|
|
1155
|
+
}
|
|
1156
|
+
const groups = projects.map((node) => projectCluster(node, groupMap.get(node.id).sort(order)));
|
|
1157
|
+
const ungrouped = memories.filter((node) => !grouped.has(node.id));
|
|
1158
|
+
const split = Math.ceil(agents.length / 2);
|
|
1159
|
+
const topAgents = agents.slice(0, split);
|
|
1160
|
+
const bottomAgents = agents.slice(split);
|
|
1161
|
+
const agentWidth = Math.min(split, 6) * 132 + 28;
|
|
1162
|
+
const agentHeight = (Math.ceil(split / 6) + Math.ceil(bottomAgents.length / 6)) * 45;
|
|
1163
|
+
const packed = packClusters(groups, graphViewport(), { width: agentWidth, height: agentHeight });
|
|
1164
|
+
const { width, rows, rowWidths, rowHeights } = packed;
|
|
1165
|
+
let y = 14 + graphPillGrid(positions, topAgents, width, 14, 132, 112) + 18;
|
|
1166
|
+
rows.forEach((row, rowIndex) => {
|
|
1167
|
+
let x = (width - rowWidths[rowIndex]) / 2 + 9.5;
|
|
1168
|
+
for (const group of row) {
|
|
1169
|
+
const cx = x + group.rx;
|
|
1170
|
+
const cy = y + rowHeights[rowIndex] / 2;
|
|
1171
|
+
clusters.push({ cx, cy, rx: group.rx, ry: group.ry });
|
|
1172
|
+
positions.set(group.hub.id, { x: cx - 66, y: cy - 21, width: 132, height: 42, count: group.count });
|
|
1173
|
+
group.rings.forEach(({ members, rx, ry }, ringIndex) => {
|
|
1174
|
+
members.forEach((node, index) => {
|
|
1175
|
+
const angle = (index + (ringIndex % 2 ? .5 : 0)) * Math.PI * 2 / members.length - Math.PI / 2;
|
|
1176
|
+
positions.set(node.id, { x: cx + Math.cos(angle) * rx - 11, y: cy + Math.sin(angle) * ry - 11, width: 22, height: 22, dot: true });
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
x += group.rx * 2 + 19;
|
|
1180
|
+
}
|
|
1181
|
+
y += rowHeights[rowIndex];
|
|
1182
|
+
});
|
|
1183
|
+
y += graphPillGrid(positions, ungrouped, width, y + 3, 184);
|
|
1184
|
+
y += graphPillGrid(positions, bottomAgents, width, y + 8, 132, 112);
|
|
1185
|
+
return { positions, clusters, width, height: y + 16 };
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function graphEdgePath(edge, layout) {
|
|
1189
|
+
const a = layout.positions.get(edge.source);
|
|
1190
|
+
const b = layout.positions.get(edge.target);
|
|
1191
|
+
if (!a || !b) return '';
|
|
1192
|
+
const ax = a.x + a.width / 2;
|
|
1193
|
+
const ay = a.y + a.height / 2;
|
|
1194
|
+
const bx = b.x + b.width / 2;
|
|
1195
|
+
const by = b.y + b.height / 2;
|
|
1196
|
+
const start = 'M' + ax + ' ' + ay;
|
|
1197
|
+
if (edge.kind === 'belongs-to') return start + ' L' + bx + ' ' + by;
|
|
1198
|
+
if (recordedEdge(edge)) {
|
|
1199
|
+
const dx = bx - ax;
|
|
1200
|
+
const dy = by - ay;
|
|
1201
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
1202
|
+
const pad = Math.min(b.width / 2 / Math.abs(dx), b.height / 2 / Math.abs(dy), .38);
|
|
1203
|
+
const lift = Math.min(63, Math.max(20, length * .25));
|
|
1204
|
+
return start + ' Q' + ((ax + bx) / 2 - dy / length * lift) + ' ' + ((ay + by) / 2 + dx / length * lift) +
|
|
1205
|
+
' ' + (bx - dx * pad) + ' ' + (by - dy * pad);
|
|
1206
|
+
}
|
|
1207
|
+
const bend = (ay + by) / 2;
|
|
1208
|
+
return start + ' C' + ax + ' ' + bend + ' ' + bx + ' ' + bend + ' ' + bx + ' ' + by;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function graphNodeMarkup(node, position, tabIndex) {
|
|
1212
|
+
const { x, y, width, height } = position;
|
|
1213
|
+
const label = compact(node.label || node.id);
|
|
1214
|
+
const hasCount = Number.isFinite(position.count);
|
|
1215
|
+
const mark = node.kind === 'project' ? '<rect class="node-glyph" x="11" y="' + (hasCount ? 10 : 12) + '" width="8" height="8" rx="1"/>'
|
|
1216
|
+
: node.kind === 'agent' ? '<circle class="node-glyph" cx="15" cy="16" r="4.5"/>' : '<path class="node-glyph" d="m15 11 5 5-5 5-5-5z"/>';
|
|
1217
|
+
const display = shorten(label, Math.floor((width - 30) / 6.1));
|
|
1218
|
+
const shape = position.dot
|
|
1219
|
+
? '<circle class="node-surface" cx="11" cy="11" r="9.5"/><path class="node-glyph" d="m11 7 4 4-4 4-4-4z"/>'
|
|
1220
|
+
: '<rect class="node-surface" width="' + width + '" height="' + height + '" rx="6"/>' + mark +
|
|
1221
|
+
'<text x="26" y="' + (hasCount ? 18 : 21) + '">' + esc(display) + '</text>' +
|
|
1222
|
+
(hasCount ? '<text class="node-count" x="26" y="33">' + countLabel(position.count, 'memory', 'memories') + '</text>' : '');
|
|
1223
|
+
return '<g class="graph-node ' + node.kind + (position.dot ? ' dot-node' : '') + '" role="button" aria-label="' + esc(graphKinds[node.kind] + ': ' + label) +
|
|
1224
|
+
'" data-node="' + esc(node.id) + '" aria-pressed="false" tabindex="' + tabIndex + '" transform="translate(' + x + ' ' + y + ')"><title>' +
|
|
1225
|
+
esc(label) + '</title>' + shape + '</g>';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function renderGraphList() {
|
|
1229
|
+
const graph = state.graph;
|
|
1230
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1231
|
+
const links = graph.edges.filter(recordedEdge).map((edge) => '<li class="graph-row"><button type="button" class="button" data-edge-record="' +
|
|
1232
|
+
esc(edge.id) + '" aria-pressed="false"><span>' + esc(byId.get(edge.source)?.label || edge.source) + ' · ' +
|
|
1233
|
+
esc(graphEdgeLabels[edge.kind]) + ' · ' + esc(byId.get(edge.target)?.label || edge.target) + '</span></button></li>').join('');
|
|
1234
|
+
$('graph-list').innerHTML = (links ? '<details open class="panel-body"><summary>Recorded memory links</summary><ul class="list">' + links + '</ul></details>' : '') +
|
|
1235
|
+
'<ul class="list">' + graph.nodes.map((node) => '<li class="graph-row"><button type="button" class="button" data-node="' +
|
|
1236
|
+
esc(node.id) + '" aria-pressed="false">' + icon(node.kind, true) + '<span>' + esc(node.label || node.id) +
|
|
1237
|
+
'</span><span class="tag ' + node.kind + '">' + graphKinds[node.kind] + '</span></button></li>').join('') + '</ul>';
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function renderGraphMap() {
|
|
1241
|
+
const graph = state.graph;
|
|
1242
|
+
const layout = graph.layout = graphLayout(graph.nodes, graph.edges);
|
|
1243
|
+
const svg = $('graph-svg');
|
|
1244
|
+
const clusters = layout.clusters.map(({ cx, cy, rx, ry }) => '<ellipse class="cluster-area" cx="' + cx + '" cy="' + cy +
|
|
1245
|
+
'" rx="' + rx + '" ry="' + ry + '"/>').join('');
|
|
1246
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1247
|
+
const structural = [];
|
|
1248
|
+
const recordedLinks = [];
|
|
1249
|
+
graph.edges.forEach((edge, i) => {
|
|
1250
|
+
const path = graphEdgePath(edge, layout);
|
|
1251
|
+
const recorded = recordedEdge(edge);
|
|
1252
|
+
const visual = '<path class="graph-edge' + (recorded ? ' recorded-edge ' + edge.kind : graphEdgeClass[edge.kind] || '') +
|
|
1253
|
+
'" data-edge="' + i + '" d="' + path + '"' + (recorded && edge.kind !== 'related' ? ' marker-end="url(#graph-arrow)"' : '') + '/>';
|
|
1254
|
+
if (recorded) {
|
|
1255
|
+
const from = byId.get(edge.source)?.label || edge.source;
|
|
1256
|
+
const to = byId.get(edge.target)?.label || edge.target;
|
|
1257
|
+
const label = from + ' · ' + graphEdgeLabels[edge.kind] + ' · ' + to;
|
|
1258
|
+
recordedLinks.push('<g class="graph-link-group" data-edge-record="' + esc(edge.id) + '" role="button" tabindex="-1" aria-pressed="false" aria-label="' +
|
|
1259
|
+
esc(label) + '"><title>' + esc(label) + '</title>' + visual + '<path class="graph-edge-hit" d="' + path + '"/></g>');
|
|
1260
|
+
} else structural.push(visual);
|
|
1261
|
+
});
|
|
1262
|
+
const nodes = graph.nodes.map((node, i) => graphNodeMarkup(node, layout.positions.get(node.id), i === 0 ? 0 : -1)).join('');
|
|
1263
|
+
svg.innerHTML = '<defs><pattern id="map-dots" width="18" height="18" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1" fill="var(--graph-dot)"/></pattern>' +
|
|
1264
|
+
'<marker id="graph-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path class="graph-arrow" d="M0 1 9 5 0 9z"/></marker></defs>' +
|
|
1265
|
+
'<rect width="100%" height="100%" fill="url(#map-dots)"/><g id="graph-transform"><g aria-hidden="true">' + clusters + structural.join('') + '</g>' +
|
|
1266
|
+
recordedLinks.join('') + nodes + '<g class="graph-tooltip" id="graph-tooltip" aria-hidden="true"></g></g>';
|
|
1267
|
+
fitGraph();
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function graphViewport() {
|
|
1271
|
+
const rect = $('graph-svg').getBoundingClientRect();
|
|
1272
|
+
return { width: Math.max(rect.width, 1), height: Math.max(rect.height, 1) };
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function updateGraphTransform() {
|
|
1276
|
+
const graph = state.graph;
|
|
1277
|
+
const transform = $('graph-transform');
|
|
1278
|
+
if (!transform || graph.mode !== 'map') return;
|
|
1279
|
+
const { width, height } = graphViewport();
|
|
1280
|
+
$('graph-svg').setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
|
1281
|
+
transform.setAttribute('transform', 'translate(' + graph.panX + ' ' + graph.panY + ') scale(' + graph.zoom + ')');
|
|
1282
|
+
$('graph-scale').textContent = Math.round(graph.zoom * 100) + '%';
|
|
1283
|
+
$('graph-out').disabled = graph.zoom <= .06;
|
|
1284
|
+
$('graph-in').disabled = graph.zoom >= 2.75;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function fitGraph() {
|
|
1288
|
+
const graph = state.graph;
|
|
1289
|
+
if (!graph.layout || graph.mode !== 'map' || $('graph-body').hidden || state.view !== 'graph') return;
|
|
1290
|
+
const { width, height } = graphViewport();
|
|
1291
|
+
const { width: mapWidth, height: mapHeight } = graph.layout;
|
|
1292
|
+
const widthScale = Math.max(1, width - 24) / mapWidth;
|
|
1293
|
+
const heightScale = Math.max(1, height - 54) / mapHeight;
|
|
1294
|
+
const zoom = Math.min(1.35, widthScale, heightScale);
|
|
1295
|
+
graph.zoom = zoom;
|
|
1296
|
+
graph.panX = (width - mapWidth * zoom) / 2;
|
|
1297
|
+
graph.panY = Math.max(0, (height - 38 - mapHeight * zoom) / 2);
|
|
1298
|
+
updateGraphTransform();
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function zoomGraph(factor, anchor) {
|
|
1302
|
+
const graph = state.graph;
|
|
1303
|
+
if (!graph.layout) return;
|
|
1304
|
+
const viewport = graphViewport();
|
|
1305
|
+
const center = anchor || { x: viewport.width / 2, y: viewport.height / 2 };
|
|
1306
|
+
const previous = graph.zoom;
|
|
1307
|
+
const target = Math.min(2.75, Math.max(Math.min(.06, previous), previous * factor));
|
|
1308
|
+
const ratio = target / previous;
|
|
1309
|
+
graph.panX = center.x - (center.x - graph.panX) * ratio;
|
|
1310
|
+
graph.panY = center.y - (center.y - graph.panY) * ratio;
|
|
1311
|
+
graph.zoom = target;
|
|
1312
|
+
updateGraphTransform();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function centerGraphNode(id) {
|
|
1316
|
+
const graph = state.graph;
|
|
1317
|
+
const position = graph.layout?.positions.get(id);
|
|
1318
|
+
if (!position || graph.mode !== 'map') return;
|
|
1319
|
+
const { width, height } = graphViewport();
|
|
1320
|
+
const zoom = Math.max(.8, graph.zoom);
|
|
1321
|
+
graph.zoom = zoom;
|
|
1322
|
+
graph.panX = width / 2 - (position.x + position.width / 2) * zoom;
|
|
1323
|
+
graph.panY = (height - 38) / 2 - (position.y + position.height / 2) * zoom;
|
|
1324
|
+
updateGraphTransform();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function renderGraphTooltip(id) {
|
|
1328
|
+
const graph = state.graph;
|
|
1329
|
+
const tooltip = $('graph-tooltip');
|
|
1330
|
+
if (!tooltip) return;
|
|
1331
|
+
const point = graph.layout.positions.get(id);
|
|
1332
|
+
const node = graph.nodes.find((item) => item.id === id);
|
|
1333
|
+
if (!point?.dot || !node) {
|
|
1334
|
+
tooltip.replaceChildren();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const label = shorten(compact(node.label || node.id), 48);
|
|
1338
|
+
const width = Math.max(70, label.length * 6.7 + 18);
|
|
1339
|
+
const x = Math.min(graph.layout.width - width - 2, Math.max(2, point.x + 11 - width / 2));
|
|
1340
|
+
const y = Math.max(1, point.y - 32);
|
|
1341
|
+
tooltip.setAttribute('transform', 'translate(' + x + ' ' + y + ')');
|
|
1342
|
+
tooltip.innerHTML = '<rect width="' + width + '" height="26" rx="5"/><text x="9" y="17">' + esc(label) + '</text>';
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function renderGraphInspector() {
|
|
1346
|
+
const graph = state.graph;
|
|
1347
|
+
const all = new Map(list(graph.snapshot?.nodes).map((node) => [node.id, node]));
|
|
1348
|
+
const selected = all.get(graph.selected);
|
|
1349
|
+
const inspector = $('graph-inspector');
|
|
1350
|
+
const selectedEdge = graph.edges.find((edge) => recordedEdge(edge) && edge.id === graph.selectedEdge);
|
|
1351
|
+
if (selectedEdge) {
|
|
1352
|
+
const from = all.get(selectedEdge.source);
|
|
1353
|
+
const to = all.get(selectedEdge.target);
|
|
1354
|
+
const record = (node) => node ? memoryLink(graphMemoryId(node), node.project, node.label || node.id) : '';
|
|
1355
|
+
inspector.innerHTML = '<div class="panel-head"><h3>Link details</h3></div><div class="panel-body">' +
|
|
1356
|
+
'<p>' + record(from) + '</p><div class="tags"><span class="tag memory">' + esc(graphEdgeLabels[selectedEdge.kind]) + '</span></div>' +
|
|
1357
|
+
'<p>' + record(to) + '</p>' + (selectedEdge.note ? '<p class="small muted detail-note">' + esc(selectedEdge.note) + '</p>' : '') + '</div>';
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (!selected) {
|
|
1361
|
+
inspector.innerHTML = '<div class="panel-head"><h3>Node details</h3></div>' +
|
|
1362
|
+
blank('graph', 'Select a node', 'See its stored details and follow its recorded connections.');
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const adjacent = graphNeighbors(graph.edges, selected.id).filter((item) => all.has(item.id));
|
|
1366
|
+
const connected = adjacent.length ? '<details open><summary>' + countLabel(adjacent.length, 'connection') + '</summary><div class="related">' +
|
|
1367
|
+
adjacent.map(({ id, edge }) => {
|
|
1368
|
+
const node = all.get(id);
|
|
1369
|
+
if (recordedEdge(edge)) return '<button type="button" class="button quiet" data-edge-record="' + esc(edge.id) +
|
|
1370
|
+
'"><span>' + esc(graphEdgeLabels[edge.kind]) + ' · ' + esc(node.label || node.id) + '</span></button>';
|
|
1371
|
+
return '<button type="button" class="button quiet" data-related="' + esc(id) + '" title="' +
|
|
1372
|
+
esc(graphEdgeLabels[edge.kind] || nice(edge.kind) || 'Connection') + '">' + icon(node.kind, true) + '<span>' +
|
|
1373
|
+
esc(node.label || node.id) + '</span></button>';
|
|
1374
|
+
}).join('') + '</div></details>' : '<p class="muted small">No connections in this view.</p>';
|
|
1375
|
+
const project = state.projects.find((item) => item.id === selected.project)?.name || selected.project;
|
|
1376
|
+
const snippet = compact(selected.snippet) && compact(selected.snippet) !== compact(selected.label)
|
|
1377
|
+
? '<p class="small">' + esc(selected.snippet) + '</p>' : '';
|
|
1378
|
+
inspector.innerHTML = '<div class="panel-head"><h3>Node details</h3></div><div class="panel-body">' +
|
|
1379
|
+
tags([[selected.kind, graphKinds[selected.kind]], ['memory', nice(selected.category)], ['state-' + (selected.effectiveState || ''), reviewLabel(selected.effectiveState)]]) +
|
|
1380
|
+
'<h3>' + (selected.kind === 'memory' ? memoryLink(graphMemoryId(selected), selected.project, selected.label || selected.id) : esc(selected.label || selected.id)) + '</h3>' +
|
|
1381
|
+
(project && selected.kind !== 'project' ? '<p class="muted small">' + esc(project) + '</p>' : '') + snippet +
|
|
1382
|
+
(selected.kind === 'memory' ? sourceMarkup([...list(selected.sources), selected.source]) : '') + connected + '</div>';
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function updateGraphSelection() {
|
|
1386
|
+
const graph = state.graph;
|
|
1387
|
+
const id = graph.selected;
|
|
1388
|
+
const selectedEdge = graph.edges.find((edge) => recordedEdge(edge) && edge.id === graph.selectedEdge);
|
|
1389
|
+
const connected = new Set(graphNeighbors(graph.edges, id).map((item) => item.id));
|
|
1390
|
+
connected.add(id);
|
|
1391
|
+
if (selectedEdge) { connected.add(selectedEdge.source); connected.add(selectedEdge.target); }
|
|
1392
|
+
const hasSelection = !!id || !!selectedEdge;
|
|
1393
|
+
const tabStop = id || selectedEdge?.source || graph.nodes[0]?.id;
|
|
1394
|
+
$('graph-body').querySelectorAll('[data-node]').forEach((element) => {
|
|
1395
|
+
const nodeId = element.dataset.node;
|
|
1396
|
+
element.setAttribute('aria-pressed', String(id === nodeId));
|
|
1397
|
+
if (element instanceof SVGElement) {
|
|
1398
|
+
element.classList.toggle('unrelated', hasSelection && !connected.has(nodeId));
|
|
1399
|
+
element.setAttribute('tabindex', nodeId === tabStop ? '0' : '-1');
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
if (graph.mode === 'map') {
|
|
1403
|
+
$('graph-svg').querySelectorAll('[data-edge]').forEach((element) => {
|
|
1404
|
+
const edge = graph.edges[Number(element.dataset.edge)];
|
|
1405
|
+
const active = edge === selectedEdge || edge.source === id || edge.target === id;
|
|
1406
|
+
element.classList.toggle('active', active);
|
|
1407
|
+
element.classList.toggle('unrelated', hasSelection && !active);
|
|
1408
|
+
});
|
|
1409
|
+
const linkTabStop = graph.selectedEdge || graph.edges.find(recordedEdge)?.id;
|
|
1410
|
+
$('graph-svg').querySelectorAll('[data-edge-record]').forEach((element) => {
|
|
1411
|
+
element.setAttribute('aria-pressed', String(element.dataset.edgeRecord === graph.selectedEdge));
|
|
1412
|
+
element.setAttribute('tabindex', element.dataset.edgeRecord === linkTabStop ? '0' : '-1');
|
|
1413
|
+
});
|
|
1414
|
+
renderGraphTooltip(id);
|
|
1415
|
+
}
|
|
1416
|
+
$('graph-list').querySelectorAll('[data-edge-record]').forEach((element) => {
|
|
1417
|
+
element.setAttribute('aria-pressed', String(element.dataset.edgeRecord === graph.selectedEdge));
|
|
1418
|
+
});
|
|
1419
|
+
renderGraphInspector();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function selectGraphNode(id, { center = false, focus = false } = {}) {
|
|
1423
|
+
const graph = state.graph;
|
|
1424
|
+
graph.selected = id;
|
|
1425
|
+
graph.selectedEdge = '';
|
|
1426
|
+
updateGraphSelection();
|
|
1427
|
+
if (center) centerGraphNode(id);
|
|
1428
|
+
if (focus) {
|
|
1429
|
+
const target = graph.mode === 'map' ? $('graph-svg') : $('graph-list');
|
|
1430
|
+
target.querySelector('[data-node="' + CSS.escape(id) + '"]')?.focus();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function selectGraphEdge(id, fromInspector) {
|
|
1435
|
+
const graph = state.graph;
|
|
1436
|
+
if (!graph.edges.some((edge) => recordedEdge(edge) && edge.id === id)) return;
|
|
1437
|
+
graph.selected = '';
|
|
1438
|
+
graph.selectedEdge = id;
|
|
1439
|
+
updateGraphSelection();
|
|
1440
|
+
if (fromInspector) $('graph-inspector').querySelector('a')?.focus();
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function renderGraph() {
|
|
1444
|
+
const graph = state.graph;
|
|
1445
|
+
if (!graph.snapshot) return;
|
|
1446
|
+
const search = $('graph-query').value.trim().toLowerCase();
|
|
1447
|
+
const kind = $('graph-kind').value;
|
|
1448
|
+
const relation = $('graph-relation').value;
|
|
1449
|
+
const nodes = list(graph.snapshot.nodes).filter((node) => Object.hasOwn(graphKinds, node.kind));
|
|
1450
|
+
const matches = nodes.filter((node) => (!kind || node.kind === kind) &&
|
|
1451
|
+
(!search || [node.label, node.snippet, node.project, node.category].filter(Boolean).join(' ').toLowerCase().includes(search)));
|
|
1452
|
+
const ids = new Set(matches.map((node) => node.id));
|
|
1453
|
+
if (search && !kind) {
|
|
1454
|
+
matches.forEach((node) => graphNeighbors(list(graph.snapshot.edges), node.id).forEach((item) => ids.add(item.id)));
|
|
1455
|
+
}
|
|
1456
|
+
const matchingEdges = list(graph.snapshot.edges).filter((edge) => ids.has(edge.source) && ids.has(edge.target) &&
|
|
1457
|
+
(!relation || (relation === 'recorded' ? recordedEdge(edge) : edge.kind === relation)));
|
|
1458
|
+
if (relation) {
|
|
1459
|
+
const endpoints = new Set(matchingEdges.flatMap((edge) => [edge.source, edge.target]));
|
|
1460
|
+
for (const id of ids) if (!endpoints.has(id)) ids.delete(id);
|
|
1461
|
+
}
|
|
1462
|
+
graph.nodes = nodes.filter((node) => ids.has(node.id));
|
|
1463
|
+
graph.edges = matchingEdges;
|
|
1464
|
+
if (graph.selected && !ids.has(graph.selected)) graph.selected = '';
|
|
1465
|
+
if (graph.selectedEdge && !graph.edges.some((edge) => recordedEdge(edge) && edge.id === graph.selectedEdge)) graph.selectedEdge = '';
|
|
1466
|
+
const matchingCount = search && matches.length !== graph.nodes.length ? countLabel(matches.length, 'match', 'matches') + ' · ' : '';
|
|
1467
|
+
$('graph-count').textContent = matchingCount + countLabel(graph.nodes.length, 'node') + ' · ' + countLabel(graph.edges.length, 'connection');
|
|
1468
|
+
$('graph-body').hidden = !graph.nodes.length;
|
|
1469
|
+
$('graph-legend').hidden = !graph.nodes.length;
|
|
1470
|
+
const note = graph.snapshot.truncated ? '<div class="panel-body small muted">Showing ' +
|
|
1471
|
+
number(graph.snapshot.shownMemories) + ' memories in this snapshot. Filter by project to explore a narrower set.</div>' : '';
|
|
1472
|
+
const focused = graph.focusMemory && graph.nodes.find((node) => graphMemoryId(node) === graph.focusMemory);
|
|
1473
|
+
const missing = graph.focusMemory && !focused ? '<p class="panel-body small muted">This memory is outside the current graph view.</p>' : '';
|
|
1474
|
+
$('graph-status').innerHTML = graph.nodes.length ? note + missing : missing + blank('graph', search || kind || relation ? 'No matching nodes' : 'No relationships yet',
|
|
1475
|
+
search || kind || relation ? 'Change the name, node type, or relationship filter.' : 'The graph fills in as project and agent memories are stored.');
|
|
1476
|
+
if (!graph.nodes.length) return;
|
|
1477
|
+
$('graph-stage').hidden = graph.mode !== 'map';
|
|
1478
|
+
$('graph-list').hidden = graph.mode !== 'list';
|
|
1479
|
+
$('graph-mode-map').setAttribute('aria-pressed', String(graph.mode === 'map'));
|
|
1480
|
+
$('graph-mode-list').setAttribute('aria-pressed', String(graph.mode === 'list'));
|
|
1481
|
+
renderGraphList();
|
|
1482
|
+
if (graph.mode === 'map') renderGraphMap();
|
|
1483
|
+
if (focused) {
|
|
1484
|
+
graph.focusMemory = '';
|
|
1485
|
+
selectGraphNode(focused.id);
|
|
1486
|
+
} else updateGraphSelection();
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
async function loadGraph(project, force = false, memory = '') {
|
|
1490
|
+
const graph = state.graph;
|
|
1491
|
+
graph.focusMemory = memory;
|
|
1492
|
+
if (!force && !memory && graph.snapshot && graph.project === project) {
|
|
1493
|
+
renderGraph();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const seq = ++graph.request;
|
|
1497
|
+
graph.controller?.abort();
|
|
1498
|
+
const controller = new AbortController();
|
|
1499
|
+
graph.controller = controller;
|
|
1500
|
+
graph.snapshot = null;
|
|
1501
|
+
graph.project = project;
|
|
1502
|
+
graph.selected = '';
|
|
1503
|
+
graph.selectedEdge = '';
|
|
1504
|
+
setBusy($('graph-panel'), true);
|
|
1505
|
+
$('graph-status').innerHTML = skeleton('Loading relationships…');
|
|
1506
|
+
$('graph-count').textContent = '';
|
|
1507
|
+
$('graph-body').hidden = true;
|
|
1508
|
+
$('graph-legend').hidden = true;
|
|
1509
|
+
try {
|
|
1510
|
+
const snapshot = await api(queryPath('graph', { project, memory }), { signal: AbortSignal.any([controller.signal, AbortSignal.timeout(12000)]) });
|
|
1511
|
+
if (seq !== graph.request) return;
|
|
1512
|
+
graph.snapshot = snapshot;
|
|
1513
|
+
renderGraph();
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
if (seq === graph.request && error.name !== 'AbortError') $('graph-status').innerHTML = errorMarkup(error.message, 'graph');
|
|
1516
|
+
} finally {
|
|
1517
|
+
if (seq === graph.request) setBusy($('graph-panel'), false);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function renderContextControls() {
|
|
1522
|
+
const input = $('context-cross');
|
|
1523
|
+
const toggle = state.features?.features;
|
|
1524
|
+
const canCross = toggle?.contextAssembly === true && toggle?.crossProjectContext === true;
|
|
1525
|
+
$('context-cross-row').hidden = !canCross;
|
|
1526
|
+
if (!canCross) input.checked = false;
|
|
1527
|
+
$('context-submit').disabled = !toggle?.contextAssembly || !state.projects.length || state.context.pending;
|
|
1528
|
+
const gate = $('context-gate');
|
|
1529
|
+
if (!toggle && state.featurePending) {
|
|
1530
|
+
gate.innerHTML = '<p class="muted small" role="status">Checking feature settings…</p>';
|
|
1531
|
+
} else if (!toggle) {
|
|
1532
|
+
gate.innerHTML = errorMarkup('Could not check context assembly settings.', 'features');
|
|
1533
|
+
} else if (!toggle.contextAssembly) {
|
|
1534
|
+
gate.innerHTML = '<p class="muted small">Context assembly is turned off. <a href="#features">Manage in Features</a></p>';
|
|
1535
|
+
} else if (!state.overviewLoaded && state.overviewPending) {
|
|
1536
|
+
gate.innerHTML = '<p class="muted small" role="status">Loading projects…</p>';
|
|
1537
|
+
} else if (!state.overviewLoaded) {
|
|
1538
|
+
gate.innerHTML = errorMarkup('Could not load projects.', 'overview');
|
|
1539
|
+
} else if (!state.projects.length) {
|
|
1540
|
+
gate.innerHTML = '<p class="muted small">Store a project memory before assembling context.</p>';
|
|
1541
|
+
} else {
|
|
1542
|
+
gate.replaceChildren();
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function contextReasonList(items, format) {
|
|
1547
|
+
return '<ul class="list">' + items.map(format).join('') + '</ul>';
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function renderContext() {
|
|
1551
|
+
const output = $('context-output');
|
|
1552
|
+
const { pack, dirty } = state.context;
|
|
1553
|
+
if (!pack) {
|
|
1554
|
+
output.innerHTML = blank('context', 'Ready for a task', 'Choose a project and describe the work to see the context and its sources here.');
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const entries = list(pack.entries);
|
|
1558
|
+
const excluded = list(pack.excluded);
|
|
1559
|
+
const budget = pack.budget || {};
|
|
1560
|
+
const warnings = list(pack.warnings);
|
|
1561
|
+
const allowed = state.features?.features?.contextAssembly === true;
|
|
1562
|
+
const canCopy = allowed && !dirty && !!pack.markdown && pack.enabled && entries.length > 0;
|
|
1563
|
+
const warningMarkup = warnings.map((warning) => '<p class="small muted">' + esc(warning) + '</p>').join('');
|
|
1564
|
+
const selection = contextReasonList(entries, (entry) => {
|
|
1565
|
+
const title = shorten(compact(entry.excerpt), 120) || nice(entry.category) || entry.id;
|
|
1566
|
+
const reason = list(entry.reasons).map((detail) => '<p class="small muted">' + esc(detail) + '</p>').join('');
|
|
1567
|
+
const links = list(entry.related).map((relation) => {
|
|
1568
|
+
const directional = { fromId: relation.direction === 'outgoing' ? entry.id : relation.otherMemoryId, type: relation.type };
|
|
1569
|
+
return '<p class="small">' + esc(linkCaption(directional, entry.id)) + ' ' +
|
|
1570
|
+
memoryLink(relation.otherMemoryId, entry.projectId, relation.title || relation.otherMemoryId) + '</p>';
|
|
1571
|
+
}).join('');
|
|
1572
|
+
return '<li><strong>' + memoryLink(entry.id, entry.projectId, title) + '</strong><div class="small muted">' +
|
|
1573
|
+
esc([entry.projectName, entry.agentTool, nice(entry.category)].filter(Boolean).join(' · ')) +
|
|
1574
|
+
'</div>' + reason + sourceMarkup(entry.sources) + links + '</li>';
|
|
1575
|
+
});
|
|
1576
|
+
const reasonLabels = {
|
|
1577
|
+
duplicate: 'Matches a memory already considered.',
|
|
1578
|
+
budget: 'Would exceed the selected budget.',
|
|
1579
|
+
limit: 'Beyond the brief’s memory limit.',
|
|
1580
|
+
empty: 'Contains no memory text.',
|
|
1581
|
+
lookback: 'Falls outside the selected time range.',
|
|
1582
|
+
archived: 'Archived in a recorded review.',
|
|
1583
|
+
contested: 'Marked as needing review.',
|
|
1584
|
+
superseded: 'Superseded by a recorded replacement.'
|
|
1585
|
+
};
|
|
1586
|
+
const rejections = contextReasonList(excluded, (entry) => '<li><strong class="mono">' +
|
|
1587
|
+
memoryLink(entry.id, entry.projectId || (pack.scope === 'project' ? pack.projectId : ''), entry.title || entry.id) +
|
|
1588
|
+
'</strong><p class="small muted">' + esc(reasonLabels[entry.reason] || nice(entry.reason)) + '</p></li>');
|
|
1589
|
+
const projectName = state.projects.find((item) => item.id === pack.projectId)?.name || pack.projectId;
|
|
1590
|
+
const scope = pack.scope === 'cross-project' ? 'Cross-project · ' + projectName : projectName;
|
|
1591
|
+
output.innerHTML = '<div class="panel-body stack">' + (dirty ? '<p class="status small" role="status">This preview is out of date. Assemble again to refresh it.</p>' : '') +
|
|
1592
|
+
'<div class="form-actions">' + tags([['project', scope], ['memory', number(budget.estimatedTokens) + ' / ' + number(budget.tokenBudget) + ' estimated tokens']]) +
|
|
1593
|
+
(canCopy ? '<button class="button toolbar-end" id="context-copy" type="button">' + icon('copy') + 'Copy brief</button>' : '') + '</div>' +
|
|
1594
|
+
'<p id="copy-status" class="small muted" role="status" hidden></p><p class="small muted">' +
|
|
1595
|
+
countLabel(entries.length, 'included memory', 'included memories') + ' · ' + number(excluded.length) + ' excluded · ' +
|
|
1596
|
+
number(budget.usedChars) + ' / ' + number(budget.maxChars) + ' characters</p>' +
|
|
1597
|
+
warningMarkup +
|
|
1598
|
+
(pack.markdown && pack.enabled && entries.length ? '<pre class="context-preview mono" id="context-text" tabindex="0" aria-label="Assembled context markdown">' + esc(pack.markdown) + '</pre>' :
|
|
1599
|
+
blank('context', pack.enabled ? 'No memories fit this task' : 'Context assembly is turned off', pack.enabled ? 'Review any exclusions or try a different task.' : 'Enable it in Features to build a brief.')) +
|
|
1600
|
+
'<div class="context-reasons">' +
|
|
1601
|
+
(entries.length ? '<details open><summary>Included · ' + number(entries.length) + '</summary>' + selection + '</details>' : '') +
|
|
1602
|
+
(excluded.length ? '<details><summary>Excluded · ' + number(excluded.length) + '</summary>' + rejections + '</details>' : '') +
|
|
1603
|
+
'</div></div>';
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
async function assembleContext() {
|
|
1607
|
+
const store = state.context;
|
|
1608
|
+
const toggle = state.features?.features;
|
|
1609
|
+
const query = $('context-task').value.trim();
|
|
1610
|
+
const projectId = $('context-project').value;
|
|
1611
|
+
const tokenBudget = Number($('context-budget').value);
|
|
1612
|
+
if (store.pending || !toggle?.contextAssembly || !projectId) return;
|
|
1613
|
+
if (!query) {
|
|
1614
|
+
$('context-task').setCustomValidity('Enter a task.');
|
|
1615
|
+
$('context-task').reportValidity();
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const body = { query, projectId, tokenBudget, includeCrossProject: toggle.crossProjectContext && $('context-cross').checked };
|
|
1619
|
+
store.pending = true;
|
|
1620
|
+
store.dirty = false;
|
|
1621
|
+
renderContextControls();
|
|
1622
|
+
setBusy($('context-result'), true);
|
|
1623
|
+
$('context-output').innerHTML = skeleton('Assembling context…');
|
|
1624
|
+
try {
|
|
1625
|
+
store.pack = await api('context', { body });
|
|
1626
|
+
renderContext();
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
store.pack = null;
|
|
1629
|
+
$('context-output').innerHTML = errorMarkup(error.message, 'context');
|
|
1630
|
+
} finally {
|
|
1631
|
+
store.pending = false;
|
|
1632
|
+
setBusy($('context-result'), false);
|
|
1633
|
+
renderContextControls();
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function copyContext() {
|
|
1638
|
+
const store = state.context;
|
|
1639
|
+
const status = $('copy-status');
|
|
1640
|
+
if (!store.pack?.markdown || store.dirty || !status) return;
|
|
1641
|
+
try {
|
|
1642
|
+
await navigator.clipboard.writeText(store.pack.markdown);
|
|
1643
|
+
status.textContent = 'Brief copied.';
|
|
1644
|
+
} catch {
|
|
1645
|
+
status.textContent = 'Clipboard access is unavailable. Select and copy the preview text.';
|
|
1646
|
+
}
|
|
1647
|
+
status.hidden = false;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const checkStates = { pass: ['ok', 'Passed'], warn: ['warn', 'Review'], unknown: ['', 'Unknown'], error: ['error', 'Error'] };
|
|
1651
|
+
const installStates = {
|
|
1652
|
+
complete: ['ok', 'Files present'], partial: ['warn', 'Some files present'], missing: ['', 'Files missing'],
|
|
1653
|
+
'not-checked': ['', 'Unchecked'], blocked: ['error', 'Check blocked']
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
function checkTag(value, labels) {
|
|
1657
|
+
const [kind, label] = Object.hasOwn(labels, value) ? labels[value] : ['', 'Unknown'];
|
|
1658
|
+
return '<span class="tag check-' + kind + '">' + label + '</span>';
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function commandMarkup(command) {
|
|
1662
|
+
if (typeof command !== 'string' || !command.trim()) return '';
|
|
1663
|
+
return '<div class="form-actions"><code class="mono">' + esc(command) + '</code><button type="button" class="button quiet" data-copy-command="' +
|
|
1664
|
+
esc(command) + '" aria-label="Copy command">' + icon('copy', true) + 'Copy</button><span class="small muted" role="status"></span></div>';
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function docsLink(value) {
|
|
1668
|
+
if (typeof value !== 'string') return '';
|
|
1669
|
+
try {
|
|
1670
|
+
const url = new URL(value);
|
|
1671
|
+
if (url.protocol !== 'https:') return '';
|
|
1672
|
+
return '<a class="button quiet" href="' + esc(url.href) + '" target="_blank" rel="noopener noreferrer">Documentation ' +
|
|
1673
|
+
icon('diagonal', true) + '</a>';
|
|
1674
|
+
} catch { return ''; }
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function renderIntegrations(data) {
|
|
1678
|
+
const checks = list(data.checks);
|
|
1679
|
+
const agents = list(data.agents);
|
|
1680
|
+
const checkRows = checks.map((check) => '<li class="check-row"><h3>' + esc(check.label || check.id) + '</h3><div class="check-result"><p class="small muted">' +
|
|
1681
|
+
esc(check.detail) + '</p>' + commandMarkup(check.command) + '</div>' + checkTag(check.status, checkStates) + '</li>').join('');
|
|
1682
|
+
const agentRows = agents.map((agent) => '<li class="check-row"><h3>' + esc(agent.label || agent.id) + '</h3><div class="check-result">' +
|
|
1683
|
+
tags(list(agent.capabilities).map((value) => ['memory', value])) + (agent.detail ? '<p class="small muted">' + esc(agent.detail) + '</p>' : '') +
|
|
1684
|
+
(agent.note ? '<p class="small muted">' + esc(agent.note) + '</p>' : '') + commandMarkup(agent.command) +
|
|
1685
|
+
'<div class="form-actions">' + docsLink(agent.docs) + '</div></div>' + checkTag(agent.installation, installStates) + '</li>').join('');
|
|
1686
|
+
$('integrations-body').innerHTML = '<div class="stack"><section class="panel"><div class="panel-head"><div><h2>Local diagnostics</h2>' +
|
|
1687
|
+
(data.checkedAt ? '<p class="small muted">Last checked ' + dateMarkup(data.checkedAt) + '</p>' : '') + '</div>' +
|
|
1688
|
+
(typeof data.diagnostics === 'string' && data.diagnostics ? '<div class="form-actions"><button class="button" type="button" id="diagnostics-copy">' + icon('copy', true) +
|
|
1689
|
+
'Copy report</button><span class="small muted" role="status"></span></div>' : '') + '</div>' +
|
|
1690
|
+
(checkRows ? '<ul class="list">' + checkRows + '</ul>' : blank('activity', 'No diagnostic results', 'Run checks to refresh the local status.')) +
|
|
1691
|
+
'</section><section class="panel"><div class="panel-head"><div><h2>Supported agents</h2>' +
|
|
1692
|
+
'<p class="small muted">Installation status reflects managed files in the checked scope.</p></div></div>' +
|
|
1693
|
+
(agentRows ? '<ul class="list">' + agentRows + '</ul>' : blank('integrations', 'No agent data', 'Run checks to load the support catalog.')) +
|
|
1694
|
+
'</section></div>';
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
async function loadIntegrations() {
|
|
1698
|
+
const store = state.integrations;
|
|
1699
|
+
if (store.pending) return;
|
|
1700
|
+
store.pending = true;
|
|
1701
|
+
const target = $('integrations-body');
|
|
1702
|
+
$('integrations-refresh').disabled = true;
|
|
1703
|
+
setBusy(target, true);
|
|
1704
|
+
target.innerHTML = '<div class="panel">' + skeleton('Running local checks…') + '</div>';
|
|
1705
|
+
try {
|
|
1706
|
+
store.snapshot = await api('integrations');
|
|
1707
|
+
renderIntegrations(store.snapshot);
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
target.innerHTML = errorMarkup(error.message, 'integrations');
|
|
1710
|
+
} finally {
|
|
1711
|
+
store.pending = false;
|
|
1712
|
+
$('integrations-refresh').disabled = false;
|
|
1713
|
+
setBusy(target, false);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
async function copyIntegrationText(value, message, button) {
|
|
1718
|
+
const status = button.nextElementSibling;
|
|
1719
|
+
if (!value || !status) return;
|
|
1720
|
+
try {
|
|
1721
|
+
await navigator.clipboard.writeText(value);
|
|
1722
|
+
status.textContent = message;
|
|
1723
|
+
} catch {
|
|
1724
|
+
status.textContent = 'Clipboard access is unavailable.';
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function renderFeatures() {
|
|
1729
|
+
const target = $('feature-list');
|
|
1730
|
+
if (!state.features) return;
|
|
1731
|
+
const current = state.features.features;
|
|
1732
|
+
const overrides = new Map(list(state.features.definitions).map((item) => [item.id, item]));
|
|
1733
|
+
target.innerHTML = '<ul class="list">' + definitions.map((definition) => {
|
|
1734
|
+
const display = overrides.get(definition.id) || definition;
|
|
1735
|
+
const on = current[definition.id] === true;
|
|
1736
|
+
return '<li class="setting"><div><h3 id="label-' + definition.id + '">' + esc(display.label) + '</h3><p class="muted small" id="desc-' +
|
|
1737
|
+
definition.id + '">' + esc(display.description) + '</p></div><button type="button" class="button switch" role="switch" aria-checked="' +
|
|
1738
|
+
on + '" aria-labelledby="label-' + definition.id + '" aria-describedby="desc-' + definition.id + '" data-feature="' + definition.id + '"' +
|
|
1739
|
+
(state.featureSaving ? ' disabled' : '') + '><span aria-hidden="true">' + (on ? 'On' : 'Off') + '</span><span class="switch-track" aria-hidden="true"></span></button></li>';
|
|
1740
|
+
}).join('') + '</ul>';
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function applyFeatures(snapshot) {
|
|
1744
|
+
const before = state.features?.features;
|
|
1745
|
+
const current = snapshot.features;
|
|
1746
|
+
if (!current || !Number.isSafeInteger(snapshot.revision) || !definitions.every(({ id }) => typeof current[id] === 'boolean')) {
|
|
1747
|
+
throw new Error('The server returned unreadable feature settings.');
|
|
1748
|
+
}
|
|
1749
|
+
if (state.features && snapshot.revision < state.features.revision) return;
|
|
1750
|
+
state.features = snapshot;
|
|
1751
|
+
state.activityEnabled = current.liveActivity;
|
|
1752
|
+
if (before?.crossProjectContext && !current.crossProjectContext && state.context.pack?.scope === 'cross-project') state.context.dirty = true;
|
|
1753
|
+
renderFeatures();
|
|
1754
|
+
renderActivity();
|
|
1755
|
+
renderContextControls();
|
|
1756
|
+
if (state.context.pack && !state.context.pending) renderContext();
|
|
1757
|
+
if (current.liveActivity && before?.liveActivity !== true) void refreshActivity();
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function featureStatus(message, error = false) {
|
|
1761
|
+
const target = $('feature-status');
|
|
1762
|
+
target.textContent = message;
|
|
1763
|
+
target.dataset.error = String(error);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
async function loadFeatures(force = false) {
|
|
1767
|
+
if (state.featurePending) return state.featurePending;
|
|
1768
|
+
if (state.features && !force) return;
|
|
1769
|
+
const target = $('feature-list');
|
|
1770
|
+
if (!state.features) target.innerHTML = skeleton('Loading feature settings…');
|
|
1771
|
+
setBusy(target, true);
|
|
1772
|
+
state.featurePending = (async () => {
|
|
1773
|
+
try {
|
|
1774
|
+
applyFeatures(await api('features'));
|
|
1775
|
+
return true;
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
if (!state.features) target.innerHTML = errorMarkup(error.message, 'features');
|
|
1778
|
+
else if (!state.featureSaving) featureStatus(error.message, true);
|
|
1779
|
+
return false;
|
|
1780
|
+
} finally {
|
|
1781
|
+
state.featurePending = null;
|
|
1782
|
+
setBusy(target, false);
|
|
1783
|
+
renderContextControls();
|
|
1784
|
+
}
|
|
1785
|
+
})();
|
|
1786
|
+
renderContextControls();
|
|
1787
|
+
return state.featurePending;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
async function toggleFeature(key) {
|
|
1791
|
+
if (state.featureSaving || !state.features || !definitions.some((definition) => definition.id === key)) return;
|
|
1792
|
+
state.featureSaving = true;
|
|
1793
|
+
$('feature-list').querySelectorAll('[data-feature]').forEach((button) => { button.disabled = true; });
|
|
1794
|
+
featureStatus('Saving…');
|
|
1795
|
+
try {
|
|
1796
|
+
const body = { patch: { [key]: !state.features.features[key] }, expectedRevision: state.features.revision };
|
|
1797
|
+
applyFeatures(await api('features', { body }));
|
|
1798
|
+
featureStatus('Saved.');
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
if (error.status === 409) {
|
|
1801
|
+
const refreshed = await loadFeatures(true);
|
|
1802
|
+
featureStatus(refreshed ? 'Settings changed elsewhere. Review the current settings and try again.' :
|
|
1803
|
+
'Settings changed elsewhere. Reload Features to see the current settings.', true);
|
|
1804
|
+
} else {
|
|
1805
|
+
featureStatus(error.message, true);
|
|
1806
|
+
}
|
|
1807
|
+
} finally {
|
|
1808
|
+
state.featureSaving = false;
|
|
1809
|
+
renderFeatures();
|
|
1810
|
+
if (state.view === 'features') $('feature-list').querySelector('[data-feature="' + key + '"]')?.focus();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function setPreference(key, value, announce = true) {
|
|
1815
|
+
document.documentElement.dataset[key] = value;
|
|
1816
|
+
let persisted = true;
|
|
1817
|
+
if (announce) {
|
|
1818
|
+
try {
|
|
1819
|
+
localStorage.setItem('shadow-brain:workspace:' + key, value);
|
|
1820
|
+
} catch { persisted = false; }
|
|
1821
|
+
const status = $('display-status');
|
|
1822
|
+
status.hidden = false;
|
|
1823
|
+
status.textContent = persisted ? 'Display preference saved on this device.' : 'Display preference applied for this session.';
|
|
1824
|
+
}
|
|
1825
|
+
$('theme-setting').value = document.documentElement.dataset.theme;
|
|
1826
|
+
$('motion-setting').value = document.documentElement.dataset.motion;
|
|
1827
|
+
const dark = document.documentElement.dataset.theme === 'dark';
|
|
1828
|
+
const label = dark ? 'Switch to light theme' : 'Switch to dark theme';
|
|
1829
|
+
const button = $('theme-toggle');
|
|
1830
|
+
button.innerHTML = icon(dark ? 'sun' : 'moon');
|
|
1831
|
+
button.title = label;
|
|
1832
|
+
button.setAttribute('aria-label', label);
|
|
1833
|
+
if (announce && !persisted && state.view !== 'features') {
|
|
1834
|
+
$('workspace-notice').hidden = false;
|
|
1835
|
+
$('workspace-notice').textContent = 'Display preference applied for this session.';
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function routeParams() {
|
|
1840
|
+
return new URLSearchParams(location.hash.split('?')[1] || '');
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function changeMemorySearch() {
|
|
1844
|
+
const query = memoryQuery();
|
|
1845
|
+
const route = routeLink('memories', query);
|
|
1846
|
+
if (location.hash !== route) location.hash = route;
|
|
1847
|
+
else void loadMemories(query);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function changeView(moveFocus = false) {
|
|
1851
|
+
const hash = location.hash.slice(1).split('?')[0];
|
|
1852
|
+
const view = Object.hasOwn(viewNames, hash) ? hash : 'overview';
|
|
1853
|
+
const previous = state.view;
|
|
1854
|
+
const previousRoute = state.route;
|
|
1855
|
+
state.view = view;
|
|
1856
|
+
state.route = location.hash || '#overview';
|
|
1857
|
+
$('crumb').textContent = viewNames[view];
|
|
1858
|
+
document.title = viewNames[view] + ' · Agent Shadow Brain';
|
|
1859
|
+
for (const name of Object.keys(viewNames)) {
|
|
1860
|
+
const section = $('page-' + name);
|
|
1861
|
+
section.hidden = name !== view;
|
|
1862
|
+
section.classList.remove('entered');
|
|
1863
|
+
const link = document.querySelector('[data-view="' + name + '"]');
|
|
1864
|
+
if (name === view || name === 'memories' && view === 'memory') link?.setAttribute('aria-current', 'page');
|
|
1865
|
+
else link?.removeAttribute('aria-current');
|
|
1866
|
+
}
|
|
1867
|
+
if (previous !== view) $('page-' + view).classList.add('entered');
|
|
1868
|
+
if (moveFocus && (previous !== view || view === 'memory')) {
|
|
1869
|
+
window.scrollTo(0, 0);
|
|
1870
|
+
$('title-' + view).focus({ preventScroll: true });
|
|
1871
|
+
}
|
|
1872
|
+
const params = routeParams();
|
|
1873
|
+
const project = params.get('project') || '';
|
|
1874
|
+
switch (view) {
|
|
1875
|
+
case 'overview':
|
|
1876
|
+
void loadOverview(previous !== '' && previous !== 'overview', project);
|
|
1877
|
+
void loadFeatures();
|
|
1878
|
+
break;
|
|
1879
|
+
case 'memories': {
|
|
1880
|
+
const query = { q: params.get('q') || '', project, category: params.get('category') || '', agent: params.get('agent') || '', state: params.get('state') || '' };
|
|
1881
|
+
$('memory-query').value = query.q;
|
|
1882
|
+
if (state.projects.length) optionSet('memory-project', 'All projects', state.projects, query.project);
|
|
1883
|
+
$('memory-state').value = query.state;
|
|
1884
|
+
void loadMemories(query);
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
case 'memory': {
|
|
1888
|
+
if (previous && previous !== 'memory') state.detail.back = previousRoute;
|
|
1889
|
+
const back = $('detail-back');
|
|
1890
|
+
const name = state.detail.back.slice(1).split('?')[0];
|
|
1891
|
+
back.href = state.detail.back;
|
|
1892
|
+
back.innerHTML = icon('left', true) + esc(viewNames[name] || 'Memories');
|
|
1893
|
+
void loadDetail(params.get('id') || '', project);
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
case 'graph': {
|
|
1897
|
+
void loadOverview();
|
|
1898
|
+
if (project) optionSet('graph-project', 'All projects', state.projects, project);
|
|
1899
|
+
else $('graph-project').value = '';
|
|
1900
|
+
const memory = params.get('memory') || '';
|
|
1901
|
+
if (memory) ['graph-query', 'graph-kind', 'graph-relation'].forEach((id) => { $(id).value = ''; });
|
|
1902
|
+
void loadGraph(project, previous !== '' && previous !== 'graph', memory);
|
|
1903
|
+
break;
|
|
1904
|
+
}
|
|
1905
|
+
case 'context':
|
|
1906
|
+
if (project) {
|
|
1907
|
+
state.context.routeProject = project;
|
|
1908
|
+
syncProjectOptions();
|
|
1909
|
+
if (state.context.pack && state.context.pack.projectId !== project) {
|
|
1910
|
+
state.context.dirty = true;
|
|
1911
|
+
renderContext();
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
void loadOverview();
|
|
1915
|
+
void loadFeatures(previous !== '');
|
|
1916
|
+
renderContextControls();
|
|
1917
|
+
break;
|
|
1918
|
+
case 'features':
|
|
1919
|
+
featureStatus('');
|
|
1920
|
+
void loadFeatures(true);
|
|
1921
|
+
break;
|
|
1922
|
+
case 'integrations':
|
|
1923
|
+
if (state.integrations.snapshot) renderIntegrations(state.integrations.snapshot);
|
|
1924
|
+
else void loadIntegrations();
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function graphSelectFromList(id, fromInspector) {
|
|
1930
|
+
if (!state.graph.nodes.some((node) => node.id === id)) {
|
|
1931
|
+
$('graph-query').value = '';
|
|
1932
|
+
$('graph-kind').value = '';
|
|
1933
|
+
$('graph-relation').value = '';
|
|
1934
|
+
renderGraph();
|
|
1935
|
+
}
|
|
1936
|
+
selectGraphNode(id, { center: fromInspector, focus: fromInspector });
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function handleGraphKeyboard(event) {
|
|
1940
|
+
const edge = event.target.closest('[data-edge-record]');
|
|
1941
|
+
if (edge) {
|
|
1942
|
+
const links = Array.from($('graph-svg').querySelectorAll('[data-edge-record]'));
|
|
1943
|
+
const index = links.indexOf(edge);
|
|
1944
|
+
const nextIndex = { ArrowLeft: Math.max(index - 1, 0), ArrowRight: Math.min(index + 1, links.length - 1), Home: 0, End: links.length - 1 }[event.key];
|
|
1945
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1946
|
+
event.preventDefault();
|
|
1947
|
+
selectGraphEdge(edge.dataset.edgeRecord);
|
|
1948
|
+
} else if (Number.isInteger(nextIndex)) {
|
|
1949
|
+
event.preventDefault();
|
|
1950
|
+
const next = links[nextIndex];
|
|
1951
|
+
selectGraphEdge(next.dataset.edgeRecord);
|
|
1952
|
+
next.focus();
|
|
1953
|
+
}
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const target = event.target.closest('[data-node]');
|
|
1957
|
+
if (!target) return;
|
|
1958
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1959
|
+
event.preventDefault();
|
|
1960
|
+
selectGraphNode(target.dataset.node, { center: true });
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
1964
|
+
event.preventDefault();
|
|
1965
|
+
const graph = state.graph;
|
|
1966
|
+
const { positions } = graph.layout;
|
|
1967
|
+
const points = [...positions.entries()].map(([id, point]) => ({ id, x: point.x + point.width / 2, y: point.y + point.height / 2 }));
|
|
1968
|
+
let next;
|
|
1969
|
+
if (event.key === 'Home' || event.key === 'End') {
|
|
1970
|
+
points.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
1971
|
+
next = event.key === 'Home' ? points[0] : points.at(-1);
|
|
1972
|
+
} else {
|
|
1973
|
+
const current = points.find((point) => point.id === target.dataset.node);
|
|
1974
|
+
const horizontal = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
|
|
1975
|
+
const sign = event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 1;
|
|
1976
|
+
const axis = horizontal ? 'x' : 'y';
|
|
1977
|
+
const other = horizontal ? 'y' : 'x';
|
|
1978
|
+
const score = (point) => Math.abs(point[axis] - current[axis]) + Math.abs(point[other] - current[other]) * 1.4;
|
|
1979
|
+
next = points.filter((point) => (point[axis] - current[axis]) * sign > 3).sort((a, b) => score(a) - score(b))[0];
|
|
1980
|
+
}
|
|
1981
|
+
if (next) selectGraphNode(next.id, { center: true, focus: true });
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
$('theme-toggle').addEventListener('click', () => setPreference('theme', document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'));
|
|
1985
|
+
$('theme-setting').addEventListener('change', (event) => setPreference('theme', event.target.value));
|
|
1986
|
+
$('motion-setting').addEventListener('change', (event) => setPreference('motion', event.target.value));
|
|
1987
|
+
$('memory-form').addEventListener('submit', (event) => { event.preventDefault(); changeMemorySearch(); });
|
|
1988
|
+
['memory-project', 'memory-category', 'memory-agent', 'memory-state'].forEach((id) => $(id).addEventListener('change', changeMemorySearch));
|
|
1989
|
+
$('overview-body').addEventListener('change', (event) => {
|
|
1990
|
+
if (event.target.id !== 'overview-quality-project') return;
|
|
1991
|
+
const project = event.target.value;
|
|
1992
|
+
const route = routeLink('overview', { project });
|
|
1993
|
+
window.history.replaceState(null, '', route);
|
|
1994
|
+
state.route = route;
|
|
1995
|
+
void loadOverview(true, project).then(() => {
|
|
1996
|
+
if (state.view === 'overview' && state.route === route) $('overview-quality-project')?.focus();
|
|
1997
|
+
});
|
|
1998
|
+
});
|
|
1999
|
+
$('integrations-refresh').addEventListener('click', () => { void loadIntegrations(); });
|
|
2000
|
+
$('graph-query').addEventListener('input', renderGraph);
|
|
2001
|
+
$('graph-kind').addEventListener('change', renderGraph);
|
|
2002
|
+
$('graph-relation').addEventListener('change', renderGraph);
|
|
2003
|
+
$('graph-project').addEventListener('change', (event) => {
|
|
2004
|
+
const route = routeLink('graph', { project: event.target.value });
|
|
2005
|
+
if (location.hash !== route) location.hash = route;
|
|
2006
|
+
else void loadGraph(event.target.value);
|
|
2007
|
+
});
|
|
2008
|
+
['map', 'list'].forEach((mode) => $('graph-mode-' + mode).addEventListener('click', () => {
|
|
2009
|
+
state.graph.mode = mode;
|
|
2010
|
+
$('graph-mode-map').setAttribute('aria-pressed', String(mode === 'map'));
|
|
2011
|
+
$('graph-mode-list').setAttribute('aria-pressed', String(mode === 'list'));
|
|
2012
|
+
renderGraph();
|
|
2013
|
+
}));
|
|
2014
|
+
$('graph-fit').addEventListener('click', () => fitGraph());
|
|
2015
|
+
$('graph-in').addEventListener('click', () => zoomGraph(1.35));
|
|
2016
|
+
$('graph-out').addEventListener('click', () => zoomGraph(1 / 1.35));
|
|
2017
|
+
$('graph-svg').addEventListener('keydown', handleGraphKeyboard);
|
|
2018
|
+
$('graph-svg').addEventListener('focusin', (event) => {
|
|
2019
|
+
const node = event.target.closest('[data-node]');
|
|
2020
|
+
if (node) renderGraphTooltip(node.dataset.node);
|
|
2021
|
+
});
|
|
2022
|
+
$('graph-svg').addEventListener('focusout', () => renderGraphTooltip(state.graph.selected));
|
|
2023
|
+
$('graph-svg').addEventListener('pointerdown', (event) => {
|
|
2024
|
+
if (event.button !== 0 || !event.isPrimary || event.target.closest('[data-node],[data-edge-record]')) return;
|
|
2025
|
+
const graph = state.graph;
|
|
2026
|
+
graph.drag = { id: event.pointerId, x: event.clientX - graph.panX, y: event.clientY - graph.panY };
|
|
2027
|
+
$('graph-svg').setPointerCapture(event.pointerId);
|
|
2028
|
+
$('graph-svg').classList.add('dragging');
|
|
2029
|
+
});
|
|
2030
|
+
$('graph-svg').addEventListener('pointermove', (event) => {
|
|
2031
|
+
const graph = state.graph;
|
|
2032
|
+
if (graph.drag?.id !== event.pointerId) return;
|
|
2033
|
+
graph.panX = event.clientX - graph.drag.x;
|
|
2034
|
+
graph.panY = event.clientY - graph.drag.y;
|
|
2035
|
+
updateGraphTransform();
|
|
2036
|
+
});
|
|
2037
|
+
$('graph-svg').addEventListener('lostpointercapture', () => {
|
|
2038
|
+
state.graph.drag = null;
|
|
2039
|
+
$('graph-svg').classList.remove('dragging');
|
|
2040
|
+
});
|
|
2041
|
+
window.addEventListener('resize', () => {
|
|
2042
|
+
if (state.view === 'graph' && state.graph.mode === 'map' && state.graph.nodes.length) {
|
|
2043
|
+
renderGraphMap();
|
|
2044
|
+
updateGraphSelection();
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
$('context-form').addEventListener('submit', (event) => { event.preventDefault(); void assembleContext(); });
|
|
2049
|
+
$('context-form').addEventListener('input', () => {
|
|
2050
|
+
$('context-task').setCustomValidity('');
|
|
2051
|
+
const store = state.context;
|
|
2052
|
+
if (store.pack || store.pending) {
|
|
2053
|
+
if (!store.dirty) {
|
|
2054
|
+
store.dirty = true;
|
|
2055
|
+
if (!store.pending) renderContext();
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
$('detail-body').addEventListener('submit', (event) => {
|
|
2061
|
+
const { id } = state.detail;
|
|
2062
|
+
switch (event.target.id) {
|
|
2063
|
+
case 'review-form':
|
|
2064
|
+
event.preventDefault();
|
|
2065
|
+
void saveDetail('memory-review', { memoryId: id, state: $('review-state').value, note: $('review-note').value.trim() }, 'review-submit');
|
|
2066
|
+
break;
|
|
2067
|
+
case 'link-search':
|
|
2068
|
+
event.preventDefault();
|
|
2069
|
+
void searchLinkTargets();
|
|
2070
|
+
break;
|
|
2071
|
+
case 'link-form':
|
|
2072
|
+
event.preventDefault();
|
|
2073
|
+
void saveDetail('memory-link', { fromId: id, toId: $('link-target').value, type: $('link-type').value, note: $('link-note').value.trim() }, 'link-editor');
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
document.addEventListener('click', (event) => {
|
|
2079
|
+
const element = event.target.closest('[data-retry],[data-memory-clear],[data-memory-page],[data-node],[data-related],[data-edge-record],[data-feature],[data-unlink],[data-copy-command],#diagnostics-copy,#context-copy,.skip');
|
|
2080
|
+
if (!element) return;
|
|
2081
|
+
const data = element.dataset;
|
|
2082
|
+
if (data.retry) {
|
|
2083
|
+
switch (data.retry) {
|
|
2084
|
+
case 'overview': void loadOverview(true, state.view === 'overview' ? routeParams().get('project') || '' : ''); break;
|
|
2085
|
+
case 'memories': void loadMemories(state.memory.query || memoryQuery(), state.memory.offset); break;
|
|
2086
|
+
case 'graph': void loadGraph(state.graph.project, true, state.graph.focusMemory); break;
|
|
2087
|
+
case 'detail': void loadDetail(state.detail.id, state.detail.project); break;
|
|
2088
|
+
case 'integrations': void loadIntegrations(); break;
|
|
2089
|
+
case 'context': $('context-form').requestSubmit(); break;
|
|
2090
|
+
case 'features': void loadFeatures(true); break;
|
|
2091
|
+
}
|
|
2092
|
+
} else if (Object.hasOwn(data, 'memoryClear')) {
|
|
2093
|
+
['memory-query', 'memory-project', 'memory-category', 'memory-agent', 'memory-state'].forEach((id) => { $(id).value = ''; });
|
|
2094
|
+
$('memory-query').focus();
|
|
2095
|
+
changeMemorySearch();
|
|
2096
|
+
} else if (data.memoryPage) {
|
|
2097
|
+
const { query, offset, limit } = state.memory;
|
|
2098
|
+
const step = data.memoryPage === 'next' ? limit : -limit;
|
|
2099
|
+
$('memory-search').focus();
|
|
2100
|
+
void loadMemories(query || memoryQuery(), Math.max(0, offset + step));
|
|
2101
|
+
} else if (data.node) {
|
|
2102
|
+
selectGraphNode(data.node);
|
|
2103
|
+
} else if (data.related) {
|
|
2104
|
+
graphSelectFromList(data.related, true);
|
|
2105
|
+
} else if (data.edgeRecord) {
|
|
2106
|
+
selectGraphEdge(data.edgeRecord, $('graph-inspector').contains(element));
|
|
2107
|
+
} else if (data.unlink) {
|
|
2108
|
+
void saveDetail('memory-unlink', { memoryId: state.detail.id, linkId: data.unlink }, 'link-editor');
|
|
2109
|
+
} else if (data.copyCommand) {
|
|
2110
|
+
void copyIntegrationText(data.copyCommand, 'Command copied.', element);
|
|
2111
|
+
} else if (data.feature) {
|
|
2112
|
+
void toggleFeature(data.feature);
|
|
2113
|
+
} else if (element.id === 'context-copy') {
|
|
2114
|
+
void copyContext();
|
|
2115
|
+
} else if (element.id === 'diagnostics-copy') {
|
|
2116
|
+
void copyIntegrationText(state.integrations.snapshot?.diagnostics, 'Diagnostic report copied.', element);
|
|
2117
|
+
} else if (element.classList.contains('skip')) {
|
|
2118
|
+
event.preventDefault();
|
|
2119
|
+
$('main-content').focus();
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
window.addEventListener('hashchange', () => changeView(true));
|
|
2124
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) void refreshActivity(); });
|
|
2125
|
+
setInterval(() => { void refreshActivity(); }, 10000);
|
|
2126
|
+
setPreference('theme', document.documentElement.dataset.theme, false);
|
|
2127
|
+
renderContext();
|
|
2128
|
+
changeView();
|
|
2129
|
+
</script>
|
|
2130
|
+
</body>
|
|
2131
|
+
</html>`;
|
|
2132
|
+
//# sourceMappingURL=workspace-html.js.map
|